Files
UnrealEngine/Engine/Shaders/Private/LocalFogVolumes/LocalFogVolumeCommon.ush
2025-05-18 13:04:45 +08:00

435 lines
16 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
#include "../Common.ush"
#include "../SHCommon.ush"
#include "../ParticipatingMediaCommon.ush"
#include "../ReflectionEnvironmentShared.ush"
#include "../FastMath.ush"
///////////////////////////////////////////////////////////////////////////////
// Local Fog Volume Data
struct FLocalFogVolumeGPUInstanceData
{
float3x3 PreTranslatedInvTransform;
float3 TranslatedWorlPos;
float UniformScale;
float UniformScaleInv; // Derived from UniformScale
float RadialFogExtinction;
float HeightFogExtinction;
float HeightFogFalloff;
float HeightFogOffset;
float3 Emissive;
float3 Albedo;
float PhaseG;
};
// Structured buffers cannot be sampled from the vertex shader on some mobile platforms.
// That is why simple Buffers are used for LocalFogVolumeInstances, because this tech needs to run on mobile platforms.
#if defined(LFVStruct)
#define LFVInsts LFVStruct.LFV.LocalFogVolumeInstances
#define LFVTileDataTex LFVStruct.LFV.LocalFogVolumeTileDataTexture
#define LFVTileDataResolution LFVStruct.LFV.LocalFogVolumeTileDataTextureResolution
#define LFVInstcount LFVStruct.LFV.LocalFogVolumeInstanceCount
#define LFVTilePixelSize LFVStruct.LFV.LocalFogVolumeTilePixelSize
#define LFVMaxDensityIntoVolumetricFog LFVStruct.LFV.LocalFogVolumeMaxDensityIntoVolumetricFog
#define LFVRenderInVolumetricFog LFVStruct.LFV.ShouldRenderLocalFogVolumeInVolumetricFog
#define LFVGlobalStartDistance LFVStruct.LFV.GlobalStartDistance
#define LFVHalfResTextureSizeAndInvSize LFVStruct.LFV.HalfResTextureSizeAndInvSize
#define LFVDirectionalLightColor LFVStruct.LFV.DirectionalLightColor
#define LFVDirectionalLightDirection LFVStruct.LFV.DirectionalLightDirection
#else
uint2 LFV_LocalFogVolumeTileDataTextureResolution;
uint LFV_LocalFogVolumeInstanceCount;
uint LFV_LocalFogVolumeTilePixelSize;
float LFV_LocalFogVolumeMaxDensityIntoVolumetricFog;
uint LFV_ShouldRenderLocalFogVolumeInVolumetricFog;
float LFV_GlobalStartDistance;
float4 LFV_HalfResTextureSizeAndInvSize;
float3 LFV_DirectionalLightColor;
float3 LFV_DirectionalLightDirection;
Buffer<float4> LFV_LocalFogVolumeInstances;
Texture2DArray<uint> LFV_LocalFogVolumeTileDataTexture;
#define LFVInsts LFV_LocalFogVolumeInstances
#define LFVTileDataTex LFV_LocalFogVolumeTileDataTexture
#define LFVTileDataResolution LFV_LocalFogVolumeTileDataTextureResolution
#define LFVInstcount LFV_LocalFogVolumeInstanceCount
#define LFVTilePixelSize LFV_LocalFogVolumeTilePixelSize
#define LFVMaxDensityIntoVolumetricFog LFV_LocalFogVolumeMaxDensityIntoVolumetricFog
#define LFVRenderInVolumetricFog LFV_ShouldRenderLocalFogVolumeInVolumetricFog
#define LFVGlobalStartDistance LFV_GlobalStartDistance
#define LFVHalfResTextureSizeAndInvSize LFV_HalfResTextureSizeAndInvSize
#define LFVDirectionalLightColor LFV_DirectionalLightColor
#define LFVDirectionalLightDirection LFV_DirectionalLightDirection
#endif
FLocalFogVolumeGPUInstanceData GetLocalFogVolumeGPUInstanceData(uint Index)
{
FLocalFogVolumeGPUInstanceData Data;
uint Offset = Index * 3;
float4 Data0 = LFVInsts[Offset + 0];
float4 Data1 = LFVInsts[Offset + 1];
float4 Data2 = LFVInsts[Offset + 2];
// See GetLocalFogVolumeViewSortingData for the packing mathod.
// Data0
Data.TranslatedWorlPos = Data0.xyz;
Data.UniformScale = Data0.w;
Data.UniformScaleInv = 1.0f / Data.UniformScale;
// Data1
float3 XVec;
float3 YVec;
XVec.xy = UnpackFloat2FromUInt(asuint(Data1.x));
float2 Temp = UnpackFloat2FromUInt(asuint(Data1.y));
XVec.z = Temp.x;
YVec.x = Temp.y;
YVec.yz = UnpackFloat2FromUInt(asuint(Data1.z));
float3 ZVec = cross(XVec, YVec);
Data.PreTranslatedInvTransform = float3x3(float3(XVec * Data.UniformScaleInv), float3(YVec * Data.UniformScaleInv), float3(ZVec * Data.UniformScaleInv));
// Data2
float3 Data2X = UnpackR11G11B10F(asuint(Data2.x));
Data.RadialFogExtinction= Data2X.x;
Data.HeightFogExtinction= Data2X.y;
Data.HeightFogFalloff = Data2X.z;
uint PackEmissiveData = asuint(Data2.y);
if (PackEmissiveData > 0)
{
Data.Emissive = UnpackR11G11B10F(PackEmissiveData);
}
else
{
Data.Emissive = 0.0f.xxx;
}
float4 Data2Z = UnpackRGBA8(asuint(Data2.z));
Data.Albedo = Data2Z.rgb;
Data.PhaseG = Data2Z.a;
Data.HeightFogOffset = Data2.w;
return Data;
}
///////////////////////////////////////////////////////////////////////////////
// Local Fog Volume Tile Data
uint2 LFVUnpackTile(uint In)
{
return uint2(In & 0xFFFF, In >> 16);
}
uint LFVPackTile(uint2 TileCoord)
{
#if COMPILER_SUPPORTS_PACK_INTRINSICS
return PackUInt2ToUInt(TileCoord.x, TileCoord.y);
#else
return TileCoord.x | (TileCoord.y << 16); // assumes 16bit is enough to represent a tiled resolution up to 65,535 :)
#endif
}
///////////////////////////////////////////////////////////////////////////////
// Local Fog Volume Integrals
struct FFogData
{
float IntegratedLuminanceFactor;
float Coverage;
};
FFogData LocalFogVolumeEvaluateAnalyticalIntegral(FLocalFogVolumeGPUInstanceData FogInstance, float3 RayStartU, float3 RayDirU, float RayLengthU)
{
FFogData FogData;
float RadialOpticalDepth = 0.0f;
float HeightOpticalDepth = 0.0f;
if (FogInstance.RadialFogExtinction > 0.0f)
{
//////////
// Radial fog is evaluated according to spatially varying density defined as "Density * (1 - (d*d)/(r*r))" in the unint sphere space, where density is in fact Extinction.
// An explanation of it can be found in https://iquilezles.org/articles/spheredensity/
float3 SphereCenter = 0.0f;
float3 VolumeCenterToRayO = (RayStartU - SphereCenter);
float b = dot(RayDirU, VolumeCenterToRayO);
float c = dot(VolumeCenterToRayO, VolumeCenterToRayO) - 1.0f;
float h = b * b - c;
if (h >= 0.0)
{
h = sqrt(h);
float Length0 = -b - h;
float Length1 = -b + h;
Length0 = max(Length0, 0.0);
Length1 = max(Length1, 0.0);
Length1 = min(Length1, RayLengthU);
// Integral of a Extinction that descreases according to square distance from the center of the volume.
const float Length0Sqr = Length0 * Length0;
const float Length1Sqr = Length1 * Length1;
float Integral0 = -(c * Length0 + b * Length0Sqr + Length0Sqr * Length0 / 3.0f);
float Integral1 = -(c * Length1 + b * Length1Sqr + Length1Sqr * Length1 / 3.0f);
RadialOpticalDepth = max(0.0, FogInstance.RadialFogExtinction * (Integral1 - Integral0) * (3.0f / 4.0f));
}
}
if (FogInstance.HeightFogExtinction > 0.0f)
{
//////////
// Height fog is evaluated according to spatially varying density defined as "Density * Exp[-HeightFogFalloff * z]", where density is in fact Extinction.
// An explanation of it can be found in https://iquilezles.org/articles/fog/.
float StartHeight = RayStartU.z - FogInstance.HeightFogOffset; // account for the fog offset.
// We are going to divide by RayDirU.z so we need it to be never be 0.
const float SafeDirThreshold = 0.0001;
const float SignRayDirUZ = RayDirU.z >= 0.0f ? 1.0f : -1.0f;
const float SafeRayDirUZ = (abs(RayDirU.z) < SafeDirThreshold) ? SafeDirThreshold * SignRayDirUZ : RayDirU.z;
#if 0
// Original integral, less artefact
HeightOpticalDepth = (FogInstance.HeightFogExtinction / FogInstance.HeightFogFalloff) * exp(-StartHeight * FogInstance.HeightFogFalloff) * (1.0 - exp(-SafeRayDirUZ * RayLengthU * FogInstance.HeightFogFalloff)) / SafeRayDirUZ;
#elif 1
// Reworked to avoid large value as input to exp
//OpticalDepth = (FogInstance.HeightFogExtinction / FogInstance.HeightFogFalloff) * (exp(-StartHeight * FogInstance.HeightFogFalloff) - exp(-StartHeight * FogInstance.HeightFogFalloff - RayDir.z * RayLength * FogInstance.HeightFogFalloff)) / RayDir.z;
// But we use an updated formulation to avoid large input to exp leading to inf and nan.
float Factor0 = StartHeight * FogInstance.HeightFogFalloff;
float Factor1 = SafeRayDirUZ * RayLengthU * FogInstance.HeightFogFalloff;
Factor0 = max(-80.0f, Factor0); // Simply clamp Factor0 to a valid range in order to not have exp explode.
HeightOpticalDepth += (FogInstance.HeightFogExtinction / (FogInstance.HeightFogFalloff * SafeRayDirUZ)) * (exp(-Factor0) - exp(-(Factor0 + Factor1)));
#endif
/*
Proof of the height fog integral equations using Mathematica
This show that IntegratedLuminanceFactor is really equivalent to coverage in this simple case.
(*Density of matter according to height Y*)
Density [y_] := A * Exp[-B * y]
(*Ray position*)
Ray[t_] := Oz + t * Rz
(*Integrate Optical Depth for a distance T*)
Fog[T_] := Integrate[Density[Ray[t]], {t, 0, T}]
(*Optical Depth integration equation*)
Fog[T]
(*Transmittance from Optical Depth*)
Exp[-Integrate[Density[Ray[t]], {t, 0, T}]]
(*Transmittance from a position to the origin of tracing*)
Trans[T_] := Exp[-Integrate[Density[Ray[t]], {t, 0, T}]]
(*Integrate emissive and scattering from height fog, assuming density \
is extinction and extinction=scattering, thus albedo=1*)
Integrate[Density[Ray[t]]*EmSc *Trans[t], {t, 0, T}]
=> this leads to EmSc - Transmittance * EmSc
EmSc * (1 - Transmittance)
EmSc * Coverage
*/
}
#if 1
// => Reference path
// We now combine the two optical depth for the "opacity"=1-transmittance to be combined together.
// This helps to have soft edges when using height fog. We also account for that when voxelising in the volumetric fog.
float TR = exp(-RadialOpticalDepth);
float TH = exp(-HeightOpticalDepth);
float T = 1 - (1 - TR) * (1 - TH);
float OpticalDepth = -log(T);
// Evaluate transmittance, accounting for the volume scale converted to local unit space meter.
float Transmittance = exp(-OpticalDepth * FogInstance.UniformScale * CENTIMETER_TO_METER);
#else
// => Optimised path, but does not match due to CommonFactor not being applied at the same place
const float CommonFactor = FogInstance.UniformScale * CENTIMETER_TO_METER;
float TR = exp(-RadialOpticalDepth);
float TH = exp(-HeightOpticalDepth);
float Transmittance = 1 - (1 - TR) * (1 - TH);
#endif
// Assuming extinction is grey scale, we can compute a single coverage value from transmittance
FogData.Coverage = (1.0 - Transmittance);
// Assuming extinction==scattering, i.e. albedo=1, we can compute the integrale for each point on a ray while accouting for transmittance back to the view.
// It turns out that, under these conditions, the luminance and emissive factor is simply coverage. => see Mathematica details at the bottom of this page.
FogData.IntegratedLuminanceFactor = FogData.Coverage;
return FogData;
}
///////////////////////////////////////////////////////////////////////////////
// Local Fog Volume Evaluation
float3 LocalFogVolumeEvaluateInScattering(in FLocalFogVolumeGPUInstanceData FogInstance, in FFogData FogData, in float3 RayDirWorld)
{
half3 InScattering = 0.0f;
// Lighting equation here matches the volumetric fog.
// Main forward directional light intensity
InScattering += LFVDirectionalLightColor * HenyeyGreensteinPhase(-FogInstance.PhaseG, dot(RayDirWorld, LFVDirectionalLightDirection));
// Sky light
if (View.SkyLightVolumetricScatteringIntensity > 0)
{
float3 SkyLighting = View.SkyLightColor.rgb * GetSkySHDiffuseSimple(RayDirWorld * -FogInstance.PhaseG);
const float SkyVisibility = 1.0f; // Not possible to use AO with analytical integration
InScattering += (SkyVisibility * View.SkyLightVolumetricScatteringIntensity) * SkyLighting;
}
// Apply the medium uniform single scattering albedo
InScattering *= FogInstance.Albedo;
// Now account for emissive luminance after albedo and heighfog contribution has been accounted for.
// This do mean that the emissive color follows the medium density variation.
InScattering += FogInstance.Emissive;
// Then scale the contribution according to the normalised pre intergrated scattering.
return InScattering * FogData.IntegratedLuminanceFactor;
}
///////////////////////////////////////////////////////////////////////////////
// Local Fog Volume Instance Evaluation
// Returns color transmittance
float4 GetLFVInstanceContribution(
in uint InstanceIndex,
in float3 CamRayTranslatedWorldOrigin,
in float3 CamRayTranslatedWorldDir,
in float3 DepthBufferTranslatedWorldPos,
in bool bPlatformSupportsVolumetricFogOntranslucent)
{
FLocalFogVolumeGPUInstanceData FogInstance = GetLocalFogVolumeGPUInstanceData(InstanceIndex);
float3 LuminanceColor = float3(0.0f, 0.0f, 0.0f);
float Transmittance = 1.0f;
// The "U" prefix is for all computation done in the Unit Sphere space.
float3 DepthBufferPosU = mul(DepthBufferTranslatedWorldPos - FogInstance.TranslatedWorlPos, FogInstance.PreTranslatedInvTransform);
float3 RayPosU = mul(CamRayTranslatedWorldOrigin - FogInstance.TranslatedWorlPos, FogInstance.PreTranslatedInvTransform);
float3 RayDirU = mul(CamRayTranslatedWorldDir, FogInstance.PreTranslatedInvTransform);
RayDirU.xyz = normalize(RayDirU);
float2 TsU = RayIntersectSphere(RayPosU, RayDirU, float4(0.0, 0.0, 0.0, 1.0));
//Clamp to make sure we only consider intersection in front of the camera
TsU = max(LFVGlobalStartDistance.xx * FogInstance.UniformScaleInv, TsU);
// Clamp the traced distance to the depth buffer and compute world length.
float3 ViewToDepthVector = DepthBufferPosU - RayPosU;
float ViewToDepthVectorSqrLength = dot(ViewToDepthVector, ViewToDepthVector);
float LengthDepthBufferU = sqrt(ViewToDepthVectorSqrLength);
TsU = min(TsU, LengthDepthBufferU);
if (any(TsU > 0.0))
{
// Make the ray start after the volumetric fog far distance
if (LFVRenderInVolumetricFog && bPlatformSupportsVolumetricFogOntranslucent)
{
// Volumetric fog covers up to MaxDistance along ViewZ, exclude analytical fog from this range
float CosAngle = dot(CamRayTranslatedWorldDir, View.ViewForward);
float InvCosAngle = (CosAngle > 0.0001) ? rcp(CosAngle) : 0;
float ExcludeDistance = View.VolumetricFogMaxDistance * InvCosAngle * FogInstance.UniformScaleInv;
TsU = max(TsU, ExcludeDistance);
}
float RayTracedLengthU = max(0.0, abs(TsU.y - TsU.x));
if (RayTracedLengthU > 0.0)
{
FFogData FogData = LocalFogVolumeEvaluateAnalyticalIntegral(FogInstance, RayPosU + RayDirU * TsU.x, RayDirU, RayTracedLengthU);
LuminanceColor = LocalFogVolumeEvaluateInScattering(FogInstance, FogData, CamRayTranslatedWorldDir);
Transmittance = 1.0 - FogData.Coverage;
}
}
return float4(LuminanceColor, Transmittance);
}
// Evaluate the local fog volume contribution for a world position.
// Returns a float4 with Luminance Color in RGB and Transmittance in A.
float4 GetLFVContribution(
in ViewState ResolvedView,
in uint2 TilePos,
in float3 DepthBufferTranslatedWorldPos,
in bool bPlatformSupportsVolumetricFogOntranslucent,
inout uint OutLFVCount)
{
const float3 CamRayTranslatedWorldOrigin= ResolvedView.TranslatedWorldCameraOrigin;
const float3 CamRayWorldDir = normalize(DepthBufferTranslatedWorldPos - CamRayTranslatedWorldOrigin);
OutLFVCount = LFVTileDataTex[uint3(TilePos, 0)];
float3 LFVLuminanceColor = 0.0;
float LFVTransmittance = 1.0;
for (int LFVIndex = 0; LFVIndex < OutLFVCount; ++LFVIndex)
{
uint InstanceIndex = LFVTileDataTex[uint3(TilePos, 1 + LFVIndex)];
float4 InstanceContribution = GetLFVInstanceContribution(
InstanceIndex,
CamRayTranslatedWorldOrigin,
CamRayWorldDir,
DepthBufferTranslatedWorldPos,
bPlatformSupportsVolumetricFogOntranslucent);
LFVLuminanceColor = LFVLuminanceColor * InstanceContribution.a + InstanceContribution.rgb;
LFVTransmittance = LFVTransmittance * InstanceContribution.a;
}
return float4(LFVLuminanceColor, LFVTransmittance);
}
float4 GetLFVContribution(
in ViewState ResolvedView,
in uint2 TilePos,
in float3 TranslatedWorldPosition,
in bool bPlatformSupportsVolumetricFogOntranslucent = true)
{
uint OutLFVCount = 0;
return GetLFVContribution(ResolvedView, TilePos, TranslatedWorldPosition, bPlatformSupportsVolumetricFogOntranslucent, OutLFVCount);
}