#ifndef UNITY_ATMOSPHERIC_SCATTERING_INCLUDED #define UNITY_ATMOSPHERIC_SCATTERING_INCLUDED #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/VolumeRendering.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Filtering.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/AtmosphericScattering/AtmosphericScattering.cs.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/VolumetricLighting/VBuffer.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Sky/PhysicallyBasedSky/PhysicallyBasedSkyCommon.hlsl" #ifdef DEBUG_DISPLAY #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Debug/DebugDisplay.hlsl" #endif TEXTURE3D(_VBufferLighting); float3 ExpLerp(float3 A, float3 B, float t, float x, float y) { // Remap t: (exp(10 k t) - 1) / (exp(10 k) - 1) = exp(x t) y - y. t = exp(x * t) * y - y; // Perform linear interpolation using the new value of t. return lerp(A, B, t); } float3 GetFogColor(float3 V, float fragDist) { float3 color = _FogColor.rgb; if (_FogColorMode == FOGCOLORMODE_SKY_COLOR) { // Based on Uncharted 4 "Mip Sky Fog" trick: http://advances.realtimerendering.com/other/2016/naughty_dog/NaughtyDog_TechArt_Final.pdf float mipLevel = (1.0 - _MipFogMaxMip * saturate((fragDist - _MipFogNear) / (_MipFogFar - _MipFogNear))) * (ENVCONSTANTS_CONVOLUTION_MIP_COUNT - 1); // For the atmospheric scattering, we use the GGX convoluted version of the cubemap. That matches the of the idnex 0 color *= SampleSkyTexture(-V, mipLevel, 0).rgb; // '_FogColor' is the tint } return color; } // All units in meters! // Assumes that there is NO sky occlusion along the ray AT ALL. // We evaluate atmospheric scattering for the sky and other celestial bodies // during the sky pass. The opaque atmospheric scattering pass applies atmospheric // scattering to all other opaque geometry. void EvaluatePbrAtmosphere(float3 worldSpaceCameraPos, float3 V, float distAlongRay, bool renderSunDisk, out float3 skyColor, out float3 skyOpacity) { skyColor = skyOpacity = 0; const float R = _PlanetaryRadius; const float2 n = float2(_AirDensityFalloff, _AerosolDensityFalloff); const float2 H = float2(_AirScaleHeight, _AerosolScaleHeight); // TODO: Not sure it's possible to precompute cam rel pos since variables // in the two constant buffers may be set at a different frequency? const float3 O = worldSpaceCameraPos - _PlanetCenterPosition.xyz; const float tFrag = abs(distAlongRay); // Clear the "hit ground" flag float3 N; float r; // These params correspond to the entry point float tEntry = IntersectAtmosphere(O, V, N, r).x; float tExit = IntersectAtmosphere(O, V, N, r).y; float NdotV = dot(N, V); float cosChi = -NdotV; float cosHor = ComputeCosineOfHorizonAngle(r); bool rayIntersectsAtmosphere = (tEntry >= 0); bool lookAboveHorizon = (cosChi >= cosHor); // Our precomputed tables only contain information above ground. // Being on or below ground still counts as outside. // If it's outside the atmosphere, we only need one texture look-up. bool hitGround = distAlongRay < 0; bool rayEndsInsideAtmosphere = (tFrag < tExit) && !hitGround; if (rayIntersectsAtmosphere) { float2 Z = R * n; float r0 = r, cosChi0 = cosChi; float r1 = 0, cosChi1 = 0; float3 N1 = 0; if (tFrag < tExit) { float3 P1 = O + tFrag * -V; r1 = length(P1); N1 = P1 * rcp(r1); cosChi1 = dot(P1, -V) * rcp(r1); // Potential swap. cosChi0 = (cosChi1 >= 0) ? cosChi0 : -cosChi0; } float2 ch0, ch1 = 0; { float2 z0 = r0 * n; ch0.x = RescaledChapmanFunction(z0.x, Z.x, cosChi0); ch0.y = RescaledChapmanFunction(z0.y, Z.y, cosChi0); } if (tFrag < tExit) { float2 z1 = r1 * n; ch1.x = ChapmanUpperApprox(z1.x, abs(cosChi1)) * exp(Z.x - z1.x); ch1.y = ChapmanUpperApprox(z1.y, abs(cosChi1)) * exp(Z.y - z1.y); } // We may have swapped X and Y. float2 ch = abs(ch0 - ch1); float3 optDepth = ch.x * H.x * _AirSeaLevelExtinction.xyz + ch.y * H.y * _AerosolSeaLevelExtinction; skyOpacity = 1 - TransmittanceFromOpticalDepth(optDepth); // from 'tEntry' to 'tFrag' for (uint i = 0; i < _DirectionalLightCount; i++) { DirectionalLightData light = _DirectionalLightDatas[i]; // Use scalar or integer cores (more efficient). bool interactsWithSky = asint(light.distanceFromCamera) >= 0; if (!interactsWithSky) continue; float3 L = -light.forward.xyz; // The sun disk hack causes some issues when applied to nearby geometry, so don't do that. if (renderSunDisk && asint(light.angularDiameter) != 0 && light.distanceFromCamera <= tFrag) { float c = dot(L, -V); if (-0.99999 < c && c < 0.99999) { float alpha = 0.5 * light.angularDiameter; float beta = acos(c); float gamma = min(alpha, beta); // Make sure that if (beta = Pi), no rotation is performed. gamma *= (PI - beta) * rcp(PI - gamma); // Perform a shortest arc rotation. float3 A = normalize(cross(L, -V)); float3x3 R = RotationFromAxisAngle(A, sin(gamma), cos(gamma)); // Rotate the light direction. L = mul(R, L); } } // TODO: solve in spherical coords? float height = r - R; float NdotL = dot(N, L); float3 projL = L - N * NdotL; float3 projV = V - N * NdotV; float phiL = acos(clamp(dot(projL, projV) * rsqrt(max(dot(projL, projL) * dot(projV, projV), FLT_EPS)), -1, 1)); TexCoord4D tc = ConvertPositionAndOrientationToTexCoords(height, NdotV, NdotL, phiL); float3 radiance = 0; // from 'tEntry' to 'tExit' // Single scattering does not contain the phase function. float LdotV = dot(L, V); // Air. radiance += lerp(SAMPLE_TEXTURE3D_LOD(_AirSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb, SAMPLE_TEXTURE3D_LOD(_AirSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb, tc.a) * AirPhase(LdotV); // Aerosols. // TODO: since aerosols are in a separate texture, // they could use a different max height value for improved precision. radiance += lerp(SAMPLE_TEXTURE3D_LOD(_AerosolSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb, SAMPLE_TEXTURE3D_LOD(_AerosolSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb, tc.a) * AerosolPhase(LdotV); // MS. radiance += lerp(SAMPLE_TEXTURE3D_LOD(_MultipleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb, SAMPLE_TEXTURE3D_LOD(_MultipleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb, tc.a); if (rayEndsInsideAtmosphere) { float3 radiance1 = 0; // from 'tFrag' to 'tExit' // TODO: solve in spherical coords? float height1 = r1 - R; float NdotV1 = -cosChi1; float NdotL1 = dot(N1, L); float3 projL1 = L - N1 * NdotL1; float3 projV1 = V - N1 * NdotV1; float phiL1 = acos(clamp(dot(projL1, projV1) * rsqrt(max(dot(projL1, projL1) * dot(projV1, projV1), FLT_EPS)), -1, 1)); tc = ConvertPositionAndOrientationToTexCoords(height1, NdotV1, NdotL1, phiL1); // Single scattering does not contain the phase function. // Air. radiance1 += lerp(SAMPLE_TEXTURE3D_LOD(_AirSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb, SAMPLE_TEXTURE3D_LOD(_AirSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb, tc.a) * AirPhase(LdotV); // Aerosols. // TODO: since aerosols are in a separate texture, // they could use a different max height value for improved precision. radiance1 += lerp(SAMPLE_TEXTURE3D_LOD(_AerosolSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb, SAMPLE_TEXTURE3D_LOD(_AerosolSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb, tc.a) * AerosolPhase(LdotV); // MS. radiance1 += lerp(SAMPLE_TEXTURE3D_LOD(_MultipleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb, SAMPLE_TEXTURE3D_LOD(_MultipleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb, tc.a); // L(tEntry, tFrag) = L(tEntry, tExit) - T(tEntry, tFrag) * L(tFrag, tExit) radiance = max(0, radiance - (1 - skyOpacity) * radiance1); } radiance *= light.color.rgb; // Globally scale the intensity skyColor += radiance; } skyColor = Desaturate(skyColor, _ColorSaturation); skyOpacity = Desaturate(skyOpacity, _AlphaSaturation) * _AlphaMultiplier; float horAngle = acos(cosHor); float chiAngle = acos(cosChi); // [start, end] -> [0, 1] : (x - start) / (end - start) = x * rcpLength - (start * rcpLength) // TEMPLATE_3_REAL(Remap01, x, rcpLength, startTimesRcpLength, return saturate(x * rcpLength - startTimesRcpLength)) float start = horAngle; float end = 0; float rcpLen = rcp(end - start); float nrmAngle = Remap01(chiAngle, rcpLen, start * rcpLen); // float angle = saturate((0.5 * PI) - acos(cosChi) * rcp(0.5 * PI)); skyColor *= ExpLerp(_HorizonTint.rgb, _ZenithTint.rgb, nrmAngle, _HorizonZenithShiftPower, _HorizonZenithShiftScale); } } float3 GetViewForwardDir1(float4x4 viewMatrix) { return -viewMatrix[2].xyz; } void EvaluateAtmosphericScattering(PositionInputs posInput, float3 V, out float3 color, out float3 opacity) { color = opacity = 0; #ifdef DEBUG_DISPLAY // Don't sample atmospheric scattering when lighting debug more are enabled so fog is not visible if (_DebugLightingMode >= DEBUGLIGHTINGMODE_DIFFUSE_LIGHTING && _DebugLightingMode <= DEBUGLIGHTINGMODE_EMISSIVE_LIGHTING) return; if (_DebugShadowMapMode == SHADOWMAPDEBUGMODE_SINGLE_SHADOW || _DebugLightingMode == DEBUGLIGHTINGMODE_LUX_METER || _DebugLightingMode == DEBUGLIGHTINGMODE_LUMINANCE_METER) return; #endif // TODO: do not recompute this, but rather pass it directly. // Note1: remember the hacked value of 'posInput.positionWS'. // Note2: we do not adjust it anymore to account for the distance to the planet. This can lead to wrong results (since the planet does not write depth). float fogFragDist = distance(posInput.positionWS, GetCurrentViewPosition()); if (_FogEnabled) { float4 volFog = float4(0.0, 0.0, 0.0, 0.0); float expFogStart = 0.0f; if (_EnableVolumetricFog != 0) { bool doBiquadraticReconstruction = _VolumetricFilteringEnabled == 0; // Only if filtering is disabled. float4 value = SampleVBuffer(TEXTURE3D_ARGS(_VBufferLighting, s_linear_clamp_sampler), posInput.positionNDC, fogFragDist, _VBufferViewportSize, _VBufferLightingViewportScale.xyz, _VBufferLightingViewportLimit.xyz, _VBufferDistanceEncodingParams, _VBufferDistanceDecodingParams, true, doBiquadraticReconstruction, false); // TODO: add some slowly animated noise (dither?) to the reconstructed value. // TODO: re-enable tone mapping after implementing pre-exposure. volFog = DelinearizeRGBA(float4(/*FastTonemapInvert*/(value.rgb), value.a)); expFogStart = _VBufferLastSliceDist; } // TODO: if 'posInput.linearDepth' is computed using 'posInput.positionWS', // and the latter resides on the far plane, the computation will be numerically unstable. float distDelta = fogFragDist - expFogStart; if ((distDelta > 0)) { // Apply the distant (fallback) fog. float3 positionWS = GetCurrentViewPosition() - V * expFogStart; float startHeight = positionWS.y; float cosZenith = -V.y; // For both homogeneous and exponential media, // Integrate[Transmittance[x] * Scattering[x], {x, 0, t}] = Albedo * Opacity[t]. // Note that pulling the incoming radiance (which is affected by the fog) out of the // integral is wrong, as it means that shadow rays are not volumetrically shadowed. // This will result in fog looking overly bright. float3 volAlbedo = _HeightFogBaseScattering.xyz / _HeightFogBaseExtinction; float odFallback = OpticalDepthHeightFog(_HeightFogBaseExtinction, _HeightFogBaseHeight, _HeightFogExponents, cosZenith, startHeight, distDelta); float trFallback = TransmittanceFromOpticalDepth(odFallback); float trCamera = 1 - volFog.a; volFog.rgb += trCamera * GetFogColor(V, fogFragDist) * GetCurrentExposureMultiplier() * volAlbedo * (1 - trFallback); volFog.a = 1 - (trCamera * trFallback); } color = volFog.rgb; // Already pre-exposed opacity = volFog.a; } // Sky pass already applies atmospheric scattering to the far plane. // This pass only handles geometry. if (_PBRFogEnabled && (posInput.deviceDepth != UNITY_RAW_FAR_CLIP_VALUE)) { float3 skyColor = 0, skyOpacity = 0; // Convert it to distance along the ray. Doesn't work with tilt shift, etc. float tFrag = posInput.linearDepth * rcp(dot(-V, GetViewForwardDir1(UNITY_MATRIX_V))); EvaluatePbrAtmosphere(_WorldSpaceCameraPos.xyz, V, tFrag, false, skyColor, skyOpacity); skyColor *= _IntensityMultiplier * GetCurrentExposureMultiplier(); // Rendering of fog and atmospheric scattering cannot really be decoupled. #if 0 // The best workaround is to deep composite them. float3 fogOD = OpticalDepthFromOpacity(fogOpacity); float3 fogRatio; fogRatio.r = (fogOpacity.r >= FLT_EPS) ? (fogOD.r * rcp(fogOpacity.r)) : 1; fogRatio.g = (fogOpacity.g >= FLT_EPS) ? (fogOD.g * rcp(fogOpacity.g)) : 1; fogRatio.b = (fogOpacity.b >= FLT_EPS) ? (fogOD.b * rcp(fogOpacity.b)) : 1; float3 skyRatio; skyRatio.r = (skyOpacity.r >= FLT_EPS) ? (skyOD.r * rcp(skyOpacity.r)) : 1; skyRatio.g = (skyOpacity.g >= FLT_EPS) ? (skyOD.g * rcp(skyOpacity.g)) : 1; skyRatio.b = (skyOpacity.b >= FLT_EPS) ? (skyOD.b * rcp(skyOpacity.b)) : 1; float3 logFogColor = fogRatio * fogColor; float3 logSkyColor = skyRatio * skyColor; float3 logCompositeColor = logFogColor + logSkyColor; float3 compositeOD = fogOD + skyOD; opacity = OpacityFromOpticalDepth(compositeOD); float3 rcpCompositeRatio; rcpCompositeRatio.r = (opacity.r >= FLT_EPS) ? (opacity.r * rcp(compositeOD.r)) : 1; rcpCompositeRatio.g = (opacity.g >= FLT_EPS) ? (opacity.g * rcp(compositeOD.g)) : 1; rcpCompositeRatio.b = (opacity.b >= FLT_EPS) ? (opacity.b * rcp(compositeOD.b)) : 1; color = rcpCompositeRatio * logCompositeColor; #else // Deep compositing assumes that the fog spans the same range as the atmosphere. // Our fog is short range, so deep compositing gives surprising results. // Using the "shallow" over operator is more appropriate in our context. // We could do something more clever with deep compositing, but this would // probably be a waste in terms of perf. CompositeOver(color, opacity, skyColor, skyOpacity, color, opacity); #endif } } #endif