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

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 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();
		}
	}
}

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

#pragma kernel CSMain

#define M_PI 3.14159265359

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

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

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

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

bool IsInShadow(float3 point, float3 lightDir)
{
    float shadowHitDistance;
    float epsilon = 0.001; // небольшое смещение для предотвращения самопересечения

    for (int i = 0; i < _NumSpheres; i++)
    {
        Sphere sphere = _Spheres[i];
        if (RaySphereIntersection(point + 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);
}

Необходимо добавить отражения, используя следующую идею:

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

То есть сферы должны стать как бы "стеклянными", отражая окружающий skybox

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