1 // This files include various function uses to evaluate lights
2 // use #define LIGHT_EVALUATION_NO_HEIGHT_FOG to disable Height fog attenuation evaluation
3 // use #define LIGHT_EVALUATION_NO_COOKIE to disable cookie evaluation
4 // use #define LIGHT_EVALUATION_NO_CONTACT_SHADOWS to disable contact shadow evaluation
5 // use #define LIGHT_EVALUATION_NO_SHADOWS to disable evaluation of shadow including contact shadow (but not micro shadow)
6 // use #define OVERRIDE_EVALUATE_ENV_INTERSECTION to provide a new version of EvaluateLight_EnvIntersection
8 // Samples the area light's associated cookie
9 // cookieIndex, the index of the cookie texture in the Texture2DArray
10 // L, the 4 local-space corners of the area light polygon transformed by the LTC M^-1 matrix
11 // F, the *normalized* vector irradiance
12 float3 SampleAreaLightCookie(int cookieIndex, float4x3 L, float3 F)
15 // L[1] = bottom-right
19 float3 right = L[1] - origin;
20 float3 up = L[3] - origin;
22 float3 normal = cross(right, up);
23 float sqArea = dot(normal, normal);
24 normal *= rsqrt(sqArea);
26 // Compute intersection of irradiance vector with the area light plane
27 float hitDistance = dot(origin, normal) / dot(F, normal);
28 float3 hitPosition = hitDistance * normal;
29 hitPosition -= origin; // Relative to bottom-left corner
31 // Here, right and up vectors are not necessarily orthonormal
32 // We create the orthogonal vector "ortho" by projecting "up" onto the vector orthogonal to "right"
33 // ortho = up - (up.right') * right'
34 // Where right' = right / sqrt( dot( right, right ) ), the normalized right vector
35 float recSqLengthRight = 1.0 / dot(right, right);
36 float upRightMixing = dot(up, right);
37 float3 ortho = up - upRightMixing * right * recSqLengthRight;
39 // The V coordinate along the "up" vector is simply the projection against the ortho vector
40 float v = dot(hitPosition, ortho) / dot(ortho, ortho);
42 // The U coordinate is not only the projection against the right vector
43 // but also the subtraction of the influence of the up vector upon the right vector
44 // (indeed, if the up & right vectors are not orthogonal then a certain amount of
45 // the up coordinate also influences the right coordinate)
48 // ortho ^....*--------*
55 // mix of up into right that needs to be subtracted from simple projection on right vector
57 float u = (dot(hitPosition, right) - upRightMixing * v) * recSqLengthRight;
58 // Currently the texture happens to be reversed when comparing it to the area light emissive mesh itself. This needs
59 // Further investigation to solve the problem. So for the moment we simply decided to inverse the x coordinate of hitUV
60 // as a temporary solution
61 // TODO: Invesigate more!
62 float2 hitUV = float2(1.0 - u, v);
64 // Assuming the original cosine lobe distribution Do is enclosed in a cone of 90° aperture,
65 // following the idea of orthogonal projection upon the area light's plane we find the intersection
66 // of the cone to be a disk of area PI*d² where d is the hit distance we computed above.
67 // We also know the area of the transformed polygon A = sqrt( sqArea ) and we pose the ratio of covered area as PI.d² / A.
69 // Knowing the area in square texels of the cookie texture A_sqTexels = texture width * texture height (default is 128x128 square texels)
70 // we can deduce the actual area covered by the cone in square texels as:
71 // A_covered = Pi.d² / A * A_sqTexels
73 // From this, we find the mip level as: mip = log2( sqrt( A_covered ) ) = log2( A_covered ) / 2
74 // Also, assuming that A_sqTexels is of the form 2^n * 2^n we get the simplified expression: mip = log2( Pi.d² / A ) / 2 + n
76 const float COOKIE_MIPS_COUNT = _CookieSizePOT;
77 float mipLevel = 0.5 * log2(1e-8 + PI * hitDistance*hitDistance * rsqrt(sqArea)) + COOKIE_MIPS_COUNT;
79 return SAMPLE_TEXTURE2D_ARRAY_LOD(_AreaCookieTextures, s_trilinear_clamp_sampler, hitUV, cookieIndex, mipLevel).xyz;
82 //-----------------------------------------------------------------------------
83 // Directional Light evaluation helper
84 //-----------------------------------------------------------------------------
86 float3 EvaluateCookie_Directional(LightLoopContext lightLoopContext, DirectionalLightData light,
90 // Translate and rotate 'positionWS' into the light space.
91 // 'light.right' and 'light.up' are pre-scaled on CPU.
92 float3x3 lightToWorld = float3x3(light.right, light.up, light.forward);
93 float3 positionLS = mul(lightToSample, transpose(lightToWorld));
95 // Perform orthographic projection.
96 float2 positionCS = positionLS.xy;
98 // Remap the texture coordinates from [-1, 1]^2 to [0, 1]^2.
99 float2 positionNDC = positionCS * 0.5 + 0.5;
101 // We let the sampler handle clamping to border.
102 return SampleCookie2D(lightLoopContext, positionNDC, light.cookieIndex, light.tileCookie);
105 // Returns unassociated (non-premultiplied) color with alpha (attenuation).
106 // The calling code must perform alpha-compositing.
107 float4 EvaluateLight_Directional(LightLoopContext lightLoopContext, PositionInputs posInput,
108 DirectionalLightData light)
110 float4 color = float4(light.color, 1.0);
112 #ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG
113 // Height fog attenuation.
114 // TODO: add an if()?
116 float cosZenithAngle = -light.forward.y;
117 float fragmentHeight = posInput.positionWS.y;
118 color.a *= TransmittanceHeightFog(_HeightFogBaseExtinction, _HeightFogBaseHeight,
119 _HeightFogExponents, cosZenithAngle, fragmentHeight);
123 #ifndef LIGHT_EVALUATION_NO_COOKIE
124 if (light.cookieIndex >= 0)
126 float3 lightToSample = posInput.positionWS - light.positionRWS;
127 float3 cookie = EvaluateCookie_Directional(lightLoopContext, light, lightToSample);
136 float EvaluateShadow_Directional(LightLoopContext lightLoopContext, PositionInputs posInput,
137 DirectionalLightData light, BuiltinData builtinData, float3 N)
139 #ifndef LIGHT_EVALUATION_NO_SHADOWS
141 float shadowMask = 1.0;
142 float NdotL = dot(N, -light.forward); // Disable contact shadow and shadow mask when facing away from light (i.e transmission)
144 #ifdef SHADOWS_SHADOWMASK
145 // shadowMaskSelector.x is -1 if there is no shadow mask
146 // Note that we override shadow value (in case we don't have any dynamic shadow)
147 shadow = shadowMask = (light.shadowMaskSelector.x >= 0.0 && NdotL > 0.0) ? dot(BUILTIN_DATA_SHADOW_MASK, light.shadowMaskSelector) : 1.0;
150 if ((light.shadowIndex >= 0) && (light.shadowDimmer > 0))
152 shadow = lightLoopContext.shadowValue;
154 #ifdef SHADOWS_SHADOWMASK
155 // TODO: Optimize this code! Currently it is a bit like brute force to get the last transistion and fade to shadow mask, but there is
156 // certainly more efficient to do
157 // We reuse the transition from the cascade system to fade between shadow mask at max distance
161 int shadowSplitIndex = 0;
163 shadowSplitIndex = EvalShadow_GetSplitIndex(lightLoopContext.shadowContext, light.shadowIndex, posInput.positionWS, fade, cascadeCount);
165 // we have a fade caclulation for each cascade but we must lerp with shadow mask only for the last one
166 // if shadowSplitIndex is -1 it mean we are outside cascade and should return 1.0 to use shadowmask: saturate(-shadowSplitIndex) return 0 for >= 0 and 1 for -1
167 fade = ((shadowSplitIndex + 1) == cascadeCount) ? fade : saturate(-shadowSplitIndex);
169 // In the transition code (both dithering and blend) we use shadow = lerp( shadow, 1.0, fade ) for last transition
170 // mean if we expend the code we have (shadow * (1 - fade) + fade). Here to make transition with shadow mask
171 // we will remove fade and add fade * shadowMask which mean we do a lerp with shadow mask
172 shadow = shadow - fade + fade * shadowMask;
174 // See comment in EvaluateBSDF_Punctual
175 shadow = light.nonLightMappedOnly ? min(shadowMask, shadow) : shadow;
178 shadow = lerp(shadowMask, shadow, light.shadowDimmer);
181 // Transparents have no contact shadow information
182 #if !defined(_SURFACE_TYPE_TRANSPARENT) && !defined(LIGHT_EVALUATION_NO_CONTACT_SHADOWS)
183 shadow = min(shadow, NdotL > 0.0 ? GetContactShadow(lightLoopContext, light.contactShadowMask) : 1.0);
187 if (_DebugShadowMapMode == SHADOWMAPDEBUGMODE_SINGLE_SHADOW && light.shadowIndex == _DebugSingleShadowIndex)
188 g_DebugShadowAttenuation = shadow;
192 #else // LIGHT_EVALUATION_NO_SHADOWS
197 //-----------------------------------------------------------------------------
198 // Punctual Light evaluation helper
199 //-----------------------------------------------------------------------------
201 // distances = {d, d^2, 1/d, d_proj}
202 void ModifyDistancesForFillLighting(inout float4 distances, float lightSqRadius)
204 // Apply the sphere light hack to soften the core of the punctual light.
205 // It is not physically plausible (using max() is more correct, but looks worse).
206 // See https://www.desmos.com/calculator/otqhxunqhl
207 // We only modify 1/d for performance reasons.
208 float sqDist = distances.y;
209 distances.z = rsqrt(sqDist + lightSqRadius); // Recompute 1/d
212 // Returns the normalized light vector L and the distances = {d, d^2, 1/d, d_proj}.
213 void GetPunctualLightVectors(float3 positionWS, LightData light, out float3 L, out float4 distances)
215 float3 lightToSample = positionWS - light.positionRWS;
217 distances.w = dot(lightToSample, light.forward);
219 if (light.lightType == GPULIGHTTYPE_PROJECTOR_BOX)
222 distances.xyz = 1; // No distance or angle attenuation
226 float3 unL = -lightToSample;
227 float distSq = dot(unL, unL);
228 float distRcp = rsqrt(distSq);
229 float dist = distSq * distRcp;
232 distances.xyz = float3(dist, distSq, distRcp);
234 ModifyDistancesForFillLighting(distances, light.size.x);
238 float4 EvaluateCookie_Punctual(LightLoopContext lightLoopContext, LightData light,
239 float3 lightToSample)
241 #ifndef LIGHT_EVALUATION_NO_COOKIE
242 int lightType = light.lightType;
244 // Translate and rotate 'positionWS' into the light space.
245 // 'light.right' and 'light.up' are pre-scaled on CPU.
246 float3x3 lightToWorld = float3x3(light.right, light.up, light.forward);
247 float3 positionLS = mul(lightToSample, transpose(lightToWorld));
251 UNITY_BRANCH if (lightType == GPULIGHTTYPE_POINT)
253 cookie.rgb = SampleCookieCube(lightLoopContext, positionLS, light.cookieIndex);
258 // Perform orthographic or perspective projection.
259 float perspectiveZ = (lightType != GPULIGHTTYPE_PROJECTOR_BOX) ? positionLS.z : 1.0;
260 float2 positionCS = positionLS.xy / perspectiveZ;
261 bool isInBounds = Max3(abs(positionCS.x), abs(positionCS.y), 1.0 - positionLS.z) <= 1.0;
263 // Remap the texture coordinates from [-1, 1]^2 to [0, 1]^2.
264 float2 positionNDC = positionCS * 0.5 + 0.5;
266 // Manually clamp to border (black).
267 cookie.rgb = SampleCookie2D(lightLoopContext, positionNDC, light.cookieIndex, false);
268 cookie.a = isInBounds ? 1.0 : 0.0;
273 // When we disable cookie, we must still perform border attenuation for pyramid and box
274 // as by default we always bind a cookie white texture for them to mimic it.
275 float4 cookie = float4(1.0, 1.0, 1.0, 1.0);
277 int lightType = light.lightType;
279 if (lightType == GPULIGHTTYPE_PROJECTOR_PYRAMID || lightType == GPULIGHTTYPE_PROJECTOR_BOX)
281 // Translate and rotate 'positionWS' into the light space.
282 // 'light.right' and 'light.up' are pre-scaled on CPU.
283 float3x3 lightToWorld = float3x3(light.right, light.up, light.forward);
284 float3 positionLS = mul(lightToSample, transpose(lightToWorld));
286 // Perform orthographic or perspective projection.
287 float perspectiveZ = (lightType != GPULIGHTTYPE_PROJECTOR_BOX) ? positionLS.z : 1.0;
288 float2 positionCS = positionLS.xy / perspectiveZ;
289 bool isInBounds = Max3(abs(positionCS.x), abs(positionCS.y), 1.0 - positionLS.z) <= 1.0;
291 // Manually clamp to border (black).
292 cookie.a = isInBounds ? 1.0 : 0.0;
299 // Returns unassociated (non-premultiplied) color with alpha (attenuation).
300 // The calling code must perform alpha-compositing.
301 // distances = {d, d^2, 1/d, d_proj}, where d_proj = dot(lightToSample, light.forward).
302 float4 EvaluateLight_Punctual(LightLoopContext lightLoopContext, PositionInputs posInput,
303 LightData light, float3 L, float4 distances)
305 float4 color = float4(light.color, 1.0);
307 color.a *= PunctualLightAttenuation(distances, light.rangeAttenuationScale, light.rangeAttenuationBias,
308 light.angleScale, light.angleOffset);
310 #ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG
311 // Height fog attenuation.
312 // TODO: add an if()?
314 float cosZenithAngle = L.y;
315 float distToLight = (light.lightType == GPULIGHTTYPE_PROJECTOR_BOX) ? distances.w : distances.x;
316 float fragmentHeight = posInput.positionWS.y;
317 color.a *= TransmittanceHeightFog(_HeightFogBaseExtinction, _HeightFogBaseHeight,
318 _HeightFogExponents, cosZenithAngle,
319 fragmentHeight, distToLight);
323 // Projector lights (box, pyramid) always have cookies, so we can perform clipping inside the if().
324 // Thus why we don't disable the code here based on LIGHT_EVALUATION_NO_COOKIE but we do it
325 // inside the EvaluateCookie_Punctual call
326 if (light.cookieIndex >= 0)
328 float3 lightToSample = posInput.positionWS - light.positionRWS;
329 float4 cookie = EvaluateCookie_Punctual(lightLoopContext, light, lightToSample);
337 // distances = {d, d^2, 1/d, d_proj}, where d_proj = dot(lightToSample, light.forward).
338 float EvaluateShadow_Punctual(LightLoopContext lightLoopContext, PositionInputs posInput,
339 LightData light, BuiltinData builtinData, float3 N, float3 L, float4 distances)
341 #ifndef LIGHT_EVALUATION_NO_SHADOWS
343 float shadowMask = 1.0;
344 float NdotL = dot(N, L); // Disable contact shadow and shadow mask when facing away from light (i.e transmission)
347 #ifdef SHADOWS_SHADOWMASK
348 // shadowMaskSelector.x is -1 if there is no shadow mask
349 // Note that we override shadow value (in case we don't have any dynamic shadow)
350 shadow = shadowMask = (light.shadowMaskSelector.x >= 0.0 && NdotL > 0.0) ? dot(BUILTIN_DATA_SHADOW_MASK, light.shadowMaskSelector) : 1.0;
353 if ((light.shadowIndex >= 0) && (light.shadowDimmer > 0))
355 shadow = GetPunctualShadowAttenuation(lightLoopContext.shadowContext, posInput.positionSS, posInput.positionWS, N, light.shadowIndex, L, distances.x, light.lightType == GPULIGHTTYPE_POINT, light.lightType != GPULIGHTTYPE_PROJECTOR_BOX);
357 #ifdef SHADOWS_SHADOWMASK
358 // Note: Legacy Unity have two shadow mask mode. ShadowMask (ShadowMask contain static objects shadow and ShadowMap contain only dynamic objects shadow, final result is the minimun of both value)
359 // and ShadowMask_Distance (ShadowMask contain static objects shadow and ShadowMap contain everything and is blend with ShadowMask based on distance (Global distance setup in QualitySettigns)).
360 // HDRenderPipeline change this behavior. Only ShadowMask mode is supported but we support both blend with distance AND minimun of both value. Distance is control by light.
361 // The following code do this.
362 // The min handle the case of having only dynamic objects in the ShadowMap
363 // The second case for blend with distance is handled with ShadowDimmer. ShadowDimmer is define manually and by shadowDistance by light.
364 // With distance, ShadowDimmer become one and only the ShadowMask appear, we get the blend with distance behavior.
365 shadow = light.nonLightMappedOnly ? min(shadowMask, shadow) : shadow;
368 shadow = lerp(shadowMask, shadow, light.shadowDimmer);
371 // Transparents have no contact shadow information
372 #if !defined(_SURFACE_TYPE_TRANSPARENT) && !defined(LIGHT_EVALUATION_NO_CONTACT_SHADOWS)
373 shadow = min(shadow, NdotL > 0.0 ? GetContactShadow(lightLoopContext, light.contactShadowMask) : 1.0);
377 if (_DebugShadowMapMode == SHADOWMAPDEBUGMODE_SINGLE_SHADOW && light.shadowIndex == _DebugSingleShadowIndex)
378 g_DebugShadowAttenuation = shadow;
381 #else // LIGHT_EVALUATION_NO_SHADOWS
386 //-----------------------------------------------------------------------------
387 // Reflection probe evaluation helper
388 //-----------------------------------------------------------------------------
390 #ifndef OVERRIDE_EVALUATE_ENV_INTERSECTION
391 // Environment map share function
392 #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Reflection/VolumeProjection.hlsl"
394 void EvaluateLight_EnvIntersection(float3 positionWS, float3 normalWS, EnvLightData light, int influenceShapeType, inout float3 R, inout float weight)
396 // Guideline for reflection volume: In HDRenderPipeline we separate the projection volume (the proxy of the scene) from the influence volume (what pixel on the screen is affected)
397 // However we add the constrain that the shape of the projection and influence volume is the same (i.e if we have a sphere shape projection volume, we have a shape influence).
398 // It allow to have more coherence for the dynamic if in shader code.
399 // Users can also chose to not have any projection, in this case we use the property minProjectionDistance to minimize code change. minProjectionDistance is set to huge number
400 // that simulate effect of no shape projection
402 float3x3 worldToIS = WorldToInfluenceSpace(light); // IS: Influence space
403 float3 positionIS = WorldToInfluencePosition(light, worldToIS, positionWS);
404 float3 dirIS = normalize(mul(R, worldToIS));
406 float3x3 worldToPS = WorldToProxySpace(light); // PS: Proxy space
407 float3 positionPS = WorldToProxyPosition(light, worldToPS, positionWS);
408 float3 dirPS = mul(R, worldToPS);
410 float projectionDistance = 0;
412 // Process the projection
413 // In Unity the cubemaps are capture with the localToWorld transform of the component.
414 // This mean that location and orientation matter. So after intersection of proxy volume we need to convert back to world.
415 if (influenceShapeType == ENVSHAPETYPE_SPHERE)
417 projectionDistance = IntersectSphereProxy(light, dirPS, positionPS);
418 // We can reuse dist calculate in LS directly in WS as there is no scaling. Also the offset is already include in light.capturePositionRWS
419 R = (positionWS + projectionDistance * R) - light.capturePositionRWS;
421 weight = InfluenceSphereWeight(light, normalWS, positionWS, positionIS, dirIS);
423 else if (influenceShapeType == ENVSHAPETYPE_BOX)
425 projectionDistance = IntersectBoxProxy(light, dirPS, positionPS);
426 // No need to normalize for fetching cubemap
427 // We can reuse dist calculate in LS directly in WS as there is no scaling. Also the offset is already include in light.capturePositionRWS
428 R = (positionWS + projectionDistance * R) - light.capturePositionRWS;
430 weight = InfluenceBoxWeight(light, normalWS, positionWS, positionIS, dirIS);
434 weight = Smoothstep01(weight);
435 weight *= light.weight;