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

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 к сфере:

Отражаться до тех пор, пока не кончится энергия
Отражаться до тех пор, пока не пройдём всё количество отражений

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