// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= VolumetricFog.usf =============================================================================*/ #include "Common.ush" #include "Definitions.usf" #define SUPPORT_CONTACT_SHADOWS 0 #define USE_SOURCE_TEXTURE USE_RECT_LIGHT_TEXTURE #include "DeferredLightingCommon.ush" #include "LightGridCommon.ush" #include "HeightFogCommon.ush" #include "SHCommon.ush" #if DISTANCE_FIELD_SKY_OCCLUSION #include "DistanceFieldAOShared.ush" #include "DistanceField/GlobalDistanceFieldShared.ush" #endif #include "VolumeLightingCommon.ush" #include "VolumetricLightmapShared.ush" #include "ReflectionEnvironmentShared.ush" #include "ForwardShadowingCommon.ush" #include "ParticipatingMediaCommon.ush" #include "LightDataUniforms.ush" #if LUMEN_GI #define FrontLayerTranslucencyReflectionsStruct LumenGIVolumeStruct #include "Lumen/LumenTranslucencyVolumeShared.ush" #endif #include "Random.ush" #if VIRTUAL_SHADOW_MAP #include "VirtualShadowMaps/VirtualShadowMapProjectionCommon.ush" #endif #if USE_LOCAL_FOG_VOLUMES #include "LocalFogVolumes/LocalFogVolumeCommon.ush" #endif #ifndef USE_EMISSIVE #define USE_EMISSIVE 1 #endif #ifndef USE_RAYTRACED_SHADOWS #define USE_RAYTRACED_SHADOWS 0 #endif RWTexture3D RWVBufferA; #if USE_EMISSIVE RWTexture3D RWVBufferB; #endif #if !SHADING_PATH_MOBILE #define USE_CLOUD_TRANSMITTANCE 1 #include "VolumetricCloudCommon.ush" #endif float ComputeDepthFromZSlice(float ZSlice) { float SliceDepth = (exp2(ZSlice / VolumetricFog.GridZParams.z) - VolumetricFog.GridZParams.y) / VolumetricFog.GridZParams.x; return SliceDepth; } float4x4 UnjitteredClipToTranslatedWorld; float4x4 UnjitteredPrevTranslatedWorldToClip; float3 ComputeCellTranslatedWorldPosition(uint3 GridCoordinate, float3 CellOffset, out float SceneDepth) { float2 VolumeUV = (GridCoordinate.xy + CellOffset.xy) / VolumetricFog.ViewGridSize.xy; float2 VolumeNDC = (VolumeUV * 2 - 1) * float2(1, -1); SceneDepth = ComputeDepthFromZSlice(max(GridCoordinate.z + CellOffset.z, 0.0f)); float TileDeviceZ = ConvertToDeviceZ(SceneDepth); float4 CenterPosition = mul(float4(VolumeNDC, TileDeviceZ, 1), UnjitteredClipToTranslatedWorld); return CenterPosition.xyz / CenterPosition.w; } float3 ComputeCellTranslatedWorldPosition(uint3 GridCoordinate, float3 CellOffset) { float Unused; return ComputeCellTranslatedWorldPosition(GridCoordinate, CellOffset, Unused); } float3 RaleighScattering() { float3 Wavelengths = float3(650.0f, 510.0f, 475.0f); float ParticleDiameter = 60; float ParticleRefractiveIndex = 1.3f; float3 ScaleDependentPortion = pow(ParticleDiameter, 6) / pow(Wavelengths, 4); float RefractiveIndexPortion = (ParticleRefractiveIndex * ParticleRefractiveIndex - 1) / (ParticleRefractiveIndex * ParticleRefractiveIndex + 2); return (2 * pow(PI, 5) * RefractiveIndexPortion * RefractiveIndexPortion) * ScaleDependentPortion / 3.0f; } float3 ScatteringFunction() { return 1; //return RaleighScattering(); } float4 GlobalAlbedo; float4 GlobalEmissive; float GlobalExtinctionScale; #ifndef THREADGROUP_SIZE #define THREADGROUP_SIZE 1 #endif #ifndef THREADGROUP_SIZE_X #define THREADGROUP_SIZE_X THREADGROUP_SIZE #endif #ifndef THREADGROUP_SIZE_Y #define THREADGROUP_SIZE_Y THREADGROUP_SIZE #endif #ifndef THREADGROUP_SIZE_Z #define THREADGROUP_SIZE_Z THREADGROUP_SIZE #endif #ifdef MaterialSetupCS [numthreads(THREADGROUP_SIZE, THREADGROUP_SIZE, THREADGROUP_SIZE)] void MaterialSetupCS( uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID) { uint3 GridCoordinate = DispatchThreadId; // Center of the voxel float VoxelOffset = .5f; float3 WorldPosition = ComputeCellTranslatedWorldPosition(GridCoordinate, VoxelOffset) - DFHackToFloat(PrimaryView.PreViewTranslation); float GlobalDensityFirst = FogStruct.ExponentialFogParameters3.x * exp2(-FogStruct.ExponentialFogParameters.y * (WorldPosition.z - FogStruct.ExponentialFogParameters3.y)); float GlobalDensitySecond = FogStruct.ExponentialFogParameters2.z * exp2(-FogStruct.ExponentialFogParameters2.y * (WorldPosition.z - FogStruct.ExponentialFogParameters2.w)); float GlobalDensity = GlobalDensityFirst + GlobalDensitySecond; float3 Albedo = GlobalAlbedo.rgb; // Exponential height fog interprets density differently, match its behavior float MatchHeightFogFactor = .5f; GlobalDensity *= MatchHeightFogFactor; float Extinction = max(GlobalDensity * GlobalExtinctionScale, 0); float3 Scattering = Albedo * Extinction; float3 AdditionalEmissive = 0.0f; #if USE_LOCAL_FOG_VOLUMES float CellOffset = .5f; float SceneDepth; const float3 TranslatedWorldPosition = ComputeCellTranslatedWorldPosition(GridCoordinate, CellOffset, SceneDepth); const float3 BackCellTranslatedWorldPosition = ComputeCellTranslatedWorldPosition(GridCoordinate, float3(CellOffset, CellOffset, 1.0f), SceneDepth); const float DistanceToCellBack = GetDistanceToCameraFromViewVector(BackCellTranslatedWorldPosition - PrimaryView.TranslatedWorldCameraOrigin); if(DistanceToCellBack > LFVGlobalStartDistance) { const float3 FrontCellTranslatedWorldPosition = ComputeCellTranslatedWorldPosition(GridCoordinate, float3(CellOffset, CellOffset, 0.0f), SceneDepth); const float DistanceToCellFront = GetDistanceToCameraFromViewVector(FrontCellTranslatedWorldPosition - PrimaryView.TranslatedWorldCameraOrigin); float SoftDensityScale = 1; if (DistanceToCellFront < LFVGlobalStartDistance && LFVGlobalStartDistance < DistanceToCellBack) { // Smoothly transition from the front clip planes of LFV to avoid bad straircase/aliasing visual effect. // Extinction is basically scaled as a fonction of the start distance position inside the froxel along V. SoftDensityScale = (DistanceToCellBack - LFVGlobalStartDistance) / max(0.0001, DistanceToCellBack - DistanceToCellFront); } float4 VertexClipSpacePosition = mul(float4(TranslatedWorldPosition.xyz, 1), PrimaryView.TranslatedWorldToClip); float2 SvPosition = (VertexClipSpacePosition.xy / VertexClipSpacePosition.w * float2(.5f, -.5f) + .5f) * PrimaryView.ViewSizeAndInvSize.xy; uint2 TilePos = clamp(uint2(SvPosition.xy / float(LFVTilePixelSize)), uint2(0, 0), LFVTileDataResolution - 1); uint LFVCount = LFVTileDataTex[uint3(TilePos, 0)]; for (uint LFVIndex = 0; LFVIndex < LFVCount; ++LFVIndex) { uint InstanceIndex = LFVTileDataTex[uint3(TilePos, 1 + LFVIndex)]; // LFV_TODO skip if out of sphere of influence FLocalFogVolumeGPUInstanceData FogInstance = GetLocalFogVolumeGPUInstanceData(InstanceIndex); float3 UnitSpacePos = mul(TranslatedWorldPosition - FogInstance.TranslatedWorlPos, FogInstance.PreTranslatedInvTransform); float DistToCenter = length(UnitSpacePos - float3(0.0f, 0.0f, 0.0f)); float SphereFade = saturate(1.0f - DistToCenter); // Evaluate both radial and height local fog contribution float LFVExtinctionHeight = SphereFade <= 0.0f ? 0.0f : FogInstance.HeightFogExtinction * exp(-FogInstance.HeightFogFalloff * (UnitSpacePos.z - FogInstance.HeightFogOffset)); float LFVExtinctionRadial = SphereFade <= 0.0f ? 0.0f : FogInstance.RadialFogExtinction * pow(SphereFade, 0.82); // 0.82 is a correction factor needed to get a better match. It is unclear why it is needed... // As for the local fog volume analitical evaluation, we want to combine both two optical depth for the "opacity"=1-transmittance to be combined together. // This helps to have soft edges when using height fog. /* In this case, we have to find math so that the combined extinction resulting in a transmittance representing the multiplication of coverage = 1 - transmittance factors. Reminder Transmittance as T Optical Depth as Tau T = exp(-Tau) Tau = -log(T) (*) We want to combine the both mode transmittance Tr (radial) and Th (height) as done in LocalFogVolumeEvaluateAnalyticalIntegral to have T = 1 - (1-Tr)(1-Th) // Where (1-Tr) is virtual coverage of the medium expressed from transmittance for the radial fog. T = 1 - (1 - exp(-Tau_r)) (1 - exp(-Tau_h))) // Where Tr = exp(-Tau_r) -Tau = log(1 - (1 - exp(-Tau_r)) (1 - exp(-Tau_h))) // Using (*) Tau =-log(exp(-Tau_r) - exp((-Tau_r-Tau_h) + exp(-Tau_h))) Optical Depth Tau = Sigma_t * dt so it is linear with respect to dt and extinction Sigma_t. So we can also write Sigma_T = -log(exp(-Sigma_T_r) - exp((-Sigma_T_r-Sigma_T_h) + exp(-Sigma_T_h))) Factoring some values a bit we can get A = exp(-Sigma_T_r) B = exp(-Sigma_T_h) Sigma_T = -log(A - A*B + B) */ // Optical Depth is linear with Extinction so there is no need to account for the step length. float FactorA = exp(-LFVExtinctionHeight); float FactorB = exp(-LFVExtinctionRadial); float LFVExtinction = -log(FactorA - FactorA * FactorB + FactorB); LFVExtinction = max(0.0f, LFVExtinction); // Convert extinction from unit volume 1/m into extinction for volumetric fog as 1/cm LFVExtinction *= (1.0f / METER_TO_CENTIMETER); LFVExtinction = min(LFVMaxDensityIntoVolumetricFog, LFVExtinction) * SoftDensityScale; float3 LFVScattering = LFVExtinction * FogInstance.Albedo; Scattering += LFVScattering; Extinction += LFVExtinction; #if USE_EMISSIVE // Emissive follow the density of the medium AdditionalEmissive += LFVExtinction * FogInstance.Emissive; #endif } } #endif // USE_LOCAL_FOG_VOLUMES if (all((int3)GridCoordinate < VolumetricFog.ResourceGridSizeInt)) { RWVBufferA[GridCoordinate] = float4(Scattering, Extinction); #if USE_EMISSIVE float3 HeightFogEmissive = GlobalEmissive.rgb; #if PROJECT_EXPFOG_MATCHES_VFOG HeightFogEmissive.rgb *= GlobalDensity; // Emissive needs to follow the height fog density for a better match #endif RWVBufferB[GridCoordinate] = float4(HeightFogEmissive.rgb + AdditionalEmissive, 0); #endif } } #endif // Positive g = forward scattering // Zero g = isotropic // Negative g = backward scattering float PhaseFunction(float g, float CosTheta) { return HenyeyGreensteinPhase(g, CosTheta); } struct FWriteToSliceVertexOutput { FScreenVertexOutput Vertex; #if USING_VERTEX_SHADER_LAYER uint LayerIndex : SV_RenderTargetArrayIndex; #else uint LayerIndex : TEXCOORD1; #endif }; /** Z index of the minimum slice in the range. */ int MinZ; float4 ViewSpaceBoundingSphere; float4x4 ViewToVolumeClip; /** Vertex shader that writes to a range of slices of a volume texture. */ void WriteToBoundingSphereVS( float2 InPosition : ATTRIBUTE0, float2 InUV : ATTRIBUTE1, uint LayerIndex : SV_InstanceID, out FWriteToSliceVertexOutput Output ) { float SliceDepth = ComputeDepthFromZSlice(LayerIndex + MinZ); float SliceDepthOffset = abs(SliceDepth - ViewSpaceBoundingSphere.z); if (SliceDepthOffset < ViewSpaceBoundingSphere.w) { // Compute the radius of the circle formed by the intersection of the bounding sphere and the current depth slice float SliceRadius = sqrt(ViewSpaceBoundingSphere.w * ViewSpaceBoundingSphere.w - SliceDepthOffset * SliceDepthOffset); // Place the quad vertex to tightly bound the circle float3 ViewSpaceVertexPosition = float3(ViewSpaceBoundingSphere.xy + (InUV * 2 - 1) * SliceRadius, SliceDepth); Output.Vertex.Position = mul(float4(ViewSpaceVertexPosition, 1), ViewToVolumeClip); float2 ClipRatio = float2(1.0f, 1.0f); // Scale rendered position to account for frustum difference between fog voxelization and rendered scene Output.Vertex.Position.xy *= ClipRatio; } else { // Slice does not intersect bounding sphere, emit degenerate triangle Output.Vertex.Position = 0; } // Debug - draw to entire texture in xy //Output.Vertex.Position = float4(InUV * float2(2, -2) + float2(-1, 1), 0, 1); Output.Vertex.UV = 0; Output.LayerIndex = LayerIndex + MinZ; } float HistoryWeight; float LightScatteringSampleJitterMultiplier; float4 FrameJitterOffsets[16]; uint HistoryMissSuperSampleCount; float PhaseG; float InverseSquaredLightDistanceBiasScale; int VirtualShadowMapId; #ifndef HISTORY_MISS_SUPER_SAMPLE_COUNT #define HISTORY_MISS_SUPER_SAMPLE_COUNT 1 #endif #include "LightFunctionAtlas/LightFunctionAtlasCommon.usf" Texture2D ConservativeDepthTexture; float2 PrevConservativeDepthTextureSize; Texture2D PrevConservativeDepthTexture; uint UseConservativeDepthTexture; #if USE_RAYTRACED_SHADOWS #include "/Engine/Private/RayTracing/RayTracingCommon.ush" RaytracingAccelerationStructure TLAS; float ComputeRayTracedShadowFactor(float3 TranslatedWorldPosition, float3 LightDirection, float TMax) { uint RayFlags = RAY_FLAG_SKIP_CLOSEST_HIT_SHADER | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH; uint RaytracingMask = RAY_TRACING_MASK_SHADOW | RAY_TRACING_MASK_THIN_SHADOW; FRayDesc Ray; Ray.Origin = TranslatedWorldPosition; Ray.Direction = LightDirection; Ray.TMin = 0.0f; Ray.TMax = TMax; FMinimalPayload MinimalPayload = TraceVisibilityRay( TLAS, RayFlags, RaytracingMask, Ray); if (MinimalPayload.IsHit()) { return 0.0f; } return 1.0; } #endif float4 InjectShadowedLocalLightCommon(uint3 GridCoordinate) { float4 OutScattering = 0; // Somehow pixels are being rasterized outside of the viewport on a 970 GTX, perhaps due to use of a geometry shader bypassing the viewport scissor. // This triggers the HistoryMissSuperSampleCount path causing significant overhead for shading off-screen pixels. if (all(int3(GridCoordinate) < VolumetricFog.ResourceGridSizeInt)) { FDeferredLightData LightData = InitDeferredLightFromUniforms(); float VolumetricScatteringIntensity = DeferredLightUniforms.VolumetricScatteringIntensity; float3 L = 0; float3 ToLight = 0; uint NumSuperSamples = 1; #if USE_TEMPORAL_REPROJECTION float3 HistoryUV = ComputeHistoryVolumeUVFromTranslatedPos(ComputeCellTranslatedWorldPosition(GridCoordinate, .5f), UnjitteredPrevTranslatedWorldToClip); float HistoryAlpha = HistoryWeight; FLATTEN if (any(HistoryUV < 0) || any(HistoryUV > 1)) { HistoryAlpha = 0; } NumSuperSamples = HistoryAlpha < .001f ? HistoryMissSuperSampleCount : 1; #endif const bool bSoftFadeEnabled = VolumetricFog.LightSoftFading > 0; for (uint SampleIndex = 0; SampleIndex < NumSuperSamples; SampleIndex++) { float3 CellOffset = FrameJitterOffsets[SampleIndex].xyz; //float CellOffset = .5f; float SceneDepth; float3 TranslatedWorldPosition = ComputeCellTranslatedWorldPosition(GridCoordinate, CellOffset, SceneDepth); float3 CameraVector = GetCameraVectorFromTranslatedWorldPosition(TranslatedWorldPosition); float CellRadius = length(TranslatedWorldPosition - ComputeCellTranslatedWorldPosition(GridCoordinate + uint3(1, 1, 1), CellOffset)); float Cell2DRadius = length(TranslatedWorldPosition - ComputeCellTranslatedWorldPosition(GridCoordinate + uint3(1, 1, 0), CellOffset)); float LightVolumetricSoftFadeDistance = VolumetricFog.LightSoftFading * Cell2DRadius; // Bias the inverse squared light falloff based on voxel size to prevent aliasing near the light source float DistanceBias = max(CellRadius * InverseSquaredLightDistanceBiasScale, 1); float3 LightColor = DeferredLightUniforms.Color; float LightMask = GetLocalLightAttenuation(TranslatedWorldPosition, LightData, ToLight, L); float Lighting; if( LightData.bRectLight ) { FRect Rect = GetRect(ToLight, LightData); float SoftFade = 1.0f; #if USE_LIGHT_SOFT_FADING if (bSoftFadeEnabled) { SoftFade = GetRectLightVolumetricSoftFading(LightData, Rect, LightVolumetricSoftFadeDistance, ToLight); } #endif // USE_LIGHT_SOFT_FADING Lighting = SoftFade * IntegrateLight(Rect); #if USE_RECT_LIGHT_TEXTURE if (IsRectVisible(Rect)) { const FRectTexture SourceTexture = ConvertToRectTexture(LightData); const float3 FalloffColor = SampleSourceTexture(Lighting, Rect, SourceTexture); LightColor *= FalloffColor; } #endif // USE_RECT_LIGHT_TEXTURE } else { FCapsuleLight Capsule = GetCapsule(ToLight, LightData); Capsule.DistBiasSqr = Pow2(DistanceBias); float SoftFade = 1.0f; #if USE_LIGHT_SOFT_FADING if (LightData.bSpotLight && bSoftFadeEnabled) { SoftFade = GetSpotLightVolumetricSoftFading(LightData, LightVolumetricSoftFadeDistance, ToLight); } #endif // USE_LIGHT_SOFT_FADING Lighting = SoftFade * IntegrateLight(Capsule, LightData.bInverseSquared); } float CombinedAttenuation = Lighting * LightMask; float ShadowFactor = 1.0f; bool IsPointLight = LightData.bRadialLight && !LightData.bSpotLight; #if ENABLE_SHADOW_COMPUTATION if (CombinedAttenuation > 0) { #if USE_RAYTRACED_SHADOWS ShadowFactor = ComputeRayTracedShadowFactor(TranslatedWorldPosition, normalize(ToLight), length(ToLight)); #else bool bUnused = false; ShadowFactor = ComputeVolumeShadowing(TranslatedWorldPosition, IsPointLight, LightData.bSpotLight, bUnused); #if VIRTUAL_SHADOW_MAP FVirtualShadowMapSampleResult VirtualShadowMapSample = SampleVirtualShadowMapTranslatedWorld(VirtualShadowMapId, TranslatedWorldPosition); ShadowFactor *= VirtualShadowMapSample.ShadowFactor; #endif // VIRTUAL_SHADOW_MAP #endif // USE_RAYTRACED_SHADOWS } #endif FLightFunctionColor LightFunctionColor = 1.0f; #if USE_LIGHT_FUNCTION_ATLAS LightFunctionColor = GetLocalLightFunctionCommon(TranslatedWorldPosition, LightData.LightFunctionAtlasLightIndex); #endif OutScattering.rgb += LightColor * LightFunctionColor * (PhaseFunction(PhaseG, dot(L, -CameraVector)) * CombinedAttenuation * ShadowFactor * VolumetricScatteringIntensity); // To debug culling //OutScattering.rgb += DeferredLightUniforms.Color * .0000001f; } // We pre-expose the buffer containing shadowed local lights luminance contribution. This helps maintaining details and color at high exposure for very bright scenes. OutScattering.rgb *= View.PreExposure / (float)NumSuperSamples; } return OutScattering; } #if USE_RAYTRACED_SHADOWS RWTexture3D OutVolumeTexture; uint3 OutputOffset; int FirstSlice; int3 GetVoxelIndex(int ThreadIndex, int3 Resolution) { int SliceSize = Resolution.x * Resolution.y; int SliceIndex = ThreadIndex / SliceSize; int SliceRemainder = ThreadIndex - SliceIndex * SliceSize; return int3( SliceRemainder % Resolution.x, SliceRemainder / Resolution.x, SliceIndex ); } RAY_TRACING_ENTRY_RAYGEN(InjectShadowedLocalLightRGS) { int3 Position = GetVoxelIndex(DispatchRaysIndex().x, VolumetricFog.ResourceGridSizeInt); Position.z += FirstSlice; float4 OutScattering = InjectShadowedLocalLightCommon(Position); float4 DestValue = OutVolumeTexture[Position]; OutVolumeTexture[Position] = DestValue + OutScattering; } RWTexture3D OutShadowVolumeTexture; RAY_TRACING_ENTRY_RAYGEN(InjectShadowedDirectionalLightRGS) { int3 Position = GetVoxelIndex(DispatchRaysIndex().x, VolumetricFog.ResourceGridSizeInt); const int SampleIndex = 0; uint3 Rand32Bits = Rand4DPCG32(int4(Position.xyz, View.StateFrameIndexMod8 + 8 * SampleIndex)).xyz; float3 Rand3D = (float3(Rand32Bits) / float(uint(0xffffffff))) * 2.0f - 1.0f; float3 CellOffset = FrameJitterOffsets[SampleIndex].xyz + LightScatteringSampleJitterMultiplier * Rand3D; const float3 TranslatedWorldPosition = ComputeCellTranslatedWorldPosition(Position, CellOffset); OutShadowVolumeTexture[Position] = ComputeRayTracedShadowFactor(TranslatedWorldPosition, ForwardLightStruct.DirectionalLightDirection, 1.0e27); } #endif // USE_RAYTRACED_SHADOWS void InjectShadowedLocalLightPS( FWriteToSliceGeometryOutput Input, out float4 OutScattering : SV_Target0 ) { float4 SvPosition = Input.Vertex.Position; OutScattering = 0; if (UseConservativeDepthTexture > 0) { const float SceneDepth = ConservativeDepthTexture.Load(uint3(SvPosition.xy, 0)); if (SceneDepth > SvPosition.z) { clip(-1); // If the froxel is behind conservative front depth, skip the light injection. return; } } OutScattering = InjectShadowedLocalLightCommon(uint3(SvPosition.xy, Input.LayerIndex)); } Texture3D VBufferA; Texture3D VBufferB; uint UseEmissive; Texture3D LightScatteringHistory; SamplerState LightScatteringHistorySampler; float2 LightScatteringHistoryPreExposureAndInv; Texture3D LocalShadowedLightScattering; RWTexture3D RWLightScattering; #if DISTANCE_FIELD_SKY_OCCLUSION float HemisphereConeTraceAgainstGlobalDistanceFieldClipmap( uniform uint ClipmapIndex, float3 TranslatedWorldShadingPosition, float3 ConeDirection, float TanConeHalfAngle) { float MinStepSize = GlobalVolumeTranslatedCenterAndExtent[ClipmapIndex].w * 2 / 100.0f; float InvAOGlobalMaxOcclusionDistance = 1.0f / AOGlobalMaxOcclusionDistance; float MinVisibility = 1; float WorldStepOffset = 2; LOOP for (uint StepIndex = 0; StepIndex < NUM_CONE_STEPS && WorldStepOffset < AOGlobalMaxOcclusionDistance; StepIndex++) { float3 TranslatedWorldSamplePosition = TranslatedWorldShadingPosition + ConeDirection * WorldStepOffset; float DistanceToOccluder = SampleGlobalDistanceField(TranslatedWorldSamplePosition, AOGlobalMaxOcclusionDistance, ClipmapIndex); float SphereRadius = WorldStepOffset * TanConeHalfAngle; float InvSphereRadius = rcpFast(SphereRadius); // Derive visibility from 1d intersection float Visibility = saturate(DistanceToOccluder * InvSphereRadius); float OccluderDistanceFraction = (WorldStepOffset + DistanceToOccluder) * InvAOGlobalMaxOcclusionDistance; // Fade out occlusion based on distance to occluder to avoid a discontinuity at the max AO distance Visibility = max(Visibility, saturate(OccluderDistanceFraction * OccluderDistanceFraction * .6f)); MinVisibility = min(MinVisibility, Visibility); WorldStepOffset += max(DistanceToOccluder, MinStepSize); } return MinVisibility; } float HemisphereConeTraceAgainstGlobalDistanceField(float3 TranslatedWorldShadingPosition, float3 ConeDirection, float TanConeHalfAngle) { int MinClipmapIndex = -1; for (uint ClipmapIndex = 0; ClipmapIndex < NumGlobalSDFClipmaps; ++ClipmapIndex) { float DistanceFromClipmap = ComputeDistanceFromBoxToPointInside(GlobalVolumeTranslatedCenterAndExtent[ClipmapIndex].xyz, GlobalVolumeTranslatedCenterAndExtent[ClipmapIndex].www, TranslatedWorldShadingPosition); if (DistanceFromClipmap > AOGlobalMaxOcclusionDistance) { MinClipmapIndex = ClipmapIndex; break; } } float MinVisibility = 1.0f; if (MinClipmapIndex >= 0) { MinVisibility = HemisphereConeTraceAgainstGlobalDistanceFieldClipmap(MinClipmapIndex, TranslatedWorldShadingPosition, ConeDirection, TanConeHalfAngle); } return MinVisibility; } #endif float SkyLightUseStaticShadowing; float ComputeSkyVisibility(float3 TranslatedWorldPosition, float3 BrickTextureUVs) { float Visibility = 1; #if DISTANCE_FIELD_SKY_OCCLUSION // Trace one 45 degree cone straight up for sky occlusion float TanConeHalfAngle = tan((float)PI / 4); Visibility = HemisphereConeTraceAgainstGlobalDistanceField(TranslatedWorldPosition, float3(0, 0, 1), TanConeHalfAngle); #endif #if ALLOW_STATIC_LIGHTING if (SkyLightUseStaticShadowing > 0) { float3 SkyBentNormal = GetVolumetricLightmapSkyBentNormal(BrickTextureUVs); Visibility = length(SkyBentNormal); } #endif return Visibility; } float4x4 DirectionalLightFunctionTranslatedWorldToShadow; Texture2D DirectionalLightLightFunctionTexture; SamplerState DirectionalLightLightFunctionSampler; uint DirectionalApplyLightFunctionFromAtlas; uint DirectionalLightFunctionAtlasLightIndex; FLightFunctionColor GetDirectionalLightFunction(float3 TranslatedWorldPosition) // directional light { #if USE_LIGHT_FUNCTION_ATLAS // Used when injecting lights from the light grid if (DirectionalApplyLightFunctionFromAtlas > 0) { return GetLocalLightFunctionCommon(TranslatedWorldPosition, DirectionalLightFunctionAtlasLightIndex); } #endif float4 HomogeneousShadowPosition = mul(float4(TranslatedWorldPosition, 1), DirectionalLightFunctionTranslatedWorldToShadow); float2 LightFunctionUV = HomogeneousShadowPosition.xy * .5f + .5f; LightFunctionUV.y = 1 - LightFunctionUV.y; return Texture2DSampleLevel(DirectionalLightLightFunctionTexture, DirectionalLightLightFunctionSampler, LightFunctionUV, 0).x; } uint SampleSkyLightDiffuseEnvMap; float2 UseHeightFogColors; // x=override directional light using height fog inscattering color, y=override sky light using heigh fog inscattering cubemap float UseDirectionalLightShadowing; float StaticLightingScatteringIntensity; #if SHADING_PATH_MOBILE uint MobileHasDirectionalLight; float3 MobileDirectionalLightColor; float3 MobileDirectionalLightDirection; #endif Texture3D RaytracedShadowsVolume; Texture3D MegaLightsVolume; // Modify UV used to sample LightScatteringHistory to avoid cells that were occluded last frame float2 FixupHistoryUV(float2 UV, float2 Size, float PrevCellNDCPositionZ, out bool bHasValidHistory) { float2 FullResUV = UV * Size; uint2 ScreenCoord = floor(FullResUV - 0.5f); float2 BilinearWeights = frac(FullResUV - 0.5f); float2 GatherUV = (ScreenCoord + 1.0f) / Size; float2 FullResOffset = FullResUV - ScreenCoord; const float2 PrevConservativeDepthGatherUV = GatherUV / (PrimaryView.VolumetricFogViewGridUVToPrevViewRectUV.xy * PrimaryView.VolumetricFogPrevViewGridRectUVToResourceUV.xy); const float4 PrevSceneDepths = PrevConservativeDepthTexture.Gather(LightScatteringHistorySampler, PrevConservativeDepthGatherUV); const bool4 ValidSamples = PrevSceneDepths < PrevCellNDCPositionZ; bHasValidHistory = true; if (all(ValidSamples)) { return UV; } // Gather pattern: // wz // xy if (all(ValidSamples.wz)) { return (ScreenCoord + float2(FullResOffset.x, 0.5f)) / Size; } if (all(ValidSamples.xy)) { return (ScreenCoord + float2(FullResOffset.x, 1.5f)) / Size; } if (all(ValidSamples.wx)) { return (ScreenCoord + float2(0.5f, FullResOffset.y)) / Size; } if (all(ValidSamples.zy)) { return (ScreenCoord + float2(1.5f, FullResOffset.y)) / Size; } if (ValidSamples.x) { return (ScreenCoord + float2(0.5f, 1.5f)) / Size; } if (ValidSamples.y) { return (ScreenCoord + float2(1.5f, 1.5f)) / Size; } if (ValidSamples.w) { return (ScreenCoord + float2(0.5f, 0.5f)) / Size; } if (ValidSamples.z) { return (ScreenCoord + float2(1.5f, 0.5f)) / Size; } bHasValidHistory = false; return UV; } #ifdef LightScatteringCS [numthreads(THREADGROUP_SIZE_X, THREADGROUP_SIZE_Y, THREADGROUP_SIZE_Z)] void LightScatteringCS( uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID) { uint3 GridCoordinate = DispatchThreadId; float3 LightScattering = 0; uint NumSuperSamples = 1; #if USE_TEMPORAL_REPROJECTION float3 HistoryUV = ComputeHistoryVolumeUVFromTranslatedPos(ComputeCellTranslatedWorldPosition(GridCoordinate, .5f), UnjitteredPrevTranslatedWorldToClip); bool bHasValidHistory = true; #endif // If the froxel is behind front depth, do not evaluate any lighting. if (UseConservativeDepthTexture > 0) { // expand by half voxel towards the camera to support bilinear filtering const float3 FarDepthOffset = float3(0.5f, 0.5f, -0.5f); float3 CellTranslatedWorldPosition = ComputeCellTranslatedWorldPosition(GridCoordinate, FarDepthOffset); float4 CellNDCPosition = mul(float4(CellTranslatedWorldPosition, 1), PrimaryView.TranslatedWorldToClip); CellNDCPosition.xyz /= CellNDCPosition.w; float2 UVs = CellNDCPosition.xy * float2(0.5f, -0.5f) + 0.5f; const float SceneDepth = ConservativeDepthTexture.Load(uint3(GridCoordinate.xy, 0)); if (SceneDepth > CellNDCPosition.z) { RWLightScattering[GridCoordinate] = float4(0.0f, 0.0f, 0.0f, 0.0f); return; } #if USE_TEMPORAL_REPROJECTION // TODO: investigate whether using CellTranslatedWorldPosition (which is biased towards camera) is correct here // it might be more accurate to transform unbiased translated world position to previous frame clip space and apply bias towards camera in that space? float4 PrevCellNDCPosition = mul(float4(CellTranslatedWorldPosition, 1), UnjitteredPrevTranslatedWorldToClip); PrevCellNDCPosition.xyz /= PrevCellNDCPosition.w; HistoryUV.xy = FixupHistoryUV(HistoryUV.xy, View.VolumetricFogPrevResourceGridSize.xy, PrevCellNDCPosition.z, bHasValidHistory); #endif } #if USE_TEMPORAL_REPROJECTION float HistoryAlpha = HistoryWeight; // We need to test with View.VolumetricFogPrevUVMaxForTemporalBlend for HistoryUV because that is what we clamp with in ComputeHistoryVolumeUVFromTranslatedPos. FLATTEN if (any(HistoryUV < 0) || any(HistoryUV >= float3(View.VolumetricFogPrevUVMaxForTemporalBlend, 1.0f)) || !bHasValidHistory) { HistoryAlpha = 0; } // Supersample if the history was outside the camera frustum // The compute shader is dispatched with extra threads, make sure those don't supersample NumSuperSamples = HistoryAlpha < .001f && all(int3(GridCoordinate) < VolumetricFog.ViewGridSizeInt) ? HISTORY_MISS_SUPER_SAMPLE_COUNT : 1; #endif for (uint SampleIndex = 0; SampleIndex < NumSuperSamples; SampleIndex++) { uint3 Rand32Bits = Rand4DPCG32(int4(GridCoordinate.xyz, View.StateFrameIndexMod8 + 8 * SampleIndex)).xyz; float3 Rand3D = (float3(Rand32Bits) / float(uint(0xffffffff))) * 2.0f - 1.0f; float3 CellOffset = FrameJitterOffsets[SampleIndex].xyz + LightScatteringSampleJitterMultiplier * Rand3D; //CellOffset = 0.5f; float SceneDepth; float3 TranslatedWorldPosition = ComputeCellTranslatedWorldPosition(GridCoordinate, CellOffset, SceneDepth); float3 WorldPosition = TranslatedWorldPosition - DFHackToFloat(PrimaryView.PreViewTranslation); // LWC_TODO float CameraVectorLength = GetDistanceToCameraFromViewVector(TranslatedWorldPosition - PrimaryView.TranslatedWorldCameraOrigin); float3 CameraVector = GetCameraVectorFromTranslatedWorldPosition(TranslatedWorldPosition); #if SHADING_PATH_MOBILE const bool bHasDirectionalLight = MobileHasDirectionalLight > 0; const float3 InDirectionalLightColor = MobileDirectionalLightColor; // DirectionalLightVolumetricScatteringIntensity is baked in MobileDirectionalLightColor const float3 InDirectionalLightDirection= MobileDirectionalLightDirection; const bool bEvaluateDirectionalLight = true; #else const bool bHasDirectionalLight = ForwardLightStruct.HasDirectionalLight; const float3 InDirectionalLightColor = ForwardLightStruct.DirectionalLightColor * ForwardLightStruct.DirectionalLightVolumetricScatteringIntensity; const float3 InDirectionalLightDirection= ForwardLightStruct.DirectionalLightDirection; const bool bEvaluateDirectionalLight = !USE_MEGA_LIGHTS || !ForwardLightStruct.DirectionalLightHandledByMegaLights; #endif BRANCH if (bHasDirectionalLight && bEvaluateDirectionalLight) { float ShadowFactor = 1; if (UseDirectionalLightShadowing > 0) { ShadowFactor *= ComputeDirectionalLightStaticShadowing(TranslatedWorldPosition); bool bUnused = false; ShadowFactor *= ComputeDirectionalLightDynamicShadowing(TranslatedWorldPosition, SceneDepth, bUnused); #if VIRTUAL_SHADOW_MAP FVirtualShadowMapSampleResult VirtualShadowMapSample = SampleVirtualShadowMapTranslatedWorld(ForwardLightStruct.DirectionalLightVSM, TranslatedWorldPosition); ShadowFactor *= VirtualShadowMapSample.ShadowFactor; #endif #if USE_RAYTRACED_SHADOWS_VOLUME ShadowFactor *= RaytracedShadowsVolume[GridCoordinate]; #endif #if !SHADING_PATH_MOBILE if (CloudShadowmapStrength > 0.0f) { float OutOpticalDepth = 0.0f; ShadowFactor *= lerp(1.0f, GetCloudVolumetricShadow(TranslatedWorldPosition, CloudShadowmapTranslatedWorldToLightClipMatrix, CloudShadowmapFarDepthKm, CloudShadowmapTexture, CloudShadowmapSampler, OutOpticalDepth), CloudShadowmapStrength); } #endif } FLightFunctionColor LightFunctionColor = GetDirectionalLightFunction(TranslatedWorldPosition); float3 DirectionalLightColor = InDirectionalLightColor; if (UseHeightFogColors.x > 0) { // Attempt to maintain intensity ratio between sky and sun DirectionalLightColor = VolumetricFog.HeightFogDirectionalLightInscatteringColor * Luminance(InDirectionalLightColor); } LightScattering += DirectionalLightColor * LightFunctionColor * ShadowFactor * PhaseFunction(PhaseG, dot(InDirectionalLightDirection, -CameraVector)); } FTwoBandSHVector RotatedHGZonalHarmonic; RotatedHGZonalHarmonic.V = float4(1.0f, CameraVector.y, CameraVector.z, CameraVector.x) * float4(1.0f, PhaseG, PhaseG, PhaseG); // Note: I believe PhaseG here hsould be negated to match the sky & environment. float3 BrickTextureUVs = 0; #if ALLOW_STATIC_LIGHTING if (View.SkyLightVolumetricScatteringIntensity > 0 || StaticLightingScatteringIntensity > 0) { BrickTextureUVs = ComputeVolumetricLightmapBrickTextureUVs(WorldPosition); } #endif #if LUMEN_GI // Lumen Dynamic GI + shadowed Skylight FTwoBandSHVectorRGB TranslucencyGISH = GetTranslucencyGIVolumeLighting(DFPromote(WorldPosition), PrimaryView.WorldToClip, false); // LUMEN_LWC_TODO LightScattering += max(DotSH(TranslucencyGISH, RotatedHGZonalHarmonic), 0); #else // Skylight if (View.SkyLightVolumetricScatteringIntensity > 0) { float3 SkyLighting; if (UseHeightFogColors.y > 0) { float3 HeightFogInscatteringColor = ComputeInscatteringColor(CameraVector, CameraVectorLength); float ScalarFactor = SHAmbientFunction(); FTwoBandSHVectorRGB SkyIrradianceSH; SkyIrradianceSH.R.V = float4(ScalarFactor * HeightFogInscatteringColor.r, 0, 0, 0); SkyIrradianceSH.G.V = float4(ScalarFactor * HeightFogInscatteringColor.g, 0, 0, 0); SkyIrradianceSH.B.V = float4(ScalarFactor * HeightFogInscatteringColor.b, 0, 0, 0); SkyLighting = max(DotSH(SkyIrradianceSH, RotatedHGZonalHarmonic), 0); } else { // NOTE it should be CameraVector * PhaseG. But we keep it like that for backward compatibility for now. (visible when r.Lumen.TranslucencyVolume.Enable 0) SkyLighting = SampleSkyLightDiffuseEnvMap > 0 ? View.SkyLightColor.rgb * GetSkySHDiffuseSimple(CameraVector * -PhaseG) : 0.0f.xxx; } float SkyVisibility = ComputeSkyVisibility(TranslatedWorldPosition, BrickTextureUVs); LightScattering += (SkyVisibility * View.SkyLightVolumetricScatteringIntensity) * SkyLighting; } #endif #if ALLOW_STATIC_LIGHTING // Indirect lighting of Stationary lights and Direct + Indirect lighting of Static lights if (StaticLightingScatteringIntensity > 0) { FTwoBandSHVectorRGB IrradianceSH = GetVolumetricLightmapSH2(BrickTextureUVs); LightScattering += (StaticLightingScatteringIntensity / PI) * max(DotSH(IrradianceSH, RotatedHGZonalHarmonic), 0); } #endif uint GridIndex = ComputeLightGridCellIndex(GridCoordinate.xy * VolumetricFog.FogGridToPixelXY, SceneDepth, 0); const FCulledLightsGridHeader CulledLightsGridHeader = GetCulledLightsGridHeader(GridIndex); float CellRadius = length(TranslatedWorldPosition - ComputeCellTranslatedWorldPosition(GridCoordinate + uint3(1, 1, 1), CellOffset)); float Cell2DRadius = length(TranslatedWorldPosition - ComputeCellTranslatedWorldPosition(GridCoordinate + uint3(1, 1, 0), CellOffset)); float LightVolumetricSoftFadeDistance = VolumetricFog.LightSoftFading * Cell2DRadius; // Bias the inverse squared light falloff based on voxel size to prevent aliasing near the light source float DistanceBiasSqr = max(CellRadius * InverseSquaredLightDistanceBiasScale, 1); DistanceBiasSqr *= DistanceBiasSqr; #if USE_MEGA_LIGHTS // when using Mega Lights Volume only need to apply "non-mega" lights here const uint NumLights = CulledLightsGridHeader.NumLights - CulledLightsGridHeader.NumMegaLights; #else const uint NumLights = CulledLightsGridHeader.NumLights; #endif // Forward lighting of unshadowed point and spot lights LOOP for (uint GridLightListIndex = 0; GridLightListIndex < NumLights; GridLightListIndex++) { const FLocalLightData LocalLight = GetLocalLightDataFromGrid(CulledLightsGridHeader.DataStartIndex + GridLightListIndex, 0); #if USE_MEGA_LIGHTS checkSlow(!UnpackIsHandledByMegaLights(LocalLight)); #endif const float VolumetricScatteringIntensity = UnpackVolumetricScatteringIntensity(LocalLight); if (VolumetricScatteringIntensity > 0) { const bool bSoftFadeEnabled = VolumetricFog.LightSoftFading > 0; const FDeferredLightData LightData = ConvertToDeferredLight(LocalLight); float3 L = 0; float3 ToLight = 0; float LightMask = GetLocalLightAttenuation(TranslatedWorldPosition, LightData, ToLight, L); float Lighting; if( LightData.bRectLight ) { FRect Rect = GetRect( ToLight, LightData ); float SofFade = 1.0f; #if USE_LIGHT_SOFT_FADING if (bSoftFadeEnabled) { SofFade = GetRectLightVolumetricSoftFading(LightData, Rect, LightVolumetricSoftFadeDistance, ToLight); } #endif // USE_LIGHT_SOFT_FADING Lighting = SofFade * IntegrateLight(Rect); } else { FCapsuleLight Capsule = GetCapsule(ToLight, LightData); Capsule.DistBiasSqr = DistanceBiasSqr; float SofFade = 1.0f; #if USE_LIGHT_SOFT_FADING if (LightData.bSpotLight && bSoftFadeEnabled) { SofFade = GetSpotLightVolumetricSoftFading(LightData, LightVolumetricSoftFadeDistance, ToLight); } #endif // USE_LIGHT_SOFT_FADING Lighting = SofFade * IntegrateLight(Capsule, LightData.bInverseSquared); } float CombinedAttenuation = Lighting * LightMask; FLightFunctionColor LightFunctionColor = 1.0f; #if USE_LIGHT_FUNCTION_ATLAS LightFunctionColor = GetLocalLightFunctionCommon(TranslatedWorldPosition, LightData.LightFunctionAtlasLightIndex); #endif LightScattering += LightData.Color * LightFunctionColor * (PhaseFunction(PhaseG, dot(L, -CameraVector)) * CombinedAttenuation * VolumetricScatteringIntensity); // To debug culling //LightScattering += UnpackLightColor(LocalLight) * .0000001f; } } } LightScattering /= (float)NumSuperSamples; // Shadowed point and spot lights were computed earlier. // Note: this texture was pre-exposed from InjectShadowedLocalLightPS, se we revert pre-exposure. LightScattering += View.OneOverPreExposure * LocalShadowedLightScattering[GridCoordinate].xyz; #if USE_MEGA_LIGHTS { // Inject MegaLights which were computed earlier. This texture was already pre-exposed, so need to rever it here. LightScattering += MegaLightsVolume[GridCoordinate] * View.OneOverPreExposure; } #endif float4 MaterialScatteringAndExtinction = VBufferA[GridCoordinate]; float Extinction = MaterialScatteringAndExtinction.w; float3 MaterialEmissive = 0.0f; BRANCH if (UseEmissive > 0) { MaterialEmissive = VBufferB[GridCoordinate].xyz; } float4 PreExposedScatteringAndExtinction = float4(View.PreExposure * (LightScattering * MaterialScatteringAndExtinction.xyz + MaterialEmissive), Extinction); #if USE_TEMPORAL_REPROJECTION BRANCH if (HistoryAlpha > 0) { float4 PreExposedHistoryScatteringAndExtinction = Texture3DSampleLevel(LightScatteringHistory, LightScatteringHistorySampler, min(HistoryUV, float3(View.VolumetricFogPrevUVMax, 1.0f)), 0); PreExposedHistoryScatteringAndExtinction.rgb *= LightScatteringHistoryPreExposureAndInv.y * View.PreExposure; // Bring previous frame pre exposed history luminance as current frame pre exposed luminance. Leave extinction untouched! PreExposedScatteringAndExtinction = lerp(PreExposedScatteringAndExtinction, PreExposedHistoryScatteringAndExtinction, HistoryAlpha); } #endif // Visualize history rejection for debugging purposes #if 0 && USE_TEMPORAL_REPROJECTION if (HistoryAlpha < 0.001f) { PreExposedScatteringAndExtinction = float4(View.PreExposure * float3(1,0,0), Extinction); } #endif if (all(int3(GridCoordinate) < VolumetricFog.ResourceGridSizeInt)) { PreExposedScatteringAndExtinction = MakePositiveFinite(PreExposedScatteringAndExtinction); RWLightScattering[GridCoordinate] = PreExposedScatteringAndExtinction; } } #endif Texture3D LightScattering; RWTexture3D RWIntegratedLightScattering; float VolumetricFogNearFadeInDistanceInv; #ifdef FinalIntegrationCS [numthreads(THREADGROUP_SIZE, THREADGROUP_SIZE, 1)] void FinalIntegrationCS( uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID) { uint3 GridCoordinate = DispatchThreadId; float3 AccumulatedLighting = 0; float AccumulatedTransmittance = 1.0f; float3 PreviousSliceTranslatedWorldPosition = ComputeCellTranslatedWorldPosition(uint3(GridCoordinate.xy, 0), float3(0.5f, 0.5f, 0.0f)); float AccumulatedDepth = 0.0; for (int LayerIndex = 0; LayerIndex < VolumetricFog.ViewGridSizeInt.z; LayerIndex++) { uint3 LayerCoordinate = uint3(GridCoordinate.xy, LayerIndex); float4 PreExposedScatteringAndExtinction = LightScattering[LayerCoordinate]; float3 LayerTranslatedWorldPosition = ComputeCellTranslatedWorldPosition(LayerCoordinate, .5f); float StepLength = length(LayerTranslatedWorldPosition - PreviousSliceTranslatedWorldPosition); PreviousSliceTranslatedWorldPosition = LayerTranslatedWorldPosition; float Transmittance = exp(-PreExposedScatteringAndExtinction.w * StepLength); AccumulatedDepth += StepLength; // Fade in as a function of depth float FadeInLerpValue = saturate(AccumulatedDepth * VolumetricFogNearFadeInDistanceInv); // See "Physically Based and Unified Volumetric Rendering in Frostbite" #define ENERGY_CONSERVING_INTEGRATION 1 #if ENERGY_CONSERVING_INTEGRATION float3 ScatteringIntegratedOverSlice = FadeInLerpValue * (PreExposedScatteringAndExtinction.rgb - PreExposedScatteringAndExtinction.rgb * Transmittance) / max(PreExposedScatteringAndExtinction.w, .00001f); AccumulatedLighting += ScatteringIntegratedOverSlice * AccumulatedTransmittance; #else AccumulatedLighting += FadeInLerpValue * PreExposedScatteringAndExtinction.rgb * AccumulatedTransmittance * StepLength; #endif AccumulatedTransmittance *= lerp(1.0f, Transmittance, FadeInLerpValue); RWIntegratedLightScattering[LayerCoordinate] = float4(AccumulatedLighting, AccumulatedTransmittance); } } #endif