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

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 spheres = new List();
private ComputeBuffer sphereBuffer;

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

private void Awake()
{
_camera = GetComponent();
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
{
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), которая характеризует отражённый свет от поверхности материала.

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

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