using System;
using System.Collections.Generic;
using Unity.Mathematics;
using System.Runtime.InteropServices;
using UnityEngine.Rendering;
using UnityEngine.Rendering.UnifiedRayTracing;
namespace UnityEngine.PathTracing.Core
{
using InstanceHandle = Handle<World.InstanceKey>;
using InstanceHandleSet = HandleSet<World.InstanceKey>;
using LightHandle = Handle<World.LightDescriptor>;
using LightHandleSet = HandleSet<World.LightDescriptor>;
using MaterialHandle = Handle<MaterialPool.MaterialDescriptor>;
using MaterialHandleSet = HandleSet<MaterialPool.MaterialDescriptor>;
internal enum RenderedGameObjectsFilter
{
All = 0,
OnlyStatic = 1,
AllInCameraRaysThenOnlyStatic = 2
}
internal enum LightPickingMethod
{
Uniform = 0,
Regir,
LightGrid
}
internal enum InstanceFlags
{
DIRECT_RAY_VIS_MASK = 1,
INDIRECT_RAY_VIS_MASK = 2,
SHADOW_RAY_VIS_MASK = 4,
CURRENT_LOD_FOR_LIGHTMAP_INSTANCE = 8,
LOD_ZERO_FOR_LIGHTMAP_INSTANCE = 16,
CURRENT_LOD_FOR_LIGHTMAP_INSTANCE_SHADOW = 32,
LOD_ZERO_FOR_LIGHTMAP_INSTANCE_SHADOW = 64
}
internal class World : IDisposable
{
internal const int EMISSIVE_MESH = 8; // Must match the EMISSIVE_MESH define in Common.hlsl
internal const int ENVIRONMENT_LIGHT = 9; // Must match the ENVIRONMENT_LIGHT define in Common.hlsl
// This trivial type only exists so that handles can be type-safe.
// If we make an InstanceDescriptor type, we can use that instead.
internal readonly struct InstanceKey { }
internal struct LightDescriptor
{
public LightType Type;
public Vector3 LinearLightColor;
public LightShadows Shadows;
public Matrix4x4 Transform;
public float ColorTemperature;
public LightmapBakeType LightmapBakeType;
public Experimental.GlobalIllumination.FalloffType FalloffType;
public Vector2 AreaSize;
public float SpotAngle;
public float InnerSpotAngle;
public uint CullingMask;
public float BounceIntensity;
public float Range;
public int ShadowMaskChannel;
public bool UseColorTemperature;
public float ShadowRadius;
public Texture CookieTexture;
public float CookieSize;
}
[StructLayout(LayoutKind.Sequential)]
internal struct PTLight // Must match the PTLight definition in Common.hlsl
{
public Vector3 position;
public int type;
public Vector3 intensity;
public int castShadows;
public Vector3 forward;
public int contributesToDirectLighting;
public Vector4 attenuation;
public Vector3 up;
public float width;
public Vector3 right;
public float height;
public uint layerMask;
public float indirectScale;
public float spotAngle;
public float innerSpotAngle;
public float range;
public int shadowMaskChannel;
public int falloffIndex;
public float shadowRadius;
public int cookieIndex;
public override readonly int GetHashCode()
{
return HashCode.Combine(position, type, intensity, castShadows, forward, contributesToDirectLighting, range)
^ HashCode.Combine(attenuation, up, width, right, height, layerMask, indirectScale, falloffIndex);
}
}
[StructLayout(LayoutKind.Sequential)]
internal struct ThinReservoir
{
public int LightIndex;
public float Weight;
}
private readonly InstanceHandleSet _instanceHandleSet = new();
private readonly MaterialHandleSet _materialHandleSet = new();
#region Lighting State
internal class LightState
{
public LightPickingMethod lightPickingMethod;
// Holds a list of light sources in the scene
public readonly List<PTLight> LightList = new(64);
public ComputeBuffer LightListBuffer;
// Holds a list of light falloff LUTs
public const uint LightFalloffLUTLength = 1024;
public List<LightFalloffDesc> LightFalloffDescs = new();
public float[] LightFalloff = null; // The light falloff LUT tables
public float[] LightFalloffLUTRanges = null; // The range that each LUT applies to
public ComputeBuffer LightFalloffBuffer;
public ComputeBuffer LightFalloffLUTRangeBuffer;
// Light dictionary (Handle, Light entry)
public Dictionary<LightHandle, PTLight> LightHandleToLightListEntry = new();
// Lookup table from LightHandle to the corresponding index in LightList. Only used for baking.
public Dictionary<LightHandle, int> LightHandleToLightListIndex = new();
// Mesh light dictionary (SubMeshHash, emissive mesh entry)
public Dictionary<int, PTLight> MeshLights = new();
// Light handle set
public LightHandleSet LightHandleSet = new();
public bool HasEnvironmentLight = false;
public int EnvLightCount => HasEnvironmentLight ? 1 : 0;
public int MeshLightCount => MeshLights.Count;
public int LightCount => LightHandleToLightListEntry.Count + MeshLightCount + EnvLightCount;
private static PTLight CreateEnvironmentLight()
{
PTLight envLight;
envLight.type = ENVIRONMENT_LIGHT;
envLight.position = Vector3.zero;
envLight.intensity = Vector3.zero;
envLight.castShadows = 1;
envLight.contributesToDirectLighting = 1;
envLight.forward = Vector3.zero;
envLight.attenuation = Vector4.one;
envLight.up = Vector3.zero;
envLight.right = Vector3.zero;
envLight.width = 0;
envLight.height = 0;
envLight.spotAngle = 0;
envLight.innerSpotAngle = 0;
envLight.layerMask = uint.MaxValue;
envLight.indirectScale = 1.0f;
envLight.attenuation = Vector4.zero;
envLight.range = float.MaxValue;
envLight.shadowMaskChannel = -1;
envLight.falloffIndex = -1;
envLight.shadowRadius = 0.0f;
envLight.cookieIndex = -1;
return envLight;
}
public void Build(Bounds sceneBounds, CommandBuffer cmdBuf, bool addEnvironmentLight)
{
// Make sure the light list is empty before we build it.
LightList.Clear();
LightHandleToLightListIndex.Clear();
// Combine the mesh lights and the rest of the lights.
foreach (var light in LightHandleToLightListEntry)
{
LightHandleToLightListIndex.Add(light.Key, LightList.Count);
LightList.Add(light.Value);
}
foreach (var meshLight in MeshLights)
LightList.Add(meshLight.Value);
// If we explicitly sample emitters, include the environment light in the light list
if (addEnvironmentLight)
{
LightList.Add(CreateEnvironmentLight());
HasEnvironmentLight = true;
}
BuildLightFalloffLUTs(this);
SetLightDataOnCommandBuffer(this, cmdBuf);
}
private static void BuildLightFalloffLUTs(LightState lightState)
{
// Build the LUT data
lightState.LightFalloff = LightFalloffLUT.BuildLightFalloffLUTs(lightState.LightFalloffDescs.ToArray(), LightFalloffLUTLength);
Debug.Assert(lightState.LightFalloff.Length == LightFalloffLUTLength * lightState.LightFalloffDescs.Count);
// Store the range of each LUT
lightState.LightFalloffLUTRanges = new float[lightState.LightFalloffDescs.Count];
int i = 0;
foreach (var desc in lightState.LightFalloffDescs)
{
Debug.Assert(desc.LUTRange >= 0.0f);
lightState.LightFalloffLUTRanges[i] = desc.LUTRange;
i++;
}
// We have built the LUTs, now clear the descriptors so we don't rebuild them every frame
lightState.LightFalloffDescs.Clear();
}
private static void SetLightDataOnCommandBuffer(LightState lightState, CommandBuffer cmdBuf)
{
if (lightState.LightListBuffer == null || lightState.LightListBuffer.count < lightState.LightList.Count)
{
lightState.LightListBuffer?.Release();
lightState.LightListBuffer = new ComputeBuffer(math.max(64, lightState.LightList.Count), Marshal.SizeOf<PTLight>());
}
if (lightState.LightList.Count > 0)
{
cmdBuf.SetBufferData(lightState.LightListBuffer, lightState.LightList, 0, 0, lightState.LightList.Count);
}
if (lightState.LightFalloffBuffer == null || lightState.LightFalloffBuffer.count < lightState.LightFalloff.Length)
{
lightState.LightFalloffBuffer?.Release();
int count = math.max(1, lightState.LightFalloff.Length);
int stride = sizeof(float);
lightState.LightFalloffBuffer = new ComputeBuffer(count, stride);
}
if (lightState.LightFalloff.Length > 0)
{
cmdBuf.SetBufferData(lightState.LightFalloffBuffer, lightState.LightFalloff, 0, 0, lightState.LightFalloff.Length);
}
if (lightState.LightFalloffLUTRangeBuffer == null || lightState.LightFalloffLUTRangeBuffer.count < lightState.LightFalloffLUTRanges.Length)
{
lightState.LightFalloffLUTRangeBuffer?.Release();
int count = math.max(1, lightState.LightFalloffLUTRanges.Length);
int stride = sizeof(float);
lightState.LightFalloffLUTRangeBuffer = new ComputeBuffer(count, stride);
}
if (lightState.LightFalloffLUTRanges.Length > 0)
{
cmdBuf.SetBufferData(lightState.LightFalloffLUTRangeBuffer, lightState.LightFalloffLUTRanges, 0, 0, lightState.LightFalloffLUTRanges.Length);
}
}
}
private LightState _lightState;
public LightPickingMethod lightPickingMethod
{
set => _lightState.lightPickingMethod = value;
get => _lightState.lightPickingMethod;
}
public int MaterialCount => _materialPool.MaterialCount;
public int LightCount => NonMeshLightCount + MeshLightCount + EnvLightCount;
public int NonMeshLightCount => _lightState.LightHandleToLightListEntry.Count;
public int MeshLightCount => _lightState.MeshLights.Count;
public int EnvLightCount => _lightState.HasEnvironmentLight ? 1 : 0;
public List<PTLight> LightList => _lightState.LightList;
public Dictionary<LightHandle, int> LightHandleToLightListIndex => _lightState.LightHandleToLightListIndex;
public ComputeBuffer LightListBuffer => _lightState.LightListBuffer;
public ComputeBuffer LightFalloffBuffer => _lightState.LightFalloffBuffer;
public ComputeBuffer LightFalloffLUTRangeBuffer => _lightState.LightFalloffLUTRangeBuffer;
public uint LightFalloffLUTLength => LightState.LightFalloffLUTLength;
public int LightListHashCode
{
get
{
int lightHashCode = 0;
foreach (var light in _lightState.LightList)
lightHashCode = HashCode.Combine(lightHashCode, light.GetHashCode());
return lightHashCode;
}
}
// SubMesh dictionary (instance handle, emissive submesh list)
private readonly Dictionary<InstanceHandle, List<int>> _subMeshIndices = new();
#endregion
private MaterialPool _materialPool;
private AccelStructAdapter _rayTracingAccelerationStructure;
// Skybox sampling
private CubemapRender _cubemapRender;
private EnvironmentImportanceSampling _environmentSampling;
private int _currentSkyboxHash;
// Many light sampling
private RegirLightGrid _reservoirGrid;
private ConservativeLightGrid _conservativeLightGrid;
public void Init(RayTracingContext ctx, WorldResourceSet worldResources)
{
_materialPool = new MaterialPool(worldResources.SetAlphaChannelShader, worldResources.BlitCubemap, worldResources.BlitGrayScaleCookie);
var options = new AccelerationStructureOptions()
{
buildFlags = BuildFlags.None, // TODO: Consider whether to use BuildFlags.MinimizeMemory once https://jira.unity3d.com/browse/UUM-54575 is fixed.
};
_rayTracingAccelerationStructure = new AccelStructAdapter(ctx.CreateAccelerationStructure(options), new GeometryPool(GeometryPoolDesc.NewDefault(), ctx.Resources.geometryPoolKernels, ctx.Resources.copyBuffer));
_lightState = new LightState();
_cubemapRender = new CubemapRender(worldResources.SkyBoxMesh, worldResources.SixFaceSkyBoxMesh);
_environmentSampling = new EnvironmentImportanceSampling(worldResources.EnvironmentImportanceSamplingBuild);
_reservoirGrid = new RegirLightGrid(worldResources.BuildLightGridShader);
_conservativeLightGrid = new ConservativeLightGrid(worldResources.BuildLightGridShader);
}
private static float Luminance(Color color)
{
return color.r * 0.2126f + color.g * 0.7152f + color.b * 0.0722f;
}
public void SetEnvironmentMaterial(Material mat)
{
_cubemapRender.SetMaterial(mat);
}
public ComputeBuffer GetMaterialListBuffer()
{
return _materialPool.MaterialBuffer;
}
public RenderTexture GetMaterialAlbedoTextures()
{
return _materialPool.AlbedoTextures;
}
public RenderTexture GetMaterialEmissionTextures()
{
return _materialPool.EmissionTextures;
}
public RenderTexture GetMaterialTransmissionTextures()
{
return _materialPool.TransmissionTextures;
}
public RenderTexture GetLightCookieTextures()
{
return _materialPool.LightCookieTextures;
}
public RenderTexture GetLightCubemapTextures()
{
return _materialPool.LightCubemapTextures;
}
public Texture GetEnvironmentTexture(CommandBuffer cmd, int resolution, out EnvironmentCDF environmentCDF)
{
Debug.Assert(_environmentSampling != null, "You should call World::Init() first");
var envTex = _cubemapRender.GetCubemap(resolution, out int skyHash);
if (_currentSkyboxHash != skyHash)
{
_environmentSampling.ComputeCDFBuffers(cmd, envTex);
_currentSkyboxHash = skyHash;
}
environmentCDF = _environmentSampling.GetSkyboxCDF();
return envTex;
}
public void BindLightAccelerationStructure(CommandBuffer cmd, IRayTracingShader shader)
{
// As we don't use shader variants, we need to bind buffers for all types of supported light sampling
// Some of them will be dummy (see the underlying implementation)
if (_lightState.lightPickingMethod == LightPickingMethod.Regir)
_reservoirGrid.Bind(cmd, shader);
else
_conservativeLightGrid.Bind(cmd, shader);
}
public void Dispose()
{
_rayTracingAccelerationStructure?.Dispose();
_materialPool?.Dispose();
_subMeshIndices.Clear();
_lightState.LightListBuffer?.Dispose();
_lightState.LightFalloffBuffer?.Dispose();
_lightState.LightFalloffLUTRangeBuffer?.Dispose();
_lightState.LightFalloffDescs.Clear();
_lightState.LightFalloff = null;
_lightState.LightList.Clear();
_lightState.LightHandleToLightListEntry.Clear();
_lightState.MeshLights.Clear();
_cubemapRender?.Dispose();
_environmentSampling?.Dispose();
_reservoirGrid?.Dispose();
_conservativeLightGrid?.Dispose();
}
public AccelStructAdapter GetAccelerationStructure()
{
return _rayTracingAccelerationStructure;
}
public void NextFrame()
{
_rayTracingAccelerationStructure.NextFrame();
}
// Provide a unique hash for the renderer sub meshes
private static int GetSubMeshHash(InstanceHandle instance, int subMeshIndex)
{
return HashCode.Combine(instance.Value, subMeshIndex);
}
public void RemoveInstance(InstanceHandle instance)
{
try
{
_rayTracingAccelerationStructure.RemoveInstance(instance.Value);
_instanceHandleSet.Remove(instance);
RemoveEmissiveMeshes(instance);
}
catch (Exception e)
{
LogException("Failed to remove instance", e, instance.Value);
}
}
private void RemoveEmissiveMeshes(InstanceHandle instance)
{
if (_subMeshIndices.ContainsKey(instance))
{
var emissiveSubMeshes = _subMeshIndices[instance];
foreach (var subMeshIndex in emissiveSubMeshes)
{
var subMeshHash = GetSubMeshHash(instance, subMeshIndex);
if (_lightState.MeshLights.ContainsKey(subMeshHash))
_lightState.MeshLights.Remove(subMeshHash);
}
_subMeshIndices.Remove(instance);
}
}
public void RemoveMaterial(MaterialHandle materialHandle)
{
try
{
_materialHandleSet.Remove(materialHandle);
_materialPool.RemoveMaterial(materialHandle.Value);
}
catch (Exception e)
{
LogException("failed to remove material", e, materialHandle.Value);
}
}
public MaterialHandle AddMaterial(in MaterialPool.MaterialDescriptor material, UVChannel albedoAndEmissionUVChannel)
{
MaterialHandle handle = _materialHandleSet.Add();
try
{
_materialPool.AddMaterial(handle.Value, in material, albedoAndEmissionUVChannel);
}
catch (Exception e)
{
LogException("failed to add material", e, handle.Value);
}
return handle;
}
public void UpdateMaterial(MaterialHandle materialHandle, in MaterialPool.MaterialDescriptor material, UVChannel albedoAndEmissionUVChannel)
{
try
{
_materialPool.UpdateMaterial(materialHandle.Value, in material, albedoAndEmissionUVChannel);
}
catch (Exception e)
{
LogException("failed to modify material", e, materialHandle.Value);
}
}
private void LogException(string message, Exception e, Object obj)
{
var objName = obj != null ? obj.name : "null";
Debug.LogError($"PathTracing: {message} <{objName}> \n{e.Message}", obj);
}
private void LogException(string message, Exception e, UInt64 instanceHandle)
{
Debug.LogError($"PathTracing: {message} <{instanceHandle}> \n{e.Message}");
}
private void LogError(string message)
{
Debug.LogError($"PathTracing: {message} \n");
}
public InstanceHandle AddInstance(
Mesh mesh,
Span<MaterialHandle> materials,
Span<uint> masks,
uint renderingLayerMask,
in Matrix4x4 localToWorldMatrix,
Bounds bounds,
bool isStatic,
RenderedGameObjectsFilter filter,
bool enableEmissiveSampling)
{
Debug.Assert(mesh.subMeshCount == materials.Length);
Debug.Assert(mesh.subMeshCount == masks.Length);
Span<uint> materialIndices = stackalloc uint[mesh.subMeshCount];
Span<bool> isOpaque = stackalloc bool[mesh.subMeshCount];
for (int i = 0; i < materials.Length; ++i)
{
if (materials[i] == MaterialHandle.Invalid)
continue;
bool isTransmissive = false;
_materialPool.GetMaterialInfo(materials[i].Value, out materialIndices[i], out isTransmissive);
isOpaque[i] = !isTransmissive;
}
InstanceHandle instance = _instanceHandleSet.Add();
_rayTracingAccelerationStructure.AddInstance(instance.Value, mesh, localToWorldMatrix, masks, materialIndices, isOpaque, renderingLayerMask);
if (enableEmissiveSampling && !ProcessEmissiveMeshes(instance, mesh, bounds, materials, isStatic, _rayTracingAccelerationStructure, _materialPool, filter, _lightState.MeshLights, _subMeshIndices))
LogError($"Failed to process emissive triangles in mesh {mesh.name}.");
return instance;
}
public void UpdateInstanceTransform(InstanceHandle instance, Matrix4x4 localToWorldMatrix)
{
_rayTracingAccelerationStructure.UpdateInstanceTransform(instance.Value, localToWorldMatrix);
}
public void UpdateInstanceMask(InstanceHandle instance, Span<uint> perSubMeshMask)
{
_rayTracingAccelerationStructure.UpdateInstanceMask(instance.Value, perSubMeshMask);
}
public void UpdateInstanceMask(InstanceHandle instance, uint mask)
{
_rayTracingAccelerationStructure.UpdateInstanceMask(instance.Value, mask);
}
public void UpdateInstanceMaterials(InstanceHandle instance, Span<MaterialHandle> materials)
{
Span<uint> materialIndices = stackalloc uint[materials.Length];
for (int i = 0; i < materials.Length; ++i)
{
_materialPool.GetMaterialInfo(materials[i].Value, out materialIndices[i], out bool isTransmissive);
}
_rayTracingAccelerationStructure.UpdateInstanceMaterialIDs(instance.Value, materialIndices);
}
public void UpdateInstanceEmission(
InstanceHandle instance,
Mesh mesh,
Bounds bounds,
Span<MaterialHandle> materials,
bool isStatic,
RenderedGameObjectsFilter filter)
{
if (!ProcessEmissiveMeshes(instance, mesh, bounds, materials, isStatic, _rayTracingAccelerationStructure, _materialPool, filter, _lightState.MeshLights, _subMeshIndices))
{
LogError($"failed to process emissive triangles in mesh with handle {instance}");
}
}
// InstanceMask bit encoding format:
internal static uint GetInstanceMask(ShadowCastingMode shadowMode, bool isStatic, RenderedGameObjectsFilter filter, bool hasLightmaps = true)
{
uint instanceMask = 0u;
if (shadowMode != ShadowCastingMode.Off)
{
if (filter == RenderedGameObjectsFilter.All)
{
instanceMask |= (uint)InstanceFlags.SHADOW_RAY_VIS_MASK;
}
else
{
if (isStatic)
{
instanceMask |= (uint)InstanceFlags.SHADOW_RAY_VIS_MASK;
}
}
}
if (shadowMode != ShadowCastingMode.ShadowsOnly)
{
if (filter == RenderedGameObjectsFilter.All)
{
instanceMask |= (uint)InstanceFlags.DIRECT_RAY_VIS_MASK;
instanceMask |= (uint)InstanceFlags.INDIRECT_RAY_VIS_MASK;
}
else if (filter == RenderedGameObjectsFilter.OnlyStatic)
{
if (isStatic)
{
if (hasLightmaps)
{
instanceMask |= (uint)InstanceFlags.DIRECT_RAY_VIS_MASK;
}
instanceMask |= (uint)InstanceFlags.INDIRECT_RAY_VIS_MASK;
}
}
else if (filter == RenderedGameObjectsFilter.AllInCameraRaysThenOnlyStatic)
{
if (isStatic)
{
instanceMask |= (uint)InstanceFlags.DIRECT_RAY_VIS_MASK;
instanceMask |= (uint)InstanceFlags.INDIRECT_RAY_VIS_MASK;
}
else
{
instanceMask |= (uint)InstanceFlags.DIRECT_RAY_VIS_MASK;
}
}
}
return instanceMask;
}
private static bool ProcessEmissiveMeshes(
InstanceHandle instance,
Mesh mesh,
Bounds bounds,
Span<MaterialHandle> materials,
bool isStatic,
AccelStructAdapter rtAccelStruct,
MaterialPool sceneMaterials,
RenderedGameObjectsFilter filter,
Dictionary<int, PTLight> meshLights,
Dictionary<InstanceHandle, List<int>> subMeshIndexMap)
{
if (filter != RenderedGameObjectsFilter.All && !isStatic)
return true;
int subMeshCount = mesh.subMeshCount;
if (!rtAccelStruct.GetInstanceIDs(instance.Value, out var instanceHandles))
{
// This should never happen as long as the renderer was already added to the acceleration structure
return false;
}
// Approximate area of the emissive mesh using the bounding box
float boundingBoxArea = 2 * (bounds.size.x * bounds.size.y) +
2 * (bounds.size.y * bounds.size.z) +
2 * (bounds.size.x * bounds.size.z);
// List to keep track of the emissive subMeshes
List<int> subMeshIndices = new List<int>(subMeshCount);
for (int i = 0; i < subMeshCount; ++i)
{
MaterialHandle mat = materials[i];
// If it's an emissive material, create emissive triangles for this submesh
if (sceneMaterials.IsEmissive(mat.Value, out float3 emission))
{
var triangleIndices = mesh.GetTriangles(i);
var triangleCount = triangleIndices.Length / 3;
// create a new MeshLight entry
PTLight newLight;
{
newLight.type = EMISSIVE_MESH;
newLight.height = instanceHandles[i];
newLight.attenuation = Vector4.one;
newLight.attenuation.x = triangleCount; // number of emissive triangles
newLight.intensity = emission;
newLight.position = Vector3.zero;
newLight.up = Vector3.zero;
newLight.right = Vector3.zero;
newLight.forward = Vector3.zero;
newLight.width = boundingBoxArea;
newLight.castShadows = 1;
newLight.contributesToDirectLighting = 1;
newLight.indirectScale = 1.0f;
newLight.spotAngle = 0;
newLight.innerSpotAngle = 0;
newLight.layerMask = uint.MaxValue;
newLight.range = float.MaxValue;
newLight.shadowMaskChannel = -1;
newLight.falloffIndex = -1;
newLight.shadowRadius = 0.0f;
newLight.cookieIndex = -1; // mesh lights sample directly the emission texture, they don't have a light cookie
}
var subMeshHash = GetSubMeshHash(instance, i);
meshLights[subMeshHash] = newLight;
// keep track of which subMeshes are emissive (need this for when removing meshes)
subMeshIndices.Add(i);
}
}
if (subMeshIndices.Count > 0)
{
subMeshIndexMap[instance] = subMeshIndices;
}
return true;
}
public LightHandle[] AddLights(Span<LightDescriptor> lights,
bool respectLightLayers,
bool autoEstimateLUTRange,
MixedLightingMode mixedLightingMode)
{
// Generate handles
LightHandle[] handles = new LightHandle[lights.Length];
for (int i = 0; i < lights.Length; i++)
{
LightHandle handle = _lightState.LightHandleSet.Add();
handles[i] = handle;
}
// Use the handles to update the lights for the first time
UpdateLights(handles, lights, respectLightLayers, autoEstimateLUTRange, mixedLightingMode);
return handles;
}
private static float EstimateLUTRange(float range, float luminance, Experimental.GlobalIllumination.FalloffType falloffType, float threshold = 0.01f)
{
Debug.Assert(threshold > 0.0f);
Debug.Assert(range > 0.0f);
if (luminance <= 0.0f)
return 1.0f;
switch (falloffType)
{
case Experimental.GlobalIllumination.FalloffType.InverseSquaredNoRangeAttenuation:
case Experimental.GlobalIllumination.FalloffType.InverseSquared:
{
// compute the range at which the attenuated luminance is below the threshold
float estimatedRange = math.max(1.0f, math.ceil(math.sqrt(luminance / threshold)));
Debug.Assert(luminance * LightFalloffLUT.InverseSquaredFalloff(estimatedRange * estimatedRange) <= threshold);
return math.min(estimatedRange, range);
}
case Experimental.GlobalIllumination.FalloffType.Linear:
case Experimental.GlobalIllumination.FalloffType.Legacy:
return range;
}
return range;
}
public void UpdateLights(LightHandle[] lightHandles, Span<LightDescriptor> lightDescriptors,
bool respectLightLayers,
bool autoEstimateLUTRange,
MixedLightingMode mixedLightingMode)
{
Debug.Assert(lightHandles.Length == lightDescriptors.Length);
Dictionary<int, int> falloffHashToFalloffIndex = new();
int falloffIndex = 0;
// Convert the lights.
for (int i = 0; i < lightHandles.Length; i++)
{
ref readonly LightDescriptor light = ref lightDescriptors[i];
PTLight newLight;
newLight.position = light.Transform.GetPosition();
newLight.intensity = light.LinearLightColor;
newLight.type = (int)light.Type;
newLight.castShadows = light.Shadows != LightShadows.None ? 1 : 0;
newLight.forward = (light.Transform.rotation * Vector3.forward).normalized;
newLight.attenuation = Vector4.one;
newLight.up = (light.Transform.rotation * Vector3.up).normalized;
newLight.right = (light.Transform.rotation * Vector3.right).normalized;
newLight.width = light.AreaSize.x;
newLight.height = light.AreaSize.y;
newLight.spotAngle = light.SpotAngle;
newLight.innerSpotAngle = light.InnerSpotAngle;
newLight.layerMask = respectLightLayers ? light.CullingMask : uint.MaxValue;
newLight.indirectScale = light.BounceIntensity;
newLight.range = light.Range;
newLight.shadowRadius = light.ShadowRadius;
newLight.shadowMaskChannel = light.ShadowMaskChannel;
switch (light.LightmapBakeType)
{
case LightmapBakeType.Baked:
case LightmapBakeType.Mixed when mixedLightingMode == MixedLightingMode.Subtractive:
newLight.contributesToDirectLighting = 1;
break;
case LightmapBakeType.Mixed when mixedLightingMode == MixedLightingMode.Shadowmask:
// Fallback to baked behavior if we don't have a valid shadowmask channel
newLight.contributesToDirectLighting = newLight.shadowMaskChannel != -1 ? 0 : 1;
break;
case LightmapBakeType.Mixed when mixedLightingMode == MixedLightingMode.IndirectOnly:
case LightmapBakeType.Realtime:
default:
newLight.contributesToDirectLighting = 0;
break;
}
if (light.Type == LightType.Spot)
{
// aspect ratio is serialized in areaSize.x
float aspect = light.AreaSize.x;
float frustumHeight = 2.0f * Mathf.Tan(light.SpotAngle * 0.5f * Mathf.Deg2Rad);
float frustumWidth = frustumHeight * aspect;
newLight.width = frustumWidth;
newLight.height = frustumHeight;
}
if (light.Type == LightType.Spot || light.Type == LightType.Point)
{
Debug.Assert(light.FalloffType != Experimental.GlobalIllumination.FalloffType.Undefined);
float estimatedLUTRange = light.Range;
if (autoEstimateLUTRange)
{
// Guesstimate a good LUT range, such that the LUT covers the falloff up to a distance where it is nearly 0.
// The range can be any number and we don't want to stretch the LUT to some arbitrary range (most of which is practically 0).
estimatedLUTRange = EstimateLUTRange(light.Range, Luminance(new Color(light.LinearLightColor.x, light.LinearLightColor.y, light.LinearLightColor.z, 1.0f)), light.FalloffType, 0.01f);
}
LightFalloffDesc falloffDesc = new LightFalloffDesc
{
LUTRange = estimatedLUTRange,
FalloffType = light.FalloffType
};
var falloffHash = falloffDesc.GetHashCode();
if (!falloffHashToFalloffIndex.TryGetValue(falloffHash, out newLight.falloffIndex))
{
// Add new falloff entry
newLight.falloffIndex = falloffIndex++;
falloffHashToFalloffIndex.Add(falloffHash, newLight.falloffIndex);
_lightState.LightFalloffDescs.Add(falloffDesc);
}
}
else
newLight.falloffIndex = -1;
#pragma warning disable 162 // Disable unreachable code warning
// Light attenuation parameters and math from HDRP
const bool applyRangeAttenuation = true;
if (applyRangeAttenuation)
{
newLight.attenuation.x = 1.0f / (light.Range * light.Range);
newLight.attenuation.y = 1.0f;
}
else
{
const float hugeValue = 16777216.0f;
const float sqrtHuge = 4096.0f;
newLight.attenuation.x = sqrtHuge / (light.Range * light.Range);
newLight.attenuation.y = hugeValue;
}
#pragma warning restore 162
newLight.attenuation.z = 0.0f;
if (light.Type == LightType.Spot)
{
var spotAngle = light.SpotAngle;
var cosSpotOuterHalfAngle = Mathf.Clamp(Mathf.Cos(spotAngle * 0.5f * Mathf.Deg2Rad), 0.0f, 1.0f);
var cosSpotInnerHalfAngle = Mathf.Clamp(Mathf.Cos(0.5f * light.InnerSpotAngle * Mathf.Deg2Rad), 0.0f, 1.0f); // inner cone
var val = Mathf.Max(0.0001f, (cosSpotInnerHalfAngle - cosSpotOuterHalfAngle));
newLight.attenuation.z = 1.0f / val;
newLight.attenuation.w = -cosSpotOuterHalfAngle * newLight.attenuation.z;
}
newLight.cookieIndex = _materialPool.AddCookieTexture(light.CookieTexture);
_lightState.LightHandleToLightListEntry[lightHandles[i]] = newLight;
}
}
public void RemoveLights(Span<LightHandle> lights)
{
foreach (var light in lights)
{
if (_lightState.LightHandleToLightListEntry.TryGetValue(light, out var ptLight))
{
var cookieFaces = (ptLight.type == (int)LightType.Point) ? 6 : 1;
_materialPool.RemoveCookieTexture(cookieFaces, ptLight.cookieIndex);
}
_lightState.LightHandleSet.Remove(light);
_lightState.LightHandleToLightListEntry.Remove(light);
}
}
public void Build(Bounds sceneBounds, CommandBuffer cmdBuf, ref GraphicsBuffer scratchBuffer, Rendering.Sampling.SamplingResources samplingResources, bool emissiveSampling)
{
Debug.Assert(_rayTracingAccelerationStructure != null);
_lightState.Build(sceneBounds, cmdBuf, emissiveSampling && _cubemapRender.GetMaterial() != null);
if (_lightState.lightPickingMethod == LightPickingMethod.Regir)
{
_reservoirGrid.Build(cmdBuf, _lightState, sceneBounds, samplingResources);
}
else if (_lightState.lightPickingMethod == LightPickingMethod.LightGrid)
{
_conservativeLightGrid.Build(cmdBuf, _lightState, sceneBounds, samplingResources);
}
_materialPool.Build(cmdBuf);
_rayTracingAccelerationStructure.Build(cmdBuf, ref scratchBuffer);
}
public UInt64 GetInstanceHandles(InstanceHandle handle)
{
_rayTracingAccelerationStructure.GetInstanceIDs(handle.Value, out var ids);
return (UInt64)ids[0];
}
}
}