#ifndef UNITY_PATH_TRACING_LIGHT_INCLUDED #define UNITY_PATH_TRACING_LIGHT_INCLUDED // This is just because it need to be defined, shadow maps are not used. #define SHADOW_LOW #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Lighting.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/CookieSampling.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/LightLoopDef.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightEvaluation.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/Raytracing/Shaders/ShaderVariablesRaytracingLightLoop.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/Raytracing/Shaders/Shadows/SphericalQuad.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/Raytracing/Shaders/Common/AtmosphericScatteringRayTracing.hlsl" // How many lights (at most) do we support at one given shading point // FIXME: hardcoded limits are evil, this LightList should instead be put together in C# #define MAX_LOCAL_LIGHT_COUNT 16 #define MAX_DISTANT_LIGHT_COUNT 4 #define DELTA_PDF 1000000.0 // Supports punctual, spot, rect area and directional lights at the moment struct LightList { uint localCount; uint localPointCount; uint localIndex[MAX_LOCAL_LIGHT_COUNT]; float localWeight; uint distantCount; uint distantIndex[MAX_DISTANT_LIGHT_COUNT]; float distantWeight; #ifdef USE_LIGHT_CLUSTER uint cellIndex; #endif }; bool IsRectAreaLightActive(LightData lightData, float3 position, float3 normal) { float3 lightToPosition = position - lightData.positionRWS; #ifndef USE_LIGHT_CLUSTER // Check light range first if (Length2(lightToPosition) > Sq(lightData.range)) return false; #endif // Check that the shading position is in front of the light float lightCos = dot(lightToPosition, lightData.forward); if (lightCos < 0.0) return false; // Check that at least part of the light is above the tangent plane float lightTangentDist = dot(normal, lightToPosition); if (4.0 * lightTangentDist * abs(lightTangentDist) > Sq(lightData.size.x) + Sq(lightData.size.y)) return false; return true; } bool IsPointLightActive(LightData lightData, float3 position, float3 normal) { float3 lightToPosition = position - lightData.positionRWS; #ifndef USE_LIGHT_CLUSTER // Check light range first if (Length2(lightToPosition) > Sq(lightData.range)) return false; #endif // Check that at least part of the light is above the tangent plane float lightTangentDist = dot(normal, lightToPosition); if (lightTangentDist * abs(lightTangentDist) > lightData.size.x) return false; // If this is an omni-directional point light, we're done if (lightData.lightType == GPULIGHTTYPE_POINT) return true; // Check that we are on the right side of the light plane float z = dot(lightToPosition, lightData.forward); if (z < 0.0) return false; if (lightData.lightType == GPULIGHTTYPE_SPOT) { // Offset the light position towards the back, to account for the radius, // then check whether we are still within the dilated cone angle float sinTheta2 = 1.0 - Sq(lightData.angleOffset / lightData.angleScale); float3 lightRadiusOffset = sqrt(lightData.size.x / sinTheta2) * lightData.forward; float lightCos = dot(normalize(lightToPosition + lightRadiusOffset), lightData.forward); return lightCos * lightData.angleScale + lightData.angleOffset > 0.0; } // Our light type is either BOX or PYRAMID float x = abs(dot(lightToPosition, lightData.right)); float y = abs(dot(lightToPosition, lightData.up)); return (lightData.lightType == GPULIGHTTYPE_PROJECTOR_BOX) ? x < 1.0 && y < 1.0 : // BOX x < z && y < z; // PYRAMID } bool IsDistantLightActive(DirectionalLightData lightData, float3 normal) { return dot(normal, lightData.forward) <= sin(lightData.angularDiameter * 0.5); } LightList CreateLightList(float3 position, float3 normal, uint lightLayers, bool withLocal = true, bool withDistant = true) { LightList list; uint i; // First take care of local lights (point, area) list.localCount = 0; list.localPointCount = 0; if (withLocal) { uint localPointCount, localCount; #ifdef USE_LIGHT_CLUSTER if (PointInsideCluster(position)) { list.cellIndex = GetClusterCellIndex(position); localPointCount = GetPunctualLightClusterCellCount(list.cellIndex); localCount = GetAreaLightClusterCellCount(list.cellIndex); } else { localPointCount = 0; localCount = 0; } #else localPointCount = _PunctualLightCountRT; localCount = _PunctualLightCountRT + _AreaLightCountRT; #endif // First point lights (including spot lights) for (i = 0; i < localPointCount && list.localPointCount < MAX_LOCAL_LIGHT_COUNT; i++) { #ifdef USE_LIGHT_CLUSTER const LightData lightData = FetchClusterLightIndex(list.cellIndex, i); #else const LightData lightData = _LightDatasRT[i]; #endif if (IsMatchingLightLayer(lightData.lightLayers, lightLayers) && IsPointLightActive(lightData, position, normal)) list.localIndex[list.localPointCount++] = i; } // Then rect area lights for (list.localCount = list.localPointCount; i < localCount && list.localCount < MAX_LOCAL_LIGHT_COUNT; i++) { #ifdef USE_LIGHT_CLUSTER const LightData lightData = FetchClusterLightIndex(list.cellIndex, i); #else const LightData lightData = _LightDatasRT[i]; #endif if (IsMatchingLightLayer(lightData.lightLayers, lightLayers) && IsRectAreaLightActive(lightData, position, normal)) list.localIndex[list.localCount++] = i; } } // Then filter the active distant lights (directional) list.distantCount = 0; if (withDistant) { for (i = 0; i < _DirectionalLightCount && list.distantCount < MAX_DISTANT_LIGHT_COUNT; i++) { if (IsMatchingLightLayer(_DirectionalLightDatas[i].lightLayers, lightLayers) && IsDistantLightActive(_DirectionalLightDatas[i], normal)) list.distantIndex[list.distantCount++] = i; } } // Compute the weights, used for the lights PDF (we split 50/50 between local and distant, if both are present) list.localWeight = list.localCount ? (list.distantCount ? 0.5 : 1.0) : 0.0; list.distantWeight = list.distantCount ? 1.0 - list.localWeight : 0.0; return list; } uint GetLightCount(LightList list) { return list.localCount + list.distantCount; } LightData GetLocalLightData(LightList list, uint i) { #ifdef USE_LIGHT_CLUSTER return FetchClusterLightIndex(list.cellIndex, list.localIndex[i]); #else return _LightDatasRT[list.localIndex[i]]; #endif } LightData GetLocalLightData(LightList list, float inputSample) { return GetLocalLightData(list, (uint)(inputSample * list.localCount)); } DirectionalLightData GetDistantLightData(LightList list, uint i) { return _DirectionalLightDatas[list.distantIndex[i]]; } DirectionalLightData GetDistantLightData(LightList list, float inputSample) { return GetDistantLightData(list, (uint)(inputSample * list.distantCount)); } float GetLocalLightWeight(LightList list) { return list.localWeight / list.localCount; } float GetDistantLightWeight(LightList list) { return list.distantWeight / list.distantCount; } bool PickLocalLights(LightList list, inout float theSample) { if (theSample < list.localWeight) { // We pick local lighting theSample /= list.localWeight; return true; } // Otherwise, distant lighting theSample = (theSample - list.localWeight) / list.distantWeight; return false; } bool PickDistantLights(LightList list, inout float theSample) { return !PickLocalLights(list, theSample); } float3 GetPunctualEmission(LightData lightData, float3 outgoingDir, float dist) { float3 emission = lightData.color; // Punctual attenuation float4 distances = float4(dist, Sq(dist), rcp(dist), -dist * dot(outgoingDir, lightData.forward)); emission *= PunctualLightAttenuation(distances, lightData.rangeAttenuationScale, lightData.rangeAttenuationBias, lightData.angleScale, lightData.angleOffset); #ifndef LIGHT_EVALUATION_NO_COOKIE if (lightData.cookieMode != COOKIEMODE_NONE) { LightLoopContext context; emission *= EvaluateCookie_Punctual(context, lightData, -dist * outgoingDir); } #endif return emission; } float3 GetDirectionalEmission(DirectionalLightData lightData, float3 outgoingVec) { float3 emission = lightData.color; #ifndef LIGHT_EVALUATION_NO_COOKIE if (lightData.cookieMode != COOKIEMODE_NONE) { LightLoopContext context; emission *= EvaluateCookie_Directional(context, lightData, -outgoingVec); } #endif return emission; } float3 GetAreaEmission(LightData lightData, float centerU, float centerV, float sqDist) { float3 emission = lightData.color; // Range windowing (see LightLoop.cs to understand why it is written this way) if (lightData.rangeAttenuationBias == 1.0) emission *= SmoothDistanceWindowing(sqDist, rcp(Sq(lightData.range)), lightData.rangeAttenuationBias); #ifndef LIGHT_EVALUATION_NO_COOKIE if (lightData.cookieMode != COOKIEMODE_NONE) { float2 uv = float2(0.5 - centerU, 0.5 + centerV); emission *= SampleCookie2D(uv, lightData.cookieScaleOffset); } #endif return emission; } bool SampleLights(LightList lightList, float3 inputSample, float3 position, float3 normal, out float3 outgoingDir, out float3 value, out float pdf, out float dist) { if (!GetLightCount(lightList)) return false; // Are we lighting a volume or a surface? bool isVolume = !any(normal); if (PickLocalLights(lightList, inputSample.z)) { // Pick a local light from the list LightData lightData = GetLocalLightData(lightList, inputSample.z); if (lightData.lightType == GPULIGHTTYPE_RECTANGLE) { // Generate a point on the surface of the light float centerU = inputSample.x - 0.5; float centerV = inputSample.y - 0.5; float3 lightCenter = lightData.positionRWS; float3 samplePos = lightCenter + centerU * lightData.size.x * lightData.right + centerV * lightData.size.y * lightData.up; // And the corresponding direction outgoingDir = samplePos - position; float sqDist = Length2(outgoingDir); dist = sqrt(sqDist); outgoingDir /= dist; if (!isVolume && dot(normal, outgoingDir) < 0.001) return false; float cosTheta = -dot(outgoingDir, lightData.forward); if (cosTheta < 0.001) return false; float lightArea = length(cross(lightData.size.x * lightData.right, lightData.size.y * lightData.up)); value = GetAreaEmission(lightData, centerU, centerV, sqDist); pdf = GetLocalLightWeight(lightList) * sqDist / (lightArea * cosTheta); } else // Punctual light { // Direction from shading point to light position outgoingDir = lightData.positionRWS - position; float sqDist = Length2(outgoingDir); dist = sqrt(sqDist); outgoingDir /= dist; if (lightData.size.x > 0.0) // Stores the square radius { float3x3 localFrame = GetLocalFrame(outgoingDir); SampleCone(inputSample.xy, sqrt(1.0 / (1.0 + lightData.size.x / sqDist)), outgoingDir, pdf); // computes rcpPdf outgoingDir = outgoingDir.x * localFrame[0] + outgoingDir.y * localFrame[1] + outgoingDir.z * localFrame[2]; pdf = min(rcp(pdf), DELTA_PDF); } else { // DELTA_PDF represents 1 / area, where the area is infinitesimal pdf = DELTA_PDF; } if (!isVolume && dot(normal, outgoingDir) < 0.001) return false; value = GetPunctualEmission(lightData, outgoingDir, dist) * pdf; pdf = GetLocalLightWeight(lightList) * pdf; } if (isVolume) value *= lightData.volumetricLightDimmer; #ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG ApplyFogAttenuation(position, outgoingDir, dist, value); #endif } else // Distant lights { // Pick a distant light from the list DirectionalLightData lightData = GetDistantLightData(lightList, inputSample.z); // The position-to-light unnormalized vector is used for cookie evaluation float3 OutgoingVec = lightData.positionRWS - position; if (lightData.angularDiameter > 0.0) { SampleCone(inputSample.xy, cos(lightData.angularDiameter * 0.5), outgoingDir, pdf); // computes rcpPdf value = GetDirectionalEmission(lightData, OutgoingVec) / pdf; pdf = GetDistantLightWeight(lightList) / pdf; outgoingDir = normalize(outgoingDir.x * normalize(lightData.right) + outgoingDir.y * normalize(lightData.up) - outgoingDir.z * lightData.forward); } else { value = GetDirectionalEmission(lightData, OutgoingVec) * DELTA_PDF; pdf = GetDistantLightWeight(lightList) * DELTA_PDF; outgoingDir = -lightData.forward; } if (!isVolume && (dot(normal, outgoingDir) < 0.001)) return false; dist = FLT_INF; if (isVolume) value *= lightData.volumetricLightDimmer; #ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG ApplyFogAttenuation(position, outgoingDir, value); #endif } return any(value); } void EvaluateLights(LightList lightList, RayDesc rayDescriptor, out float3 value, out float pdf) { value = 0.0; pdf = 0.0; uint i; // First local lights (area lights only, as we consider the probability of hitting a point light neglectable) for (i = lightList.localPointCount; i < lightList.localCount; i++) { LightData lightData = GetLocalLightData(lightList, i); float t = rayDescriptor.TMax; float cosTheta = -dot(rayDescriptor.Direction, lightData.forward); float3 lightCenter = lightData.positionRWS; // Check if we hit the light plane, at a distance below our tMax (coming from indirect computation) if (cosTheta > 0.0 && IntersectPlane(rayDescriptor.Origin, rayDescriptor.Direction, lightCenter, lightData.forward, t)) { if (t < rayDescriptor.TMax) { float3 hitVec = rayDescriptor.Origin + t * rayDescriptor.Direction - lightCenter; // Then check if we are within the rectangle bounds float centerU = dot(hitVec, lightData.right) / (lightData.size.x * Length2(lightData.right)); float centerV = dot(hitVec, lightData.up) / (lightData.size.y * Length2(lightData.up)); if (abs(centerU) < 0.5 && abs(centerV) < 0.5) { float t2 = Sq(t); float3 lightValue = GetAreaEmission(lightData, centerU, centerV, t2); #ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG ApplyFogAttenuation(rayDescriptor.Origin, rayDescriptor.Direction, t, lightValue); #endif value += lightValue; float lightArea = length(cross(lightData.size.x * lightData.right, lightData.size.y * lightData.up)); pdf += GetLocalLightWeight(lightList) * t2 / (lightArea * cosTheta); // If we consider that a ray is very unlikely to hit 2 area lights one after another, we can exit the loop break; } } } } // Then distant lights for (i = 0; i < lightList.distantCount; i++) { DirectionalLightData lightData = GetDistantLightData(lightList, i); if (lightData.angularDiameter > 0.0 && rayDescriptor.TMax >= FLT_INF) { float cosHalfAngle = cos(lightData.angularDiameter * 0.5); float cosTheta = -dot(rayDescriptor.Direction, lightData.forward); if (cosTheta >= cosHalfAngle) { float3 lightValue = GetDirectionalEmission(lightData, rayDescriptor.Direction); #ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG ApplyFogAttenuation(rayDescriptor.Origin, rayDescriptor.Direction, lightValue); #endif float rcpPdf = TWO_PI * (1.0 - cosHalfAngle); value += lightValue / rcpPdf; pdf += GetDistantLightWeight(lightList) / rcpPdf; } } } } // Functions used by volumetric sampling bool GetSphereInterval(float3 lightToRayOrigin, float radius, float3 rayDirection, out float tMin, out float tMax) { // We consider Direction to be normalized => a = 1 float b = 2.0 * dot(rayDirection, lightToRayOrigin); float c = Length2(lightToRayOrigin) - Sq(radius); float2 t; if (!SolveQuadraticEquation(1.0, b, c, t)) return false; tMin = max(t.x, 0.0); tMax = max(t.y, 0.0); return tMin < tMax; } bool GetRectAreaLightInterval(LightData lightData, float3 rayOrigin, float3 rayDirection, out float tMin, out float tMax) { if (lightData.volumetricLightDimmer < 0.001) return false; float3 lightToRayOrigin = rayOrigin - lightData.positionRWS; if (!GetSphereInterval(lightToRayOrigin, lightData.range, rayDirection, tMin, tMax)) return false; float LdotD = dot(lightData.forward, rayDirection); float t = -dot(lightData.forward, lightToRayOrigin) / LdotD; if (LdotD > 0.0) tMin = max(tMin, t); else tMax = min(tMax, t); return tMin < tMax; } void Sort(inout float x, inout float y) { if (x > y) { float tmp = x; x = y; y = tmp; } } void GetFrontInterval(float oz, float dz, float t1, float t2, inout float tMin, inout float tMax) { bool t1Valid = oz + t1 * dz > 0.0; bool t2Valid = oz + t2 * dz > 0.0; if (t1Valid) { if (t2Valid) { tMin = max(t1, tMin); tMax = min(t2, tMax); } else { tMax = min(t1, tMax); } } else { tMin = t2Valid ? max(t2, tMin) : tMax; } } bool GetPointLightInterval(LightData lightData, float3 rayOrigin, float3 rayDirection, out float tMin, out float tMax) { if (lightData.volumetricLightDimmer < 0.001) return false; float3 lightToRayOrigin = rayOrigin - lightData.positionRWS; if (!GetSphereInterval(lightToRayOrigin, lightData.range, rayDirection, tMin, tMax)) return false; // This is just a point light (no spot cone angle) if (lightData.lightType == GPULIGHTTYPE_POINT) return true; // We are dealing with either a cone, a pyramid or a box float3 localOrigin = float3(dot(lightToRayOrigin, lightData.right), dot(lightToRayOrigin, lightData.up), dot(lightToRayOrigin, lightData.forward)); float3 localDirection = float3(dot(rayDirection, lightData.right), dot(rayDirection, lightData.up), dot(rayDirection, lightData.forward)); if (lightData.lightType == GPULIGHTTYPE_PROJECTOR_BOX) { // Compute intersections with planes x=-1 and x=1 float tx1 = (-1.0 - localOrigin.x) / localDirection.x; float tx2 = (1.0 - localOrigin.x) / localDirection.x; Sort(tx1, tx2); // Compute intersections with planes y=-1 and y=1 float ty1 = (-1.0 - localOrigin.y) / localDirection.y; float ty2 = (1.0 - localOrigin.y) / localDirection.y; Sort(ty1, ty2); // Compute intersection with plane z=0 float tz = -localOrigin.z / localDirection.z; float t1 = max(tx1, ty1); float t2 = min(tx2, ty2); // Check validity of the intersections (we want them only in front of the light) bool t1Valid = localOrigin.z + t1 * localDirection.z > 0.0; bool t2Valid = localOrigin.z + t2 * localDirection.z > 0.0; tMin = t1Valid ? max(t1, tMin) : tz; tMax = t2Valid ? min(t2, tMax) : tz; } else if (lightData.lightType == GPULIGHTTYPE_PROJECTOR_PYRAMID) { // Compute intersections with planes x=-z and x=z float tx1 = -(localOrigin.x - localOrigin.z) / (localDirection.x - localDirection.z); float tx2 = -(localOrigin.x + localOrigin.z) / (localDirection.x + localDirection.z); Sort(tx1, tx2); // Check validity of the intersections (we want them only in front of the light) GetFrontInterval(localOrigin.z, localDirection.z, tx1, tx2, tMin, tMax); if (tMin < tMax) { // Compute intersections with planes y=-1 and y=1 float ty1 = -(localOrigin.y - localOrigin.z) / (localDirection.y - localDirection.z); float ty2 = -(localOrigin.y + localOrigin.z) / (localDirection.y + localDirection.z); Sort(ty1, ty2); // Check validity of the intersections (we want them only in front of the light) GetFrontInterval(localOrigin.z, localDirection.z, ty1, ty2, tMin, tMax); } } else // lightData.lightType == GPULIGHTTYPE_SPOT { float cosTheta2 = Sq(lightData.angleOffset / lightData.angleScale); // Offset light origin to account for light radius localOrigin.z += sqrt(lightData.size.x / (1.0 - cosTheta2)); // Account for non-normalized local basis float3 normalizedLocalOrigin = float3(localOrigin.x / Length2(lightData.right), localOrigin.y / Length2(lightData.up), localOrigin.z); float a = Sq(localDirection.z) - cosTheta2; float b = 2.0 * (localOrigin.z * localDirection.z - dot(normalizedLocalOrigin, localDirection) * cosTheta2); float c = Sq(localOrigin.z) - dot(normalizedLocalOrigin, localOrigin) * cosTheta2; float2 t; if (!SolveQuadraticEquation(a, b, c, t)) return false; // Check validity of the intersections (we want them only in front of the light) GetFrontInterval(localOrigin.z, localDirection.z, t.x, t.y, tMin, tMax); } return tMin < tMax; } float GetLocalLightsInterval(float3 rayOrigin, float3 rayDirection, out float tMin, out float tMax) { tMin = FLT_MAX; tMax = 0.0; float tLightMin, tLightMax; // First process point lights uint i = 0, n = _PunctualLightCountRT, localCount = 0; for (; i < n; i++) { if (GetPointLightInterval(_LightDatasRT[i], rayOrigin, rayDirection, tLightMin, tLightMax)) { tMin = min(tMin, tLightMin); tMax = max(tMax, tLightMax); localCount++; } } // Then area lights n += _AreaLightCountRT; for (; i < n; i++) { if (GetRectAreaLightInterval(_LightDatasRT[i], rayOrigin, rayDirection, tLightMin, tLightMax)) { tMin = min(tMin, tLightMin); tMax = max(tMax, tLightMax); localCount++; } } uint lightCount = localCount + _DirectionalLightCount; return lightCount ? float(localCount) / lightCount : -1.0; } LightList CreateLightList(float3 position, bool sampleLocalLights) { return CreateLightList(position, 0.0, ~0, sampleLocalLights, !sampleLocalLights); } #endif // UNITY_PATH_TRACING_LIGHT_INCLUDED