// ... // Need #define Before Lighting.hlsl, Shadows.hlsl: //#define _SURFACE_TYPE_TRANSPARENT #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl" // https://docs.unity3d.com/6000.3/Documentation/Manual/urp/use-built-in-shader-methods-shadows.html // https://docs.unity3d.com/6000.3/Documentation/Manual/urp/use-built-in-shader-methods-additional-lights-fplus.html //#pragma multi_compile _ _CLUSTER_LIGHT_LOOP #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS // Light cookie support. #pragma multi_compile_fragment _ _LIGHT_COOKIES // Assign these via script. // Can't use name _AdditionalLightsCount (with plural 'Lights') // because it would be a redefinition of an existing variable that // I can't use because it appears to be used by Unity... uint _AdditionalLightCount; float4 _AmbientLighting; // ... #pragma multi_compile _ PROBE_VOLUMES_L1 PROBE_VOLUMES_L2 #include "Packages/com.unity.render-pipelines.core/Runtime/Lighting/ProbeVolume/ProbeVolume.hlsl" float3 SampleAPV(float3 positionWS, float3 viewDirectionWS, float2 uvPP) { float3 bakedGI = 0.0; #if defined(PROBE_VOLUMES_L1) || defined(PROBE_VOLUMES_L2) // For fog, there is no surface normal. // Use consistent vector: -viewDirWS for camera-facing fog. float3 normalWS = -viewDirectionWS; uint renderingLayer = 0xFFFFFFFF; EvaluateAdaptiveProbeVolume(positionWS, normalWS, viewDirectionWS, uvPP, renderingLayer, bakedGI); #endif return bakedGI; } // ... // Henyey–Greenstein anisotropic phase function. // Would optimization via pre-compute really matter? // Compiler may take care of it... // Else, I'll need to pass in pre-computed values for anisotropySqr, etc. //float HenyeyGreensteinPhase(float VdotL, float anisotropy) //{ // float g = anisotropy; // float gSqr = g * g; // float denom = 1.0 + gSqr - ((2.0 * g) * VdotL); // return (1.0 - gSqr) / (4.0 * PI * denom * sqrt(denom)); //} float HenyeyGreensteinPhase(float VdotL, float anisotropy) { float g = anisotropy; //float g = clamp(anisotropy, -0.9999, 0.9999); float gSqr = g * g; float denom = 1.0 + gSqr - 2.0 * g * VdotL; return (1.0 - gSqr) / (denom * sqrt(denom)); // g = 0.0 → 1.0, energy-neutral. } // ... float GetVolumetricFogDensity(float3 positionWS) { // TO-DO: noise, detail, height, etc. return 1.0; } // Height fog. float GetVolumetricFogDensity(float3 positionWS, float heightDistance, float heightOffset, float heightFalloff, float heightRemapMin, float heightRemapMax) { // Height of this sample above the fog base. float heightAboveBase = (positionWS.y - heightOffset) - heightDistance; // Remap height factor before exp shaping. float heightFactor = smoothstep(heightRemapMin, heightRemapMax, heightAboveBase); // Exponential falloff: full density at base, thinning with height. float heightDensity = exp(-heightFactor * heightFalloff); //float f = exp(-heightFalloff); //float heightDensity = saturate((exp(-heightFactor * heightFalloff) - f) / (1.0 - f)); return heightDensity; } struct SampleData { float scale; float2 tiling; float2 offset; float2 animation; float4 SampleTexture(Texture2D tex, float2 uv) { uv *= scale; uv *= tiling; uv -= offset; uv -= animation * _Time.y; // Using the LOD version is necessary for large-area fog. //return SAMPLE_TEXTURE2D(tex, sampler_LinearRepeat, uv); return SAMPLE_TEXTURE2D_LOD(tex, sampler_LinearRepeat, uv, 0); //return SAMPLE_TEXTURE2D_LOD(tex, sampler_LinearRepeat, frac(uv), 0); } }; struct FogDensityData { float density; bool enableHeightMask; float heightMaskBlend; float heightMaskLength; float heightMaskOffset; float heightMaskFalloff; float heightMaskRemapMin; float heightMaskRemapMax; bool enableHeightMaskTexture; float heightMaskTextureAmplitude; float heightMaskTextureScale; float2 heightMaskTextureAnimation; float heightMaskTexturePower; // Density at position, before step-length multiply. float Evaluate(float3 positionWS, Texture2D heightMaskTexture) { float result = GetVolumetricFogDensity(positionWS); if (enableHeightMask) { float heightMaskOffsetComposite = heightMaskOffset; if (enableHeightMaskTexture) { SampleData heightMaskTextureSampleData; heightMaskTextureSampleData.scale = heightMaskTextureScale; heightMaskTextureSampleData.tiling = _Height_Mask_Texture_ST.xy; heightMaskTextureSampleData.offset = _Height_Mask_Texture_ST.zw; heightMaskTextureSampleData.animation = heightMaskTextureAnimation; float heightMaskTextureSample = heightMaskTextureSampleData.SampleTexture(heightMaskTexture, positionWS.xz).r; heightMaskTextureSample = pow(heightMaskTextureSample, heightMaskTexturePower); // Remap from [0.0, 1.0] to [-1.0, 1.0], and scale. heightMaskTextureSample = (heightMaskTextureSample * 2.0) - 1.0; heightMaskTextureSample *= heightMaskTextureAmplitude; heightMaskOffsetComposite += heightMaskTextureSample; } float result_heightMask = GetVolumetricFogDensity(positionWS, heightMaskLength, heightMaskOffsetComposite, heightMaskFalloff, heightMaskRemapMin, heightMaskRemapMax); result = lerp(result, result_heightMask, heightMaskBlend); } return result; } }; // Main light self-shadow. // Marches from sample toward directional light, accumulating fog depth. // Returns path transmittance: 1.0 = clear, approaching 0.0 = occluded by fog. // Noise doesn't look good here. It just adds MORE banding. // So- no noise. Which is fine; the main loop already dithers. float GetLightSelfShadow( float3 positionWS, float3 lightDirectionWS, int steps, float dist, // Lol. Isn't 'distance' reserved? Rename this later, maybe. FogDensityData densityData, Texture2D heightMaskTexture) { float stepLength = dist / steps; float opticalDepth = 0.0; [loop] for (int i = 0; i < steps; ++i) { float shadowSampleDistance = (i + 0.5) * stepLength; float3 shadowSamplePositionWS = positionWS + (lightDirectionWS * shadowSampleDistance); float shadowSampleDensity = densityData.Evaluate(shadowSamplePositionWS, heightMaskTexture); opticalDepth += (shadowSampleDensity * densityData.density) * stepLength; } return exp(-opticalDepth); } // Volumetric fog. // WS = world space. void VolumetricFog_float( // ... float3 positionWS, float3 normalWS, float2 uvSS, float4 colour, bool enableShadowColours, float4 shadowColour, float4 selfShadowColour, int steps, float density, float maxDistance, // ... bool enableAnisotropy, float anisotropy, float anisotropyBlend, // ... bool enableBlurTexture, Texture2D blurTexture, float blurTextureBlend, float blurTextureRemapMin, float blurTextureRemapMax, // ... bool enableHeightMask, float heightMaskBlend, float heightMaskLength, float heightMaskOffset, float heightMaskFalloff, float heightMaskRemapMin, float heightMaskRemapMax, // ... bool enableHeightMaskTexture, Texture2D heightMaskTexture, float heightMaskTextureAmplitude, float heightMaskTextureScale, float2 heightMaskTextureAnimation, float heightMaskTexturePower, // ... bool enableHeightGradient, float heightGradientBlend, float4 heightGradientColourTop, float4 heightGradientColourBottom, float heightGradientLength, float heightGradientOffset, float heightGradientFalloff, float heightGradientRemapMin, float heightGradientRemapMax, // ... bool enableHeightGradientTexture, Texture2D heightGradientTexture, float heightGradientTextureAmplitude, float heightGradientTextureScale, float2 heightGradientTextureAnimation, float heightGradientTexturePower, // ... bool enableHeightGradientLUT, Texture2D heightGradientLUT, float heightGradientLUTBlend, // ... float ambientLightScale, // ... bool enableAdaptiveProbeVolumes, float adaptiveProbeVolumeScale, float adaptiveProbeVolumePower, // ... bool enableMainLightSelfShadow, int mainLightSelfShadowSteps, float mainLightSelfShadowDistance, // ... bool enableAdditionalLightSelfShadow, int additionalLightSelfShadowSteps, float additionalLightSelfShadowDistance, // ... bool enableSelfShadowCurves, float selfShadowPower, float selfShadowRemapMin, float selfShadowRemapMax, // Final fog composite/mix with the scene. // If I want the final fog, use this. out float4 composite, // Output lighting and transmittance separately. // If I want to composite them separate, use these. out float3 lighting, out float transmittance) { // -- SETUP. float3 cameraPositionWS = GetCameraPositionWS(); //float3 offsetToSurfaceWS = positionWS - cameraPositionWS; // Edit: need to calculate offset to surface manually. // > prevent issues from precision loss away from origin. float rawDepth = SampleSceneDepth(uvSS); // Unproject to view space manually. // ComputeViewSpacePosition() negates z (SRP core convention), // which mirrors ray through camera plane before I_V rotation. // -- do *not* use it here. // I_VP = I_V * I_P -> this is Shader Graph's old reconstruction, // split so camera's world translation never enters the equation. float4 positionCS = ComputeClipSpacePosition(uvSS, rawDepth); float4 positionVS = mul(UNITY_MATRIX_I_P, positionCS); // Perspective divide. // > Orthographic, w == 1.0. positionVS.xyz = positionVS.xyz / positionVS.w; //float aspectRatio = _ScreenParams.x / _ScreenParams.y; /* // Distance and direction for perspective-only calculations. float3 offsetToSurfaceWS = mul((float3x3) UNITY_MATRIX_I_V, positionVS); // Distance between camera and surface (vertex or fragment). float distanceToSurfaceWS = length(offsetToSurfaceWS); // Direction from camera to surface == -(view direction). float3 directionToSurfaceWS = offsetToSurfaceWS / distanceToSurfaceWS; */ // Setup support for both perspective and orthographic cameras/views (rendering). // Build ray in view space, where perspective/orthographic are cleanly differentiated. // Perspective: rays fan out into a frustum from camera origin (XYZ == 0.0). // Orthographic: rays shoot directly parallel to each other, down [view] -Z. // In orthographic, each ray originates on its own point on the camera plane. float3 rayOriginVS; float3 directionToSurfaceVS; float distanceToSurfaceWS; if (unity_OrthoParams.w == 1.0) { rayOriginVS = float3(positionVS.xy, 0.0); directionToSurfaceVS = float3(0.0, 0.0, -1.0); distanceToSurfaceWS = abs(positionVS.z); } else { rayOriginVS = float3(0.0, 0.0, 0.0); directionToSurfaceVS = normalize(positionVS.xyz); distanceToSurfaceWS = length(positionVS.xyz); } //float aspectRatio = _ScreenParams.x / _ScreenParams.y; // Rotate into world orientation. (3x3 only, no translation -- preserves precision). // Origin offset is camera-relative, added to camera position as raymarch origin. float3 rayOriginWS = cameraPositionWS + mul((float3x3) UNITY_MATRIX_I_V, rayOriginVS); float3 directionToSurfaceWS = mul((float3x3) UNITY_MATRIX_I_V, directionToSurfaceVS); // Screen pixel coordinates. // '_ScaledScreenParams' better for scaled resolution consistency vs. '_ScreenParams'. // Example: like the renderer settings/asset scale slider. float2 uvPP = uvSS * _ScaledScreenParams.xy; // Noise. int frame = (_Time.y * 60.0) % 60; float interleavedGradientNoise = InterleavedGradientNoise(uvPP, frame); // Distance. // Limiting this allows for higher resolution. // Same number of steps, covering a smaller distance. float raymarchDistanceWS = distanceToSurfaceWS; raymarchDistanceWS = min(raymarchDistanceWS, maxDistance); // Lighting. lighting = 0.0; InputData inputData = (InputData) 0; inputData.normalWS = normalWS; inputData.viewDirectionWS = -directionToSurfaceWS; inputData.normalizedScreenSpaceUV = uvSS; // Get main light *now* -- it will not change during raymarch. #ifdef _ENABLE_MAIN_LIGHT Light mainLight = GetMainLight(); // Main colour doesn't change in raymarch loop. // Precision has practically no purpose being less than 0.001. // Note: consider making '0.001' a named constant like 'lightEpsilon', // and using it for all light (or otherwise?)... precision stuff. // I *think* the two max() calls are actually faster or less instructions than the tri-circuit '||' (or-logic) checks. //bool mainLightContributes = // mainLighting.r > 0.001 || // mainLighting.g > 0.001 || // mainLighting.b > 0.001; bool mainLightContributes = max(mainLight.color.r, max(mainLight.color.g, mainLight.color.b)) > 0.001; #endif // -- RAYMARCH. float rayStepWS = raymarchDistanceWS / steps; float rayStepNoiseWS = rayStepWS * interleavedGradientNoise; float densityPerStepWS = density * rayStepWS; // Transmittance: How much light can pass through to the camera. // Starts at 1.0 (fully clear) and decays towards 0.0 (fully blocked). transmittance = 1.0; // Pack density settings once, for both ray- and self-shadow marching. FogDensityData densityData; densityData.density = density; densityData.enableHeightMask = enableHeightMask; densityData.heightMaskBlend = heightMaskBlend; densityData.heightMaskLength = heightMaskLength; densityData.heightMaskOffset = heightMaskOffset; densityData.heightMaskFalloff = heightMaskFalloff; densityData.heightMaskRemapMin = heightMaskRemapMin; densityData.heightMaskRemapMax = heightMaskRemapMax; densityData.enableHeightMaskTexture = enableHeightMaskTexture; densityData.heightMaskTextureAmplitude = heightMaskTextureAmplitude; densityData.heightMaskTextureScale = heightMaskTextureScale; densityData.heightMaskTextureAnimation = heightMaskTextureAnimation; densityData.heightMaskTexturePower = heightMaskTexturePower; // Loop. [loop] for (int i = 0; i < steps; ++i) { // Calculate distance along ray for this step/iteration. float stepRayDistanceWS = i * rayStepWS; // Add up to one full step/iteration (noise is [0.0, 1.0] of IGNoise-based offset. stepRayDistanceWS += rayStepNoiseWS; // Depth test. if (stepRayDistanceWS > raymarchDistanceWS) { break; } // Calculate current position along ray in world space. // Note: replaced 'cameraPositionWS' with 'rayOriginWS'. // -- to support both perspective and orthographic rendering. float3 stepPositionWS = rayOriginWS + (directionToSurfaceWS * stepRayDistanceWS); // -- LIGHTING. // Setup lighting data for this position. inputData.positionWS = stepPositionWS; // Sample density. // Can vary based on noise/height/etc. float stepDensity = densityData.Evaluate(stepPositionWS, heightMaskTexture); stepDensity *= densityPerStepWS; // Calculate transmittance for this specific step (Beer-Lambert Law). float stepTransmittance = exp(-stepDensity); // Calculate scattering probability. // > Amount of light captured and scattered, this step. // Physically cannot exceed 1.0, unlike raw density. float stepScattering = 1.0 - stepTransmittance; // Combined lighting for this step... float3 stepLighting = 0.0; //#define _ENABLE_MAIN_LIGHT //#define _ENABLE_MAIN_LIGHT_SHADOWS // 1. Main light. #ifdef _ENABLE_MAIN_LIGHT if (mainLightContributes) { // Default to fully lit (no geometry shadows), until calculated/derived otherwise. mainLight.shadowAttenuation = 1.0; float mainLightSelfShadowAttenuation = 1.0; #ifdef _ENABLE_MAIN_LIGHT_SHADOWS inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS); // MainLightShadow() > MainLightRealtimeShadow(). // > it includes the fade to remove hard clipping. //mainLight.shadowAttenuation = MainLightRealtimeShadow(inputData.shadowCoord); //// Fade necessary to remove hard clipping and related artifacts at shadow distance limit. //half fade = GetMainLightShadowFade(inputData.positionWS); //mainLight.shadowAttenuation = lerp(mainLight.shadowAttenuation, 1.0, fade); mainLight.shadowAttenuation = MainLightShadow(inputData.shadowCoord, inputData.positionWS, half4(1.0, 1.0, 1.0, 1.0), _MainLightOcclusionProbes); // Blend between fully lit (1.0) and occluded by geometry, // based on shadow colour alpha (0.0 if shadows disabled, else > 0.0). if (enableShadowColours) { mainLight.shadowAttenuation = lerp(1.0, mainLight.shadowAttenuation, shadowColour.a); } #endif // Light cookie: per-position projection mask. // Cookies vary with world position, so sample per step. float3 mainLightCookie = 1.0; float mainLightCookieMax = 1.0; // I thought about adding a dedicated bool to additionally gate // lighting cookie functions and sampling, but I figure the cost // is relatively negligible to having another bool to track, and // I can't think of a strong/coherent use-case for not also having // light cookies apply to the volumetric fog lighting. #if defined(_LIGHT_COOKIES) mainLightCookie = SampleMainLightCookie(inputData.positionWS); mainLightCookieMax = max(mainLightCookie.r, max(mainLightCookie.g, mainLightCookie.b)); #endif // Self-shadow: attenuate by the fog between this sample and the light. // No contribution from light (absolutely blackened by light's shadow)? // If geometry fully occluding light, there's nothing for fog to attenuate as shadow. // > Skip self-shadowing. //bool mainLightReachesHere = mainLight.shadowAttenuation > 0.001; bool mainLightReachesHere = (mainLight.shadowAttenuation > 0.001) && (mainLightCookieMax > 0.001); if (enableMainLightSelfShadow && mainLightReachesHere) { mainLightSelfShadowAttenuation = GetLightSelfShadow( inputData.positionWS, mainLight.direction, mainLightSelfShadowSteps, mainLightSelfShadowDistance, densityData, heightMaskTexture ); // Blend between fully lit (1.0) and self-shadowed by fog, based on current geometry occlusion. // If not occluded by geometry, shadowAttenuation == 1.0; Anything less is partial occlusion/shading. // If light is fully occluded by geometry, then there is no self-shadowing. // Self-shadowing requires lighting to reach where there will be a shadow. // Alpha only applies when shadow colours are on. // Do *not* pre-multiply into mainLight.shadowAttenuation, // as that is for geometry shadows, not self-shadows. float mainLightSelfShadowStrength = enableShadowColours ? selfShadowColour.a : 1.0; //mainLightSelfShadowAttenuation = lerp(1.0, mainLightSelfShadowAttenuation, mainLight.shadowAttenuation * mainLightSelfShadowStrength); mainLightSelfShadowAttenuation = lerp(1.0, mainLightSelfShadowAttenuation, (mainLight.shadowAttenuation * mainLightCookieMax) * mainLightSelfShadowStrength); if (enableSelfShadowCurves) { mainLightSelfShadowAttenuation = smoothstep(selfShadowRemapMin, selfShadowRemapMax, mainLightSelfShadowAttenuation); mainLightSelfShadowAttenuation = pow(mainLightSelfShadowAttenuation, selfShadowPower); } } // Shadow colouring. Blend shadowed regions toward custom colour. // RGB tints, alpha blends. If alpha == 0.0, no shadows... float3 mainLighting = mainLight.color; mainLighting *= mainLightCookie; // Anisotropy applies to direct light only, *before* shadow colouring. if (enableAnisotropy) { float mainLight_VdotL = dot(directionToSurfaceWS, mainLight.direction); float mainLight_phase = HenyeyGreensteinPhase(mainLight_VdotL, anisotropy); mainLight_phase = lerp(1.0, mainLight_phase, anisotropyBlend); mainLighting *= mainLight_phase; } if (!enableShadowColours) { mainLighting *= mainLight.shadowAttenuation; mainLighting *= mainLightSelfShadowAttenuation; } else { mainLighting = lerp(mainLighting, shadowColour.rgb, 1.0 - mainLight.shadowAttenuation); mainLighting = lerp(mainLighting, selfShadowColour.rgb, 1.0 - mainLightSelfShadowAttenuation); } // Add main light to this step's lighting. stepLighting += mainLighting; } #endif // 2. Additional lights. #ifdef _ENABLE_ADDITIONAL_LIGHTS //int lightCount = GetAdditionalLightsCount(); // Doesn't seem to work for fullscreen effects. // _AdditionalLightsCount already exists as a global URP uniform (hence the redefinition error). // It is bound automatically every frame, and holds the post-cull additional light count in .x. [loop] LIGHT_LOOP_BEGIN(_AdditionalLightCount) { Light additionalLight = GetAdditionalPerObjectLight(lightIndex, inputData.positionWS); // This one works for post-processing. //Light additionalLight = GetAdditionalLight(lightIndex, inputData.positionWS, inputData.shadowMask); additionalLight.shadowAttenuation = 1.0; float additionalLightSelfShadowAttenuation = 1.0; #ifdef _ENABLE_ADDITIONAL_LIGHT_SHADOWS //additionalLight.shadowAttenuation = AdditionalLightRealtimeShadow( // lightIndex, // inputData.positionWS, // additionalLight.direction, // GetAdditionalLightShadowParams(lightIndex), // GetAdditionalLightShadowSamplingData(lightIndex) //); //half fade = GetAdditionalLightShadowFade(inputData.positionWS); //additionalLight.shadowAttenuation = lerp(additionalLight.shadowAttenuation, 1.0, fade); additionalLight.shadowAttenuation = AdditionalLightShadow( lightIndex, inputData.positionWS, additionalLight.direction, half4(1.0, 1.0, 1.0, 1.0), _AdditionalLightsOcclusionProbes[lightIndex] ); if (enableShadowColours) { additionalLight.shadowAttenuation = lerp(1.0, additionalLight.shadowAttenuation, shadowColour.a); } #endif float3 additionalLightCookie = 1.0; float additionalLightCookieMax = 1.0; #if defined(_LIGHT_COOKIES) additionalLightCookie = SampleAdditionalLightCookie(lightIndex, inputData.positionWS); additionalLightCookieMax = max(additionalLightCookie.r, max(additionalLightCookie.g, additionalLightCookie.b)); #endif // Self-shadow: attenuate by fog between *this* [sample] and light. // Check if light is dead here -- out of range/cone, or fully shadowed. //bool additionalLightReachesHere = // additionalLight.shadowAttenuation > 0.001 && // additionalLight.distanceAttenuation > 0.00001; // Needs to be more precise, hence additional decimal place(s). bool additionalLightReachesHere = (additionalLight.shadowAttenuation > 0.001) && (additionalLight.distanceAttenuation > 0.00001) && // Needs to be more precise, hence additional decimal place(s). (additionalLightCookieMax > 0.001); // ^ Without extra precision for distanceAttenuation, // there's noticeable clipping of self-shading in some cases. // Reaches here bool saves a *lot* of FPS (tested and confirmed), // > no expensive self-shadow calculations for non-contribution. if (enableAdditionalLightSelfShadow && additionalLightReachesHere) { // Point/spot lights are at finite position, so clamp march to // distance to light, else: it accumulates fog behind a near one. // -- (w = 1.0 for point/spot, 0.0 for a directional additional light.) float4 lightPositionWS = _AdditionalLightsPosition[lightIndex]; float distanceToLight = length(lightPositionWS.xyz - inputData.positionWS); //float selfShadowDistance = min(additionalLightSelfShadowDistance, distanceToLight); // Handle distance clamping for additional directional lights. float clampDistance = lerp(additionalLightSelfShadowDistance, distanceToLight, lightPositionWS.w); float selfShadowDistance = min(additionalLightSelfShadowDistance, clampDistance); additionalLightSelfShadowAttenuation = GetLightSelfShadow( inputData.positionWS, additionalLight.direction, additionalLightSelfShadowSteps, selfShadowDistance, densityData, heightMaskTexture ); float additionalLightSelfShadowStrength = enableShadowColours ? selfShadowColour.a : 1.0; //additionalLightSelfShadowAttenuation = lerp(1.0, additionalLightSelfShadowAttenuation, additionalLight.shadowAttenuation * additionalLightSelfShadowStrength); additionalLightSelfShadowAttenuation = lerp(1.0, additionalLightSelfShadowAttenuation, (additionalLight.shadowAttenuation * additionalLightCookieMax) * additionalLightSelfShadowStrength); if (enableSelfShadowCurves) { additionalLightSelfShadowAttenuation = smoothstep(selfShadowRemapMin, selfShadowRemapMax, additionalLightSelfShadowAttenuation); additionalLightSelfShadowAttenuation = pow(additionalLightSelfShadowAttenuation, selfShadowPower); } } float3 additionalLighting = additionalLight.color; additionalLighting *= additionalLightCookie; // Anisotropy applies to direct light only, *before* shadow colouring. if (enableAnisotropy) { float additionalLight_VdotL = dot(directionToSurfaceWS, additionalLight.direction); float additionalLight_phase = HenyeyGreensteinPhase(additionalLight_VdotL, anisotropy); additionalLight_phase = lerp(1.0, additionalLight_phase, anisotropyBlend); additionalLighting *= additionalLight_phase; } if (!enableShadowColours) { additionalLighting *= additionalLight.shadowAttenuation; additionalLighting *= additionalLightSelfShadowAttenuation; } else { additionalLighting = lerp(additionalLighting, shadowColour.rgb, 1.0 - additionalLight.shadowAttenuation); additionalLighting = lerp(additionalLighting, selfShadowColour.rgb, 1.0 - additionalLightSelfShadowAttenuation); } // This goes *after* custom shadow colours. additionalLighting *= additionalLight.distanceAttenuation; // Add additional light to this step's lighting. stepLighting += additionalLighting; } LIGHT_LOOP_END #endif // 3. Ambient light. #ifdef _ENABLE_AMBIENT_LIGHT stepLighting += _AmbientLighting.rgb * ambientLightScale; #endif // 4. Adaptive probe volumes. if (enableAdaptiveProbeVolumes) { // Note: changed uvSS to uvPP -> removes sampling seams in some places (like the green cube on the right). // uvPP seems to be the more consistent/correct choice for sampling probe volumes in world space... float3 apvSample = SampleAPV(stepPositionWS, -directionToSurfaceWS, uvPP); // SH ringing can go negative. Negative irradiance is non-physical, and NaNs pow. // Don't allow negative values -> fixes issues with flickering bright spots/flashes. // -- (without needing additional anti-aliasing on the camera). apvSample = max(apvSample, 0.0); apvSample = pow(apvSample, adaptiveProbeVolumePower); stepLighting += apvSample * adaptiveProbeVolumeScale; } // 5. Height gradient tint. if (enableHeightGradient) { float heightGradientOffsetComposite = heightGradientOffset; if (enableHeightGradientTexture) { SampleData heightGradientTextureSampleData; heightGradientTextureSampleData.scale = heightGradientTextureScale; heightGradientTextureSampleData.tiling = _Height_Gradient_Texture_ST.xy; heightGradientTextureSampleData.offset = _Height_Gradient_Texture_ST.zw; heightGradientTextureSampleData.animation = heightGradientTextureAnimation; float heightGradientTextureSample = heightGradientTextureSampleData.SampleTexture(heightGradientTexture, stepPositionWS.xz).r; heightGradientTextureSample = pow(heightGradientTextureSample, heightGradientTexturePower); // Remap from [0.0, 1.0] to [-1.0, 1.0], and scale. heightGradientTextureSample = (heightGradientTextureSample * 2.0) - 1.0; heightGradientTextureSample *= heightGradientTextureAmplitude; heightGradientOffsetComposite += heightGradientTextureSample; } // Normalized position of sample within gradient range. float heightGradientFactor = (stepPositionWS.y - heightGradientOffsetComposite) / heightGradientLength; // Window, then shape. heightGradientFactor = smoothstep(heightGradientRemapMin, heightGradientRemapMax, heightGradientFactor); heightGradientFactor = 1.0 - exp(-heightGradientFactor * heightGradientFalloff); // Bottom-to-top colour by height. float3 heightGradientColourTopTint = lerp(1.0, heightGradientColourTop.rgb, heightGradientColourTop.a); float3 heightGradientColourBottomTint = lerp(1.0, heightGradientColourBottom.rgb, heightGradientColourBottom.a); float3 heightGradientColour = lerp(heightGradientColourBottomTint, heightGradientColourTopTint, heightGradientFactor); // Blend tint. heightGradientColour = lerp(1.0, heightGradientColour, heightGradientBlend); // Optional LUT for complex/artistic colour grading by height. if (enableHeightGradientLUT) { //float4 heightGradientLUTSample = SAMPLE_TEXTURE2D(heightGradientLUT, sampler_LinearClamp, float2(heightGradientFactor, 0.0)); float4 heightGradientLUTSample = SAMPLE_TEXTURE2D_LOD(heightGradientLUT, sampler_LinearClamp, float2(heightGradientFactor, 0.0), 0); heightGradientColour *= lerp(1.0, heightGradientLUTSample.rgb, heightGradientLUTSample.a * heightGradientLUTBlend); } stepLighting *= heightGradientColour; } // -- INTEGRATION. // Apply scattering. stepLighting *= stepScattering; // Accumulate light into final' buffer'. // Crucial: multiply by transmittance. // Light deeper in the fog obscured by fog already stepped through. lighting += stepLighting * transmittance; // -- ABSORPTION. // Update transmittance for the NEXT step/iteration/cycle. // Beer-Lambert law: exp(-density). transmittance *= stepTransmittance; // Early exit if transmittance is nearly zero (opaque). if (transmittance < 0.001) { transmittance = 0.0f; break; } } // Tint accumulated light. lighting *= colour.rgb; // -- COMPOSITE (FINAL COLOUR). // Blend to blur based on scattering, attenuated by fog alpha. float4 sceneColour = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uvSS); if (enableBlurTexture) { // Blur. // [0.0, 1.0] depth = distance to surface (from camera, in units [meters]) / cameraFarPlane; //float linearDepth = distanceToSurfaceWS / _ProjectionParams.z; //float scattering = smoothstep(remapMin, remapMax, linearDepth); // UPDATE: Blur by fog 'density' (inverse transmittance). float blur = smoothstep(blurTextureRemapMin, blurTextureRemapMax, 1.0 - transmittance); blur *= blurTextureBlend; float4 sceneColour_blur = SAMPLE_TEXTURE2D(blurTexture, sampler_LinearClamp, uvSS); sceneColour = lerp(sceneColour, sceneColour_blur, blur * colour.a); } // Scene colour multiplied by remaining transmittance (whatever wasn't blocked). // Fog light is then added on top. composite.rgb = (sceneColour.rgb * transmittance) + lighting; composite.rgb = lerp(sceneColour.rgb, composite.rgb, colour.a); composite.a = sceneColour.a; }