У нас имеется:

1. C# скрипт для настройки параметров и передачи их в шейдер RayTracingMaster.cs:

using System.Collections.Generic;
using UnityEngine;

public class RayTracingMaster : MonoBehaviour
{
	public ComputeShader rayTracingShader;
	public Texture skyboxTexture;

	private RenderTexture target;
	private Camera _camera;
	private List<Sphere> spheres = new List<Sphere>();
	private ComputeBuffer sphereBuffer;

	struct Sphere
	{
		public Vector3 origin;
		public float radius;
		public Vector3 color;
		public float padding;
	}

	private void Awake()
	{
		_camera = GetComponent<Camera>();
		SetupScene();
	}

	private void OnRenderImage(RenderTexture source, RenderTexture destination)
	{
		InitRenderTexture();
		SetComputeShaderParameters();
		int threadGroupsX = Mathf.CeilToInt(Screen.width / 8.0f);
		int threadGroupsY = Mathf.CeilToInt(Screen.height / 8.0f);
		rayTracingShader.Dispatch(0, threadGroupsX, threadGroupsY, 1);
		Graphics.Blit(target, destination);
	}

	private void SetupScene()
	{
		spheres = new List<Sphere>
		{
			new Sphere { origin = new Vector3(0, 0, 0), radius = 1, color = new Vector3(1, 0, 0) },
			new Sphere { origin = new Vector3(2, 0, 0), radius = 1, color = new Vector3(0, 1, 0) },
			new Sphere { origin = new Vector3(-2, 0, 0), radius = 1, color = new Vector3(0, 0, 1) }
		};

		sphereBuffer = new ComputeBuffer(spheres.Count, sizeof(float) * 8);
		sphereBuffer.SetData(spheres);
	}

	private void InitRenderTexture()
	{
		if (target == null || target.width != Screen.width || target.height != Screen.height)
		{
			if (target != null)
			{
				target.Release();
			}
			target = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
			target.enableRandomWrite = true;
			target.Create();
		}
	}

	private void SetComputeShaderParameters()
	{
		rayTracingShader.SetTexture(0, "Result", target);
		rayTracingShader.SetTexture(0, "_SkyboxTexture", skyboxTexture);
		rayTracingShader.SetBuffer(0, "_Spheres", sphereBuffer);
		rayTracingShader.SetMatrix("_CameraToWorld", _camera.cameraToWorldMatrix);
		rayTracingShader.SetMatrix("_CameraInverseProjection", _camera.projectionMatrix.inverse);
		rayTracingShader.SetInt("_NumSpheres", spheres.Count);
	}

	private void OnDestroy()
	{
		if (sphereBuffer != null)
		{
			sphereBuffer.Release();
		}
	}
}

2. Compute Shader для трассировки лучей RayTracing.compute:

#pragma kernel CSMain

#define M_PI 3.14159265359

struct Sphere
{
    float3 origin;
    float radius;
    float3 color;
    float padding;
};

RWTexture2D<float4> Result;
Texture2D<float4> _SkyboxTexture;
SamplerState sampler_SkyboxTexture;

cbuffer SceneData
{
    matrix _CameraToWorld;
    matrix _CameraInverseProjection;
    int _NumSpheres;
}

StructuredBuffer<Sphere> _Spheres;

bool RaySphereIntersection(float3 origin, float3 direction, Sphere sphere, out float hitDistance)
{
    float3 oc = origin - sphere.origin;
    float a = dot(direction, direction);
    float b = dot(oc, direction) * 2.0;
    float c = dot(oc, oc) - sphere.radius * sphere.radius;
    float discriminant = b * b - 4.0 * a * c;

    if (discriminant < 0)
    {
        hitDistance = -1.0;
        return false;
    }
    else
    {
        hitDistance = (-b - sqrt(discriminant)) / (2.0 * a);
        return true;
    }
}

float3 Raytrace(float3 origin, float3 direction)
{
    float hitDistance;
    float3 hitColor = float3(0, 0, 0);

    for (int i = 0; i < _NumSpheres; i++)
    {
        Sphere sphere = _Spheres[i];
        if (RaySphereIntersection(origin, direction, sphere, hitDistance))
        {
            if (hitDistance < 1000.0)
            {
                hitColor = sphere.color;
            }
        }
    }

    if (dot(hitColor, float3(1.0, 1.0, 1.0)) == 0.0)
    {
        float theta = atan2(direction.z, direction.x) + M_PI;
        float phi = acos(direction.y);
        float2 uv = float2(theta / (2.0 * M_PI), phi / M_PI);
        hitColor = _SkyboxTexture.SampleLevel(sampler_SkyboxTexture, uv, 0.0).rgb;
    }

    return hitColor;
}

[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    uint width, height;
    Result.GetDimensions(width, height);

    float2 uv = float2(id.xy) / float2(width, height) * 2.0f - 1.0f;
    float4 origin = mul(_CameraToWorld, float4(0, 0, 0, 1));
    float3 direction = normalize(mul(_CameraToWorld, mul(_CameraInverseProjection, float4(uv, 0, 1)).xyz));

    float3 color = Raytrace(origin.xyz, direction);
    Result[id.xy] = float4(color, 1.0);
}

Используя эти описанные выше скрипт и шейдер необходимо добавить освещение, отражение и тени

Добавление освещения:
У нас есть: нормаль сферы, источник освещения, сила освещения. Введём дополнительное свойство материала: Albedo, представляет собой долю света, которая рассеивается на материале, а не отражается:
float3 lightColor = saturate(dot(bestHit.normal, -LightDirection)) * LightIntensity * sphereColor * albedo

Добавление тени:
Создать "теневой луч" и отследить его путь (если пересёкся с объектом, значит мы в тени)
Создать луч: 
- начало луча - смещённая точка "вхождения в сферу"
- направление луча - противоположное к источнику освещения
Произвести "обратную" трассировку:
- нашли объект - возвращаем чёрный цвет

Добавление отражения:
Ввести понятие "энергии луча". Ввести понятие "количества отражений". Ввести новую характеристику материала (specular), которая характеризует отражённый свет от поверхности материала.
Цель - перемещаем цвет от сфере к сфере или от skybox к сфере:
- Отражаться до тех пор, пока не кончится энергия
- Отражаться до тех пор, пока не пройдём всё количество отражений
- Если "попали" в сферу, просто принимает её цвет
- Если попали в Skybox - обнуляем энергию луча

Мне интересно, как будут полностью выглядеть скрипт и шейдер с этими добавлениями