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

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 albedo;
    }

    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), albedo = 0.9f },
                    new Sphere { origin = new Vector3(2, 0, 0), radius = 1, color = new Vector3(0, 1, 0), albedo = 0.8f },
                    new Sphere { origin = new Vector3(-2, 0, 0), radius = 1, color = new Vector3(0, 0, 1), albedo = 0.7f }
            };

            sphereBuffer = new ComputeBuffer(spheres.Count, sizeof(float) * (3 + 1 + 3 + 1)); // 32 bytes
            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);
            Vector3 lightDirection = new Vector3(1, 0, 1).normalized;
            rayTracingShader.SetVector("_LightDirection", new Vector4(lightDirection.x, lightDirection.y, lightDirection.z, 0));
            rayTracingShader.SetFloat("_LightIntensity", 2.0f);
    }

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

Compute Shader для трассировки лучей RayTracing.compute
#pragma kernel CSMain

#define M_PI 3.14159265359

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

RWTexture2D Result;
Texture2D _SkyboxTexture;
SamplerState sampler_SkyboxTexture;

cbuffer SceneData
{
matrix _CameraToWorld;
matrix _CameraInverseProjection;
int _NumSpheres;
float4 _LightDirection;
float _LightIntensity;
}

StructuredBuffer _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;
}
}

bool IsInShadow(float3 p, float3 lightDir)
{
float shadowHitDistance;
float epsilon = 0.001;


for (int i = 0; i < _NumSpheres; i++)
{
    Sphere sphere = _Spheres[i];
    if (RaySphereIntersection(p + lightDir * epsilon, -lightDir, sphere, shadowHitDistance))
    {
        if (shadowHitDistance > 0)
        {
            return true;
        }
    }
}

return false;
}

float3 Raytrace(float3 origin, float3 direction)
{
float hitDistance;
float3 hitColor = float3(0, 0, 0);
float minDist = 1e30;
Sphere bestHit;


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

if (minDist < 1000.0)
{
    float3 hitPoint = origin + direction * minDist;
    float3 normal = normalize(hitPoint - bestHit.origin);
    
    float3 lightDir = normalize(_LightDirection.xyz);
    bool inShadow = IsInShadow(hitPoint, lightDir);

    if (!inShadow)
    {
        float3 lightColor = saturate(dot(normal, -lightDir)) * _LightIntensity * bestHit.color * bestHit.albedo;
        hitColor = lightColor;
    }
}
else
{
    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);
}

Необходимо добавить отражения, чтобы в результе сферы стали как бы "стеклянными", прозрачными, отражая пространство вокруг (skybox). Можно использовать следующую идею:

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

Отражаться до тех пор, пока не кончится энергия
Отражаться до тех пор, пока не пройдём всё количество отражений
Если "попали" в сферу, просто принимает её цвет
Если попали в Skybox - обнуляем энергию луча

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