Files
UnrealEngine/Engine/Shaders/Private/PathTracing/PathTracingVolumetricCloudMaterialShader.usf
2025-05-18 13:04:45 +08:00

462 lines
19 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
#define PATH_TRACING 1
#define PATH_TRACER_USE_CLOUD_SHADER 1
#include "/Engine/Private/Common.ush"
#include "/Engine/Private/RayTracing/RayTracingCommon.ush"
#include "/Engine/Private/PathTracing/PathTracingShaderUtils.ush"
// Provide aliases for the parameters that now come through a shader buffer
#define FogDensity PathTracingCloudParameters.FogDensity
#define FogHeight PathTracingCloudParameters.FogHeight
#define FogFalloff PathTracingCloudParameters.FogFalloff
#define FogAlbedo PathTracingCloudParameters.FogAlbedo
#define FogPhaseG PathTracingCloudParameters.FogPhaseG
#define FogCenter PathTracingCloudParameters.FogCenter
#define FogMinZ PathTracingCloudParameters.FogMinZ
#define FogMaxZ PathTracingCloudParameters.FogMaxZ
#define FogRadius PathTracingCloudParameters.FogRadius
#define FogFalloffClamp PathTracingCloudParameters.FogFalloffClamp
#define PlanetCenterTranslatedWorldHi PathTracingCloudParameters.PlanetCenterTranslatedWorldHi
#define PlanetCenterTranslatedWorldLo PathTracingCloudParameters.PlanetCenterTranslatedWorldLo
#define Atmosphere PathTracingCloudParameters
#define CloudAccelerationMap PathTracingCloudParameters.CloudAccelerationMap
#define CloudAccelerationMapSampler PathTracingCloudParameters.CloudAccelerationMapSampler
#define CloudClipX PathTracingCloudParameters.CloudClipX
#define CloudClipY PathTracingCloudParameters.CloudClipY
#define CloudClipZ PathTracingCloudParameters.CloudClipZ
#define CloudLayerBotKm PathTracingCloudParameters.CloudLayerBotKm
#define CloudLayerTopKm PathTracingCloudParameters.CloudLayerTopKm
#define CloudClipDistKm PathTracingCloudParameters.CloudClipDistKm
#define CloudClipRadiusKm PathTracingCloudParameters.CloudClipRadiusKm
#define CloudClipCenterKm PathTracingCloudParameters.CloudClipCenterKm
#define CloudTracingMaxDistance PathTracingCloudParameters.CloudTracingMaxDistance
#define CloudRoughnessCutoff PathTracingCloudParameters.CloudRoughnessCutoff
#define CloudAccelMapResolution PathTracingCloudParameters.CloudAccelMapResolution
#define CloudVoxelWidth PathTracingCloudParameters.CloudVoxelWidth
#define CloudInvVoxelWidth PathTracingCloudParameters.CloudInvVoxelWidth
#include "./Volume/PathTracingCloudsMaterialCommon.ush"
#include "./Volume/PathTracingFog.ush"
#include "./Volume/PathTracingAtmosphere.ush"
#include "./Volume/PathTracingVolumeSampling.ush"
#include "./PathTracingCommon.ush"
FVolumeShadedResult CloudsGetDensity(float3 TranslatedWorldPos)
{
#if CLOUD_VISUALIZATION_SHADER == 1
float3 P = mul(GetCloudClipBasis(), TranslatedWorldPos); // WorldToTangent
float2 UV = (P.xy * CM_TO_SKY_UNIT + CloudClipDistKm.xx) / (2 * CloudClipDistKm);
const int Level = 0;
float4 Data = CloudAccelerationMap.SampleLevel(CloudAccelerationMapSampler, UV, Level);
float Z = P.z * CM_TO_SKY_UNIT;
float Density = (Z > Data.z && Z < Data.w) ? Data.x : 0.0;
FVolumeShadedResult Result = (FVolumeShadedResult)0;
Result.SigmaT = Density;
Result.SigmaSDualHG = 0.85 * Density;
return Result;
#else
FDFVector3 AbsoluteWorldPosition = DFFastSubtract(TranslatedWorldPos, ResolvedView.PreViewTranslation);
// Measure HeightKm as the distance to the planet center
const FDFVector3 OC = DFMultiply(DFSubtract(TranslatedWorldPos, MakeDFVector3(PlanetCenterTranslatedWorldHi, PlanetCenterTranslatedWorldLo)), CM_TO_SKY_UNIT);
const float H = DFLengthDemote(OC); // HeightKm
float B = CloudLayerBotKm;
float T = CloudLayerTopKm;
FSampleCloudMaterialResult CloudResult = SampleCloudMaterial(AbsoluteWorldPosition, H, B, T);
FVolumeShadedResult Result = (FVolumeShadedResult)0;
Result.SigmaT = CloudResult.Extinction;
Result.SigmaSDualHG = CloudResult.Albedo * Result.SigmaT;
Result.Emission = CloudResult.Emission;
#if MATERIAL_VOLUMETRIC_ADVANCED
Result.DualPhaseData = float3(CloudResult.PhaseG1, CloudResult.PhaseG2, CloudResult.PhaseBlend);
#endif
return Result;
#endif
}
RAY_TRACING_ENTRY_CALLABLE(PathTracingVolumetricCloudMaterialShader, FPackedPathTracingPayload, PackedPayload)
{
ResolvedView = ResolveView();
// Ray marching happens inside here so that we only have to pay for the cost of callable shading _once_
// This comes at the cost of some duplication of the logic
// TODO: Can we use HLSL templates to merge some of the duplicated logic?
RandomSequence RandSeq = PackedPayload.GetVolumetricCallableShaderInputRandSequence();
FRayDesc Ray = PackedPayload.GetVolumetricCallableShaderInputRay();
float3 PathThroughput = PackedPayload.GetVolumetricCallableShaderInputThroughput();
float CloudDensity = PackedPayload.GetVolumetricCallableShaderInputCloudDensity();
const uint Flags = PackedPayload.GetVolumetricCallableShaderInputFlags();
const int Bounce = (Flags & PATH_TRACER_VOLUME_CALLABLE_FLAGS_BOUNCE_MASK) >> PATH_TRACER_VOLUME_CALLABLE_FLAGS_BOUNCE_SHIFT;
const bool bIsCameraRay = Bounce == 0;
// TODO: evaluate if it would be better to make a second shader for this (so that we don't get two copies of the shader eval in inner loops)
// TODO: evaluate if voxel traversal is worth it for transmittance
if (Flags & PATH_TRACER_VOLUME_CALLABLE_FLAGS_TRANSMITTANCE)
{
const float CloudFactor = PackedPayload.GetVolumetricCallableShaderInputCloudFactor();
// Reset payload
PackedPayload = (FPackedPathTracingPayload)0;
// Transmittance loop for the cloud shader only
// NOTE: We have specialized this since we know our upper bound is a scalar value, most of the pdf math cancels out and we can take a faster path
float TMin = Ray.TMin;
float TMax = Ray.TMax;
float SigmaBar = CloudDensity * CloudFactor, InvSigmaBar = rcp(SigmaBar);
float3 Throughput = PathThroughput;
for (int Step = 0; Step < PathTracingCloudParameters.MaxRaymarchSteps; Step++)
{
/* take stochastic steps along the ray to estimate transmittance (null scattering) */
/* Sample the distance to the next interaction */
float RandValue = RandomSequence_GenerateSample1D(RandSeq);
TMin -= log(1 - RandValue) * InvSigmaBar;
if (TMin >= TMax)
{
/* exit the ray marching loop */
break;
}
/* our ray marching step stayed inside the atmo and is still in front of the next hit */
float3 WorldPos = Ray.Origin + TMin * Ray.Direction;
/* clamp to make sure we never exceed the majorant (should not be the case, but need to avoid any possible numerical issues) */
float3 SigmaT = min(CloudFactor * CloudsGetDensity(WorldPos).SigmaT, SigmaBar);
/* keep tracing through the volume */
Throughput *= 1.0 - SigmaT * InvSigmaBar;
/* Encourage early termination of paths with low contribution */
Throughput *= LowThroughputClampFactor(Throughput);
if (!any(Throughput > 0))
{
break;
}
}
// Do we need to know the sample at the _end_ of the ray interval? capture it first in case it does not scatter any light
if (Flags & PATH_TRACER_VOLUME_CALLABLE_FLAGS_GET_SAMPLE)
{
#if MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT > 0
FCloudMaterialMSParams MS = (FCloudMaterialMSParams)0;
if (PathTracingCloudParameters.CloudShaderMultipleScatterApproxEnabled)
{
MS = CloudMaterialGetMSParams();
}
FRISContext HitSample = InitRISContext(RandomSequence_GenerateSample1D(RandSeq));
float3 FinalThroughput = 0;
#endif
float3 TranslatedWorldPos = Ray.Origin + Ray.Direction * Ray.TMax;
FVolumeShadedResult CloudResult = (FVolumeShadedResult)0;
MergeVolumeShadedResult(CloudResult, CloudsGetDensity(TranslatedWorldPos), bIsCameraRay && (Flags & PATH_TRACER_VOLUME_HOLDOUT_CLOUDS) != 0);
FVolumeShadedResult Result = CloudResult;
if (Flags & PATH_TRACER_VOLUME_ENABLE_ATMOSPHERE)
{
MergeVolumeShadedResult(Result, AtmosphereGetDensity(TranslatedWorldPos), bIsCameraRay && (Flags & PATH_TRACER_VOLUME_HOLDOUT_ATMOSPHERE) != 0);
}
if (Flags & PATH_TRACER_VOLUME_ENABLE_FOG)
{
MergeVolumeShadedResult(Result, FogGetDensity(TranslatedWorldPos), bIsCameraRay && (Flags & PATH_TRACER_VOLUME_HOLDOUT_FOG) != 0);
}
float3 SigmaS = min(Result.SigmaSRayleigh + Result.SigmaSHG + Result.SigmaSDualHG, Result.SigmaT);
#if MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT > 0
float3 Contrib = Throughput * SigmaS;
float SelectionWeight = max3(Contrib.x, Contrib.y, Contrib.z);
if (HitSample.Accept(SelectionWeight))
#else
Throughput *= SigmaS;
#endif
{
const float3 RayleighWeight = select(SigmaS > 0.0, Result.SigmaSRayleigh / SigmaS, 0.0);
const float3 HGWeight = select(SigmaS > 0.0, Result.SigmaSHG / SigmaS, 0.0);
const float3 DualHGWeight = select(SigmaS > 0.0, Result.SigmaSDualHG / SigmaS, 0.0);
PackedPayload.SetVolumetricCallableShaderOutputSample(Ray.TMax, RayleighWeight, HGWeight, Result.PhaseG, DualHGWeight, Result.DualPhaseData, 0.0);
PackedPayload.SetVolumetricCallableShaderOutputCloudDensityFactor(1.0);
#if MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT > 0
FinalThroughput = Contrib / SelectionWeight;
#endif
}
#if MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT > 0
float S = MS.ScattFactor, E = MS.ExtinFactor, P = MS.PhaseFactor;
for (int ms = 1; ms <= MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT; ms++)
{
CloudResult.SigmaSDualHG *= S;
// Propose a candidate hit with just the scaled cloud contribution
Contrib = Throughput * CloudResult.SigmaSDualHG;
SelectionWeight = max3(Contrib.x, Contrib.y, Contrib.z);
if (HitSample.Accept(SelectionWeight))
{
// stash this hit for next time
FinalThroughput = Contrib / SelectionWeight;
// We create a blend of SigmaHG (set to isotropic) and SigmaSDualHG (set to the original phase function) to match what raster does (lerp the eval)
PackedPayload.SetVolumetricCallableShaderOutputSample(Ray.TMax, 0.0, P, 0.0, 1.0 - P, CloudResult.DualPhaseData, 0.0);
PackedPayload.SetVolumetricCallableShaderOutputCloudDensityFactor(E); // so that future cloud shadow rays get attenuated
}
// Update scale factors for next iteration
S *= S;
P *= P;
E *= E;
}
Throughput = FinalThroughput * HitSample.GetNormalization();
#endif
}
PackedPayload.SetVolumetricCallableShaderOutput(RandSeq, Throughput);
}
else
{
// Loop through combined density for sampling
FRISContext HitSample = PackedPayload.GetVolumetricCallableShaderRISContext();
#if MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT > 0
FCloudMaterialMSParams MS = (FCloudMaterialMSParams)0;
if (PathTracingCloudParameters.CloudShaderMultipleScatterApproxEnabled)
{
MS = CloudMaterialGetMSParams();
}
#endif
// zero out the payload so we can write our output to it
PackedPayload = (FPackedPathTracingPayload) 0;
PackedPayload.HitT = -1.0; // invalid sample until we are sure we got one
#define USE_VOXEL_TRAVERSAL 1
#if USE_VOXEL_TRAVERSAL
// isect clip slabs, the AABB our clouds are enclosed in
float3 Ro = mul(GetCloudClipBasis(), Ray.Origin);
float3 Rd = mul(GetCloudClipBasis(), Ray.Direction);
float3 InvRd = rcp(max(abs(Rd), 1e-6)) * select(Rd >= 0, 1.0, -1.0); // protect against division by 0
// now that we know the ray length, lets march through the grid to see if we can clip away anything
float2 P = Ro.xy + Ray.TMin * Rd.xy;
int2 Idx = PositionToVoxel(P);
float2 NextCrossingT = Ray.TMin + (VoxelToPosition(Idx + int2(Rd.xy >= 0)) - P) * InvRd.xy;
float2 DeltaT = abs(CloudVoxelWidth * InvRd.xy);
int2 Step = select_internal(Rd.xy >= 0, 1, -1);
int2 Stop = select_internal(Rd.xy >= 0, CloudAccelMapResolution, -1);
#endif
float3 VolumeRadiance = 0;
float VolumeAlpha = 0;
float3 VolumeAlbedo = 0;
// Limit number of steps to prevent timeouts
// TODO: This biases the result! Is there a better way to limit the number of steps?
for (int RaymarchStep = 0; RaymarchStep < PathTracingCloudParameters.MaxRaymarchSteps; RaymarchStep++)
{
#if USE_VOXEL_TRAVERSAL
// visit current voxel
float4 Data = CloudAccelerationMap.Load(uint3(Idx.xy, 0));
CloudDensity = Data.x;
// because each cell has slabs, we can have at most 3 segments (empty, filled, empty) per grid cell
float CellMinT = Ray.TMin;
float CellMaxT = min3(Ray.TMax, NextCrossingT.x, NextCrossingT.y);
float IntegrationMaxT = CellMaxT;
if (CloudDensity > 0)
{
float SlabT0 = (Data.z * SKY_UNIT_TO_CM - Ro.z) * InvRd.z;
float SlabT1 = (Data.w * SKY_UNIT_TO_CM - Ro.z) * InvRd.z;
float InsideMinT = max(CellMinT, min(SlabT0, SlabT1));
float InsideMaxT = min(CellMaxT, max(SlabT0, SlabT1));
// do we still have a valid interval?
if (InsideMinT < InsideMaxT)
{
// this interval must be going through at least 3 portions (some of which may be empty)
// Figure out in which we are at the moment
if (Ray.TMin < InsideMinT)
{
// we are outside the slab, take steps up to the slab boundary
CloudDensity = 0;
IntegrationMaxT = InsideMinT;
} else if (Ray.TMin < InsideMaxT)
{
// we are inside the slab, take steps up to the end of the slab
IntegrationMaxT = InsideMaxT;
} else {
// we are past the slab, take steps up to the cell boundary
CloudDensity = 0;
}
}
else
{
// we missed the slabs, so only keep the one segment, going through empty space
CloudDensity = 0;
}
}
#else
float IntegrationMaxT = Ray.TMax;
#endif
float3 SigmaBar = CloudDensity;
if (Flags & PATH_TRACER_VOLUME_ENABLE_ATMOSPHERE)
{
SigmaBar += AtmosphereGetDensityBounds(Ray.Origin, Ray.Direction, Ray.TMin, Ray.TMax).SigmaMax;
}
if (Flags & PATH_TRACER_VOLUME_ENABLE_FOG)
{
SigmaBar += FogGetDensityBounds(Ray.Origin, Ray.Direction, Ray.TMin, Ray.TMax).SigmaMax;
}
// Interval is only crossing atmo/clouds/fog
float RandValue = RandomSequence_GenerateSample1D(RandSeq);
float Distance = SampleSpectralTransmittance(RandValue, SigmaBar, PathThroughput);
if (Distance < 0.0)
{
// no energy left
break;
}
if (Ray.TMin + Distance < IntegrationMaxT)
{
float4 Evaluation = EvaluateSpectralTransmittanceHit(Distance, SigmaBar, PathThroughput);
PathThroughput *= Evaluation.xyz;
Ray.TMin += Distance;
// find out how much volume exists at the current point
float3 Ro = Ray.Origin;
float3 Rd = Ray.Direction;
float3 TranslatedWorldPos = Ro + Ray.TMin * Rd;
FVolumeShadedResult Result = (FVolumeShadedResult)0;
MergeVolumeShadedResult(Result, CloudsGetDensity(TranslatedWorldPos), bIsCameraRay && (Flags & PATH_TRACER_VOLUME_HOLDOUT_CLOUDS) != 0);
#if MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT > 0
FVolumeShadedResult CloudResult = Result; // capture just the Cloud contribution (including holdouts) so we can scale it for the MS approx
#endif
if (Flags & PATH_TRACER_VOLUME_ENABLE_ATMOSPHERE)
{
MergeVolumeShadedResult(Result, AtmosphereGetDensity(TranslatedWorldPos), bIsCameraRay && (Flags & PATH_TRACER_VOLUME_HOLDOUT_ATMOSPHERE) != 0);
}
if (Flags & PATH_TRACER_VOLUME_ENABLE_FOG)
{
MergeVolumeShadedResult(Result, FogGetDensity(TranslatedWorldPos), bIsCameraRay && (Flags & PATH_TRACER_VOLUME_HOLDOUT_FOG) != 0);
}
// clamp to make sure we never exceed the majorant (should not be the case, but need to avoid any possible numerical issues)
float3 SigmaT = min(Result.SigmaT, SigmaBar);
float3 SigmaN = SigmaBar - SigmaT;
float3 SigmaH = min(Result.SigmaH, SigmaT);
VolumeAlpha += Luminance(PathThroughput * (SigmaT - SigmaH));
// If this ray wants to get candidate samples for scatter, process them here
if (Flags & PATH_TRACER_VOLUME_CALLABLE_FLAGS_GET_SAMPLE)
{
float3 SigmaS = min(Result.SigmaSRayleigh + Result.SigmaSHG + Result.SigmaSDualHG, SigmaT);
// accumulate a signal for the denoiser
float3 Contrib = PathThroughput * SigmaS;
VolumeAlbedo += Contrib;
float SelectionWeight = max3(Contrib.x, Contrib.y, Contrib.z);
if (HitSample.Accept(SelectionWeight))
{
// stash this hit for next time
float3 PayloadThroughput = Contrib / SelectionWeight;
const float3 RayleighWeight = select(SigmaS > 0.0, Result.SigmaSRayleigh / SigmaS, 0.0);
const float3 HGWeight = select(SigmaS > 0.0, Result.SigmaSHG / SigmaS, 0.0);
const float3 DualHGWeight = select(SigmaS > 0.0, Result.SigmaSDualHG / SigmaS, 0.0);
PackedPayload.SetVolumetricCallableShaderOutputSample(Ray.TMin, RayleighWeight, HGWeight, Result.PhaseG, DualHGWeight, Result.DualPhaseData, PayloadThroughput);
PackedPayload.SetVolumetricCallableShaderOutputCloudDensityFactor(1.0);
}
#if MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT > 0
// The material has enabled fake-multiple scattering. We can take this into account here by stochastically returning additional lobes
// NOTE: This is not energy conserving! TODO: how to integrate with indirect scattering?
float S = MS.ScattFactor, E = MS.ExtinFactor, P = MS.PhaseFactor;
for (int ms = 1; ms <= MATERIAL_VOLUMETRIC_ADVANCED_MULTISCATTERING_OCTAVE_COUNT; ms++)
{
CloudResult.SigmaSDualHG *= S;
// Propose a candidate hit with just the scaled cloud contribution
Contrib = PathThroughput * CloudResult.SigmaSDualHG;
SelectionWeight = max3(Contrib.x, Contrib.y, Contrib.z);
if (HitSample.Accept(SelectionWeight))
{
// stash this hit for next time
float3 PayloadThroughput = Contrib / SelectionWeight;
// We create a blend of SigmaHG (set to isotropic) and SigmaSDualHG (set to the original phase function) to match what raster does (lerp the eval)
PackedPayload.SetVolumetricCallableShaderOutputSample(Ray.TMin, 0.0, P, 0.0, 1.0 - P, CloudResult.DualPhaseData, PayloadThroughput);
PackedPayload.SetVolumetricCallableShaderOutputCloudDensityFactor(E); // so that future cloud shadow rays get attenuated
}
// Update scale factors for next iteration
S *= S;
P *= P;
E *= E;
}
#endif
}
VolumeRadiance += Result.Emission * PathThroughput;
// keep tracing through the volume
PathThroughput *= SigmaN;
PathThroughput *= LowThroughputClampFactor(PathThroughput);
if (!any(PathThroughput > 0))
{
break;
}
}
else
{
// update the path throughput, knowing that we escaped the medium
// exit the ray marching loop
float4 Evaluation = EvaluateSpectralTransmittanceMiss(IntegrationMaxT - Ray.TMin, SigmaBar, PathThroughput);
PathThroughput *= Evaluation.xyz;
#if USE_VOXEL_TRAVERSAL
Ray.TMin = IntegrationMaxT;
#else
break;
#endif
}
#if USE_VOXEL_TRAVERSAL
// Advance to next grid cell if we have reached the end of the cell
if (Ray.TMin == CellMaxT)
{
if (NextCrossingT.x < NextCrossingT.y)
{
// step x first
Idx.x += Step.x;
if (Idx.x == Stop.x || Ray.TMax < NextCrossingT.x)
{
break;
}
Ray.TMin = NextCrossingT.x;
NextCrossingT.x += DeltaT.x;
}
else
{
Idx.y += Step.y;
if (Idx.y == Stop.y || Ray.TMax < NextCrossingT.y)
{
break;
}
Ray.TMin = NextCrossingT.y;
NextCrossingT.y += DeltaT.y;
}
}
#endif
}
// Write state back here
PackedPayload.SetVolumetricCallableShaderOutputRadiance(VolumeRadiance, VolumeAlpha);
PackedPayload.SetVolumetricCallableShaderOutput(RandSeq, PathThroughput, VolumeAlbedo);
PackedPayload.SetVolumetricCallableShaderRISContext(HitSample);
}
}