You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

890 lines
42 KiB

//--------------------------------------------------------------------------------------------------
// Definitions
//--------------------------------------------------------------------------------------------------
// #pragma enable_d3d11_debug_symbols
#pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch
#pragma kernel VolumetricLighting
#pragma multi_compile _ LIGHTLOOP_DISABLE_TILE_AND_CLUSTER
#pragma multi_compile _ ENABLE_REPROJECTION
#pragma multi_compile _ ENABLE_ANISOTROPY
#pragma multi_compile _ VL_PRESET_OPTIMAL
#pragma multi_compile _ SUPPORT_LOCAL_LIGHTS
#pragma multi_compile _ SUPPORT_WATER_ABSORPTION
#pragma multi_compile _ PROBE_VOLUMES_L1 PROBE_VOLUMES_L2
// Don't want contact shadows
#define LIGHT_EVALUATION_NO_CONTACT_SHADOWS // To define before LightEvaluation.hlsl
// #define LIGHT_EVALUATION_NO_HEIGHT_FOG
#ifndef LIGHTLOOP_DISABLE_TILE_AND_CLUSTER
#define USE_BIG_TILE_LIGHTLIST
#endif
#ifdef VL_PRESET_OPTIMAL
// E.g. for 1080p: (1920/8)x(1080/8)x(64) = 2,073,600 voxels
#define VBUFFER_VOXEL_SIZE 8
#endif
#define PREFER_HALF 0
#define GROUP_SIZE_1D 8
#define SHADOW_USE_DEPTH_BIAS 0 // Too expensive, not particularly effective
#define PUNCTUAL_SHADOW_ULTRA_LOW // Different options are too expensive.
#define DIRECTIONAL_SHADOW_ULTRA_LOW // Different options are too expensive.
#define AREA_SHADOW_LOW // Different options are too expensive.
#define SHADOW_AUTO_FLIP_NORMAL 0 // No normal information, so no need to flip
#define SHADOW_VIEW_BIAS 1 // Prevents light leaking through thin geometry. Not as good as normal bias at grazing angles, but cheaper and independent from the geometry.
#define USE_DEPTH_BUFFER 1 // Accounts for opaque geometry along the camera ray
#define SAMPLE_PROBE_VOLUMES 1 && (defined(PROBE_VOLUMES_L1) || defined(PROBE_VOLUMES_L2))
#define SHADOW_DATA_NOT_GUARANTEED_SCALAR // We are not looping over shadows in a scalarized fashion. If we will at some point, remove this define.
//--------------------------------------------------------------------------------------------------
// Included headers
//--------------------------------------------------------------------------------------------------
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Filtering.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/VolumeRendering.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/EntityLighting.hlsl"
// We need to include this "for reasons"...
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Builtin/BuiltinData.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderPass/ShaderPass.cs.hlsl"
#define SHADERPASS SHADERPASS_VOLUMETRIC_LIGHTING
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/VolumetricLighting/HDRenderPipeline.VolumetricLighting.cs.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"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/LightLoopDef.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightEvaluation.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/BuiltinGIUtilities.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Water/Shaders/UnderWaterUtilities.hlsl"
//--------------------------------------------------------------------------------------------------
// Inputs & outputs
//--------------------------------------------------------------------------------------------------
RW_TEXTURE3D(float4, _VBufferLighting);
RW_TEXTURE3D(float4, _VBufferFeedback);
TEXTURE3D(_VBufferHistory);
TEXTURE3D(_VBufferDensity); // RGB = scattering, A = extinction
TEXTURE2D_X(_MaxZMaskTexture);
StructuredBuffer<float4> _VolumetricAmbientProbeBuffer;
//--------------------------------------------------------------------------------------------------
// Implementation
//--------------------------------------------------------------------------------------------------
// Ambient probe for volumetric contains a convolution with Cornette Shank phase function so it needs to sample a different buffer.
real3 EvaluateVolumetricAmbientProbe(real3 normalWS)
{
#if SAMPLE_PROBE_VOLUMES // If we sample APV, ambient is baked in into APV and fallback into ambient is included in the APV sample.
return 0;
#else
real4 SHCoefficients[7];
SHCoefficients[0] = _VolumetricAmbientProbeBuffer[0];
SHCoefficients[1] = _VolumetricAmbientProbeBuffer[1];
SHCoefficients[2] = _VolumetricAmbientProbeBuffer[2];
SHCoefficients[3] = _VolumetricAmbientProbeBuffer[3];
SHCoefficients[4] = _VolumetricAmbientProbeBuffer[4];
SHCoefficients[5] = _VolumetricAmbientProbeBuffer[5];
SHCoefficients[6] = _VolumetricAmbientProbeBuffer[6];
return SampleSH9(SHCoefficients, normalWS);
#endif
}
// Jittered ray with screen-space derivatives.
struct JitteredRay
{
float3 originWS;
float3 centerDirWS;
float3 jitterDirWS;
float3 xDirDerivWS;
float3 yDirDerivWS;
float geomDist;
float maxDist;
};
struct VoxelLighting
{
float3 radianceComplete;
float3 radianceNoPhase;
};
bool IsInRange(float x, float2 range)
{
return clamp(x, range.x, range.y) == x;
}
float ComputeHistoryWeight()
{
// Compute the exponential moving average over 'n' frames:
// X = (1 - a) * ValueAtFrame[n] + a * AverageOverPreviousFrames.
// We want each sample to be uniformly weighted by (1 / n):
// X = (1 / n) * Sum{i from 1 to n}{ValueAtFrame[i]}.
// Therefore, we get:
// (1 - a) = (1 / n) => a = (1 - 1 / n) = (n - 1) / n,
// X = (1 / n) * ValueAtFrame[n] + (1 - 1 / n) * AverageOverPreviousFrames.
// Why does it work? We need to make the following assumption:
// AverageOverPreviousFrames ≈ AverageOverFrames[n - 1].
// AverageOverFrames[n - 1] = (1 / (n - 1)) * Sum{i from 1 to n - 1}{ValueAtFrame[i]}.
// This implies that the reprojected (accumulated) value has mostly converged.
// X = (1 / n) * ValueAtFrame[n] + ((n - 1) / n) * (1 / (n - 1)) * Sum{i from 1 to n - 1}{ValueAtFrame[i]}.
// X = (1 / n) * ValueAtFrame[n] + (1 / n) * Sum{i from 1 to n - 1}{ValueAtFrame[i]}.
// X = Sum{i from 1 to n}{ValueAtFrame[i] / n}.
float numFrames = 7;
float frameWeight = 1 / numFrames;
float historyWeight = 1 - frameWeight;
return historyWeight;
}
// Computes the light integral (in-scattered radiance) within the voxel.
// Multiplication by the scattering coefficient and the phase function is performed outside.
VoxelLighting EvaluateVoxelLightingDirectional(LightLoopContext context, uint featureFlags, PositionInputs posInput, float3 centerWS,
JitteredRay ray, float t0, float t1, float dt, float rndVal, float extinction, float anisotropy, bool underWater)
{
VoxelLighting lighting;
ZERO_INITIALIZE(VoxelLighting, lighting);
BuiltinData unused; // Unused for now, so define once
ZERO_INITIALIZE(BuiltinData, unused);
const float NdotL = 1;
float tOffset, weight;
ImportanceSampleHomogeneousMedium(rndVal, extinction, dt, tOffset, weight);
float t = t0 + tOffset;
posInput.positionWS = ray.originWS + t * ray.jitterDirWS;
context.shadowValue = 1.0;
// Evaluate sun shadows.
if (_DirectionalShadowIndex >= 0)
{
DirectionalLightData light = _DirectionalLightDatas[_DirectionalShadowIndex];
// Prep the light so that it works with non-volumetrics-aware code.
light.contactShadowMask = 0;
light.shadowDimmer = light.volumetricShadowDimmer;
float3 L = -light.forward;
// Is it worth sampling the shadow map?
if ((light.volumetricLightDimmer > 0) && (light.volumetricShadowDimmer > 0))
{
#if SHADOW_VIEW_BIAS
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
// We fake view bias by invoking the normal bias code with the view direction.
float3 shadowN = -ray.jitterDirWS;
#else
float3 shadowN = 0; // No bias
#endif // SHADOW_VIEW_BIAS
context.shadowValue = GetDirectionalShadowAttenuation(context.shadowContext,
posInput.positionSS, posInput.positionWS, shadowN,
light.shadowIndex, L);
// Apply the volumetric cloud shadow if relevant
if (_VolumetricCloudsShadowOriginToggle.w == 1.0)
context.shadowValue *= EvaluateVolumetricCloudsShadows(light, posInput.positionWS);
}
}
else
{
context.shadowValue = 1;
}
for (uint i = 0; i < _DirectionalLightCount; ++i)
{
DirectionalLightData light = _DirectionalLightDatas[i];
// Prep the light so that it works with non-volumetrics-aware code.
light.contactShadowMask = 0;
light.shadowDimmer = light.volumetricShadowDimmer;
float3 L = -light.forward;
// Is it worth evaluating the light?
float3 color; float attenuation;
if (light.volumetricLightDimmer > 0)
{
float4 lightColor = EvaluateLight_Directional(context, posInput, light);
#if SUPPORT_WATER_ABSORPTION
// Evaluate caustics for underwater
if (underWater)
{
// Project caustics as a cookie, taking account refraction otherwise it doesn't look right when sun is low
WaterSurfaceProfile prof = _WaterSurfaceProfiles[_UnderWaterSurfaceIndex];
float3 fwd = refract(light.forward, prof.upDirection, 1.0 / 1.333);
float3 tangent = normalize(cross(fwd, float3(0.0, 1.0, 0.0)));
float3 bitangent = cross(tangent, fwd);
float3 posWS = GetAbsolutePositionWS(posInput.positionWS) * _UnderWaterCausticsTilingFactor;
float2 positionNDC = float2(dot(posWS, tangent), dot(posWS, bitangent)) * 0.5 + 0.5;
float caustic = SAMPLE_TEXTURE2D_LOD(_WaterCausticsDataBuffer, s_linear_repeat_sampler, positionNDC, 0).r;
lightColor.a *= 1.0 + caustic * _UnderWaterCausticsIntensity;
// Simulate light absorption from depth
float distanceToSurface = max(-dot(posInput.positionWS, prof.upDirection) - GetWaterCameraHeight(), 0);
lightColor.a *= exp(-distanceToSurface * _UnderWaterScatteringExtinction.w);
}
#endif
// The volumetric light dimmer, unlike the regular light dimmer, is not pre-multiplied.
lightColor.a *= light.volumetricLightDimmer;
lightColor.rgb *= lightColor.a; // Composite
#if SHADOW_VIEW_BIAS
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
// We fake view bias by invoking the normal bias code with the view direction.
float3 shadowN = -ray.jitterDirWS;
#else
float3 shadowN = 0; // No bias
#endif // SHADOW_VIEW_BIAS
// This code works for both surface reflection and thin object transmission.
SHADOW_TYPE shadow = EvaluateShadow_Directional(context, posInput, light, unused, shadowN);
lightColor.rgb *= ComputeShadowColor(shadow, light.shadowTint, light.penumbraTint);
// Important:
// Ideally, all scattering calculations should use the jittered versions
// of the sample position and the ray direction. However, correct reprojection
// of asymmetrically scattered lighting (affected by an anisotropic phase
// function) is not possible. We work around this issue by reprojecting
// lighting not affected by the phase function. This basically removes
// the phase function from the temporal integration process. It is a hack.
// The downside is that anisotropy no longer benefits from temporal averaging,
// and any temporal instability of anisotropy causes causes visible jitter.
// In order to stabilize the image, we use the voxel center for all
// anisotropy-related calculations.
float cosTheta = dot(L, ray.centerDirWS);
float phase = CornetteShanksPhasePartVarying(anisotropy, cosTheta);
// Compute the amount of in-scattered radiance.
// Note: the 'weight' accounts for transmittance from 't0' to 't'.
lighting.radianceNoPhase += (weight * lightColor.rgb);
lighting.radianceComplete += (weight * lightColor.rgb) * phase;
}
}
return lighting;
}
// Computes the light integral (in-scattered radiance) within the voxel.
// Multiplication by the scattering coefficient and the phase function is performed outside.
VoxelLighting EvaluateVoxelLightingLocal(LightLoopContext context, uint groupIdx, uint featureFlags, PositionInputs posInput,
uint lightCount, uint lightStart, float3 centerWS,
JitteredRay ray, float t0, float t1, float dt, float rndVal, float extinction, float anisotropy)
{
VoxelLighting lighting;
ZERO_INITIALIZE(VoxelLighting, lighting);
BuiltinData unused; // Unused for now, so define once
ZERO_INITIALIZE(BuiltinData, unused);
const float NdotL = 1;
if (featureFlags & LIGHTFEATUREFLAGS_PUNCTUAL)
{
uint lightOffset = 0; // This is used by two subsequent loops
// This loop does not process box lights.
for (; lightOffset < lightCount; lightOffset++)
{
uint lightIndex = FetchIndex(lightStart, lightOffset);
LightData light = _LightDatas[lightIndex];
if (light.lightType >= GPULIGHTTYPE_PROJECTOR_BOX) { break; }
// Prep the light so that it works with non-volumetrics-aware code.
light.contactShadowMask = 0;
light.shadowDimmer = light.volumetricShadowDimmer;
bool sampleLight = true;
float tEntr = t0;
float tExit = t1;
// Perform ray-cone intersection for pyramid and spot lights.
if (light.lightType != GPULIGHTTYPE_POINT)
{
float lenMul = 1;
if (light.lightType == GPULIGHTTYPE_PROJECTOR_PYRAMID)
{
// 'light.right' and 'light.up' vectors are pre-scaled on the CPU
// s.t. if you were to place them at the distance of 1 directly in front
// of the light, they would give you the "footprint" of the light.
// For spot lights, the cone fit is exact.
// For pyramid lights, however, this is the "inscribed" cone
// (contained within the pyramid), and we want to intersect
// the "escribed" cone (which contains the pyramid).
// Therefore, we have to scale the radii by the sqrt(2).
lenMul = rsqrt(2);
}
float3 coneAxisX = lenMul * light.right;
float3 coneAxisY = lenMul * light.up;
sampleLight = IntersectRayCone(ray.originWS, ray.jitterDirWS,
light.positionRWS, light.forward,
coneAxisX, coneAxisY,
t0, t1, tEntr, tExit);
}
// Is it worth evaluating the light?
if (sampleLight)
{
// Each froxel in the VBuffer is curved following a spherical shape around the camera position
// To compute the arc length of the froxel, we can take the arc length of a forxel at 1m from the camera and multiply it by
// the distance to the current froxel.
float voxelArcLength = _HalfVoxelArcLength * tEntr;
// Modify the light radius to ensure that it's at least the size of the froxel, reducing the aliasing close to the center of the light.
light.size.x = max(light.size.x, voxelArcLength);
float t, distSq, rcpPdf;
ImportanceSamplePunctualLight(rndVal, light.positionRWS, light.size.x,
ray.originWS, ray.jitterDirWS,
tEntr, tExit,
t, distSq, rcpPdf);
posInput.positionWS = ray.originWS + t * ray.jitterDirWS;
float3 L;
float4 distances; // {d, d^2, 1/d, d_proj}
GetPunctualLightVectors(posInput.positionWS, light, L, distances);
float4 lightColor = EvaluateLight_Punctual(context, posInput, light, L, distances);
// The volumetric light dimmer, unlike the regular light dimmer, is not pre-multiplied.
lightColor.a *= light.volumetricLightDimmer;
lightColor.rgb *= lightColor.a; // Composite
#if SHADOW_VIEW_BIAS
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
// We fake view bias by invoking the normal bias code with the view direction.
float3 shadowN = -ray.jitterDirWS;
#else
float3 shadowN = 0; // No bias
#endif // SHADOW_VIEW_BIAS
SHADOW_TYPE shadow = EvaluateShadow_Punctual(context, posInput, light, unused, shadowN, L, distances);
lightColor.rgb *= ComputeShadowColor(shadow, light.shadowTint, light.penumbraTint);
// Important:
// Ideally, all scattering calculations should use the jittered versions
// of the sample position and the ray direction. However, correct reprojection
// of asymmetrically scattered lighting (affected by an anisotropic phase
// function) is not possible. We work around this issue by reprojecting
// lighting not affected by the phase function. This basically removes
// the phase function from the temporal integration process. It is a hack.
// The downside is that anisotropy no longer benefits from temporal averaging,
// and any temporal instability of anisotropy causes causes visible jitter.
// In order to stabilize the image, we use the voxel center for all
// anisotropy-related calculations.
float3 centerL = light.positionRWS - centerWS;
float cosTheta = dot(centerL, ray.centerDirWS) * rsqrt(dot(centerL, centerL));
float phase = CornetteShanksPhasePartVarying(anisotropy, cosTheta);
// Compute transmittance from 't0' to 't'.
float weight = TransmittanceHomogeneousMedium(extinction, t - t0) * rcpPdf;
// Compute the amount of in-scattered radiance.
lighting.radianceNoPhase += (weight * lightColor.rgb);
lighting.radianceComplete += (weight * lightColor.rgb) * phase;
}
}
// This loop only processes box lights.
for (; lightOffset < lightCount; lightOffset++)
{
uint lightIndex = FetchIndex(lightStart, lightOffset);
LightData light = _LightDatas[lightIndex];
if (light.lightType != GPULIGHTTYPE_PROJECTOR_BOX) { break; }
// Prep the light so that it works with non-volumetrics-aware code.
light.contactShadowMask = 0;
light.shadowDimmer = light.volumetricShadowDimmer;
bool sampleLight = true;
// Convert the box light from OBB to AABB.
// 'light.right' and 'light.up' vectors are pre-scaled on the CPU by (2/w) and (2/h).
float3x3 rotMat = float3x3(light.right, light.up, light.forward);
float3 o = mul(rotMat, ray.originWS - light.positionRWS);
float3 d = mul(rotMat, ray.jitterDirWS);
float3 boxPt0 = float3(-1, -1, 0);
float3 boxPt1 = float3( 1, 1, light.range);
float tEntr, tExit;
sampleLight = IntersectRayAABB(o, d, boxPt0, boxPt1, t0, t1, tEntr, tExit);
// Is it worth evaluating the light?
if (sampleLight)
{
float tOffset, weight;
ImportanceSampleHomogeneousMedium(rndVal, extinction, tExit - tEntr, tOffset, weight);
// Compute transmittance from 't0' to 'tEntr'.
weight *= TransmittanceHomogeneousMedium(extinction, tEntr - t0);
float t = tEntr + tOffset;
posInput.positionWS = ray.originWS + t * ray.jitterDirWS;
float3 L = -light.forward;
float3 lightToSample = posInput.positionWS - light.positionRWS;
float distProj = dot(lightToSample, light.forward);
float4 distances = float4(1, 1, 1, distProj);
float4 lightColor = EvaluateLight_Punctual(context, posInput, light, L, distances);
// The volumetric light dimmer, unlike the regular light dimmer, is not pre-multiplied.
lightColor.a *= light.volumetricLightDimmer;
lightColor.rgb *= lightColor.a; // Composite
#if SHADOW_VIEW_BIAS
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
// We fake view bias by invoking the normal bias code with the view direction.
float3 shadowN = -ray.jitterDirWS;
#else
float3 shadowN = 0; // No bias
#endif // SHADOW_VIEW_BIAS
SHADOW_TYPE shadow = EvaluateShadow_Punctual(context, posInput, light, unused, shadowN, L, distances);
lightColor.rgb *= ComputeShadowColor(shadow, light.shadowTint, light.penumbraTint);
// Important:
// Ideally, all scattering calculations should use the jittered versions
// of the sample position and the ray direction. However, correct reprojection
// of asymmetrically scattered lighting (affected by an anisotropic phase
// function) is not possible. We work around this issue by reprojecting
// lighting not affected by the phase function. This basically removes
// the phase function from the temporal integration process. It is a hack.
// The downside is that anisotropy no longer benefits from temporal averaging,
// and any temporal instability of anisotropy causes causes visible jitter.
// In order to stabilize the image, we use the voxel center for all
// anisotropy-related calculations.
float3 centerL = light.positionRWS - centerWS;
float cosTheta = dot(centerL, ray.centerDirWS) * rsqrt(dot(centerL, centerL));
float phase = CornetteShanksPhasePartVarying(anisotropy, cosTheta);
// Compute the amount of in-scattered radiance.
// Note: the 'weight' accounts for transmittance from 't0' to 't'.
lighting.radianceNoPhase += (weight * lightColor.rgb);
lighting.radianceComplete += (weight * lightColor.rgb) * phase;
}
}
}
return lighting;
}
float3 EvaluateVoxelDiffuseGI(PositionInputs posInput, JitteredRay ray, float t0, float dt, float rndVal, float extinction)
{
#if SAMPLE_PROBE_VOLUMES
float tOffset, weight;
ImportanceSampleHomogeneousMedium(rndVal, extinction, dt, tOffset, weight);
float t = t0 + tOffset;
posInput.positionWS = ray.originWS + t * ray.jitterDirWS;
float3 apvDiffuseGI;
// Note here we don't need undo the convolution by the cosine kernel as we only sample L0 and don't support anisotropy yet, so undoing cosine and convolve with phase function
// for L0 term it'd just cancel out as both are just constant factors.
EvaluateAdaptiveProbeVolume(GetAbsolutePositionWS(posInput.positionWS), posInput.positionSS, apvDiffuseGI);
const float cornetteShanksZonalHarmonicL0 = sqrt(4.0f * PI);
weight *= PI * cornetteShanksZonalHarmonicL0;
// It is possible that some invalid probes are sampled due to how APV is laying the probes
// For now the safest way is to ignore the NaN data when sampled.
if (AnyIsNaN(apvDiffuseGI))
{
// We don't early out here as otherwise the compiler issues warnings on some platforms.
weight = 0;
}
return apvDiffuseGI * weight * _FogGIDimmer;
#else
return 0;
#endif
}
// Computes the in-scattered radiance along the ray.
void FillVolumetricLightingBuffer(LightLoopContext context, uint featureFlags,
PositionInputs posInput, uint tileIndex, int groupIdx, JitteredRay ray, float tStart)
{
uint lightCount, lightStart;
#ifdef USE_BIG_TILE_LIGHTLIST
// Offset for stereo rendering
tileIndex += unity_StereoEyeIndex * _NumTileBigTileX * _NumTileBigTileY;
GetBigTileLightCountAndStart(tileIndex, lightCount, lightStart);
#else // USE_BIG_TILE_LIGHTLIST
lightCount = _PunctualLightCount;
lightStart = 0;
#endif // USE_BIG_TILE_LIGHTLIST
float t0 = max(tStart, DecodeLogarithmicDepthGeneralized(0, _VBufferDistanceDecodingParams));
float de = _VBufferRcpSliceCount; // Log-encoded distance between slices
// The contribution of the ambient probe does not depend on the position,
// only on the direction and the length of the interval.
// SampleSH9() evaluates the 3-band SH in a given direction.
// The probe is already pre-convolved with the phase function.
// Note: anisotropic, no jittering.
float3 probeInScatteredRadiance = EvaluateVolumetricAmbientProbe(ray.centerDirWS);
float waterDistance = FLT_MIN;
#if SUPPORT_WATER_ABSORPTION
float2 posSS = posInput.positionNDC.xy * _ScreenSize.xy;
bool underWater = posInput.positionWS.y < _UnderWaterUpHeight.w || IsUnderWater(posSS.xy);
if (underWater && (GetStencilValue(LOAD_TEXTURE2D_X(_StencilTexture, posInput.positionSS.xy)) & STENCILUSAGE_WATER_SURFACE) != 0)
{
waterDistance = LinearEyeDepth(LOAD_TEXTURE2D_X(_RefractiveDepthBuffer, posSS).r, _ZBufferParams);
waterDistance = waterDistance * rcp(dot(ray.centerDirWS, GetViewForwardDir()));
}
else
waterDistance = underWater ? FLT_MAX : 0.0f;
#endif
float3 totalRadiance = 0;
float opticalDepth = 0;
uint slice = 0;
for (; slice < _VBufferSliceCount; slice++)
{
uint3 voxelCoord = uint3(posInput.positionSS, slice + _VBufferSliceCount * unity_StereoEyeIndex);
float e1 = slice * de + de; // (slice + 1) / sliceCount
float t1 = max(tStart, DecodeLogarithmicDepthGeneralized(e1, _VBufferDistanceDecodingParams));
float tNext = t1;
#if USE_DEPTH_BUFFER
bool containsOpaqueGeometry = IsInRange(ray.geomDist, float2(t0, t1));
if (containsOpaqueGeometry)
{
// Only integrate up to the opaque surface (make the voxel shorter, but not completely flat).
// Note that we can NOT completely stop integrating when the ray reaches geometry, since
// otherwise we get flickering at geometric discontinuities if reprojection is enabled.
// In this case, a temporally stable light leak is better than flickering.
t1 = max(t0 * 1.0001, ray.geomDist);
}
#endif
float dt = t1 - t0; // Is geometry-aware
if(dt <= 0.0)
{
_VBufferLighting[voxelCoord] = 0;
#ifdef ENABLE_REPROJECTION
_VBufferFeedback[voxelCoord] = 0;
#endif
t0 = t1;
continue;
}
// Accurately compute the center of the voxel in the log space. It's important to perform
// the inversion exactly, since the accumulated value of the integral is stored at the center.
// We will use it for participating media sampling, asymmetric scattering and reprojection.
float t = DecodeLogarithmicDepthGeneralized(e1 - 0.5 * de, _VBufferDistanceDecodingParams);
float3 centerWS = ray.originWS + t * ray.centerDirWS;
// Sample the participating medium at the center of the voxel.
// We consider it to be constant along the interval [t0, t1] (within the voxel).
float4 density = LOAD_TEXTURE3D(_VBufferDensity, voxelCoord);
float3 scattering = density.rgb;
float extinction = density.a;
float anisotropy = _GlobalFogAnisotropy;
// Perform per-pixel randomization by adding an offset and then sampling uniformly
// (in the log space) in a vein similar to Stochastic Universal Sampling:
// https://en.wikipedia.org/wiki/Stochastic_universal_sampling
float perPixelRandomOffset = GenerateHashedRandomFloat(posInput.positionSS);
#ifdef ENABLE_REPROJECTION
// This is a time-based sequence of 7 equidistant numbers from 1/14 to 13/14.
// Each of them is the centroid of the interval of length 2/14.
float rndVal = frac(perPixelRandomOffset + _VBufferSampleOffset.z);
#else
float rndVal = frac(perPixelRandomOffset + 0.5);
#endif
VoxelLighting aggregateLighting;
ZERO_INITIALIZE(VoxelLighting, aggregateLighting);
// Prevent division by 0.
extinction = max(extinction, FLT_MIN);
if (featureFlags & LIGHTFEATUREFLAGS_DIRECTIONAL)
{
VoxelLighting lighting = EvaluateVoxelLightingDirectional(context, featureFlags, posInput,
centerWS, ray, t0, t1, dt, rndVal,
extinction, anisotropy, t < waterDistance);
aggregateLighting.radianceNoPhase += lighting.radianceNoPhase;
aggregateLighting.radianceComplete += lighting.radianceComplete;
}
#ifdef SUPPORT_LOCAL_LIGHTS
{
VoxelLighting lighting = EvaluateVoxelLightingLocal(context, groupIdx, featureFlags, posInput,
lightCount, lightStart,
centerWS, ray, t0, t1, dt, rndVal,
extinction, anisotropy);
aggregateLighting.radianceNoPhase += lighting.radianceNoPhase;
aggregateLighting.radianceComplete += lighting.radianceComplete;
}
#endif
#if SAMPLE_PROBE_VOLUMES
{
float3 apvDiffuseGI = EvaluateVoxelDiffuseGI(posInput, ray, t0, dt, rndVal, extinction);
aggregateLighting.radianceNoPhase += apvDiffuseGI;
aggregateLighting.radianceComplete += apvDiffuseGI;
}
#endif
#ifdef ENABLE_REPROJECTION
// Clamp here to prevent generation of NaNs.
float4 voxelValue = float4(aggregateLighting.radianceNoPhase, extinction * dt);
float4 linearizedVoxelValue = LinearizeRGBD(voxelValue);
float4 normalizedVoxelValue = linearizedVoxelValue * rcp(dt);
float4 normalizedBlendValue = normalizedVoxelValue;
#if (SHADEROPTIONS_CAMERA_RELATIVE_RENDERING != 0) && defined(USING_STEREO_MATRICES)
// With XR single-pass, remove the camera-relative offset for the reprojected sample
centerWS -= _WorldSpaceCameraPosViewOffset;
#endif
// Reproject the history at 'centerWS'.
float4 reprojValue = SampleVBuffer(TEXTURE3D_ARGS(_VBufferHistory, s_linear_clamp_sampler),
centerWS,
_PrevCamPosRWS.xyz,
UNITY_MATRIX_PREV_VP,
_VBufferPrevViewportSize,
_VBufferHistoryViewportScale.xyz,
_VBufferHistoryViewportLimit.xyz,
_VBufferPrevDistanceEncodingParams,
_VBufferPrevDistanceDecodingParams,
false, false, true) * float4(GetInversePreviousExposureMultiplier().xxx, 1);
bool reprojSuccess = (_VBufferHistoryIsValid != 0) && (reprojValue.a != 0);
if (reprojSuccess)
{
// Perform temporal blending in the log space ("Pixar blend").
normalizedBlendValue = lerp(normalizedVoxelValue, reprojValue, ComputeHistoryWeight());
}
// Store the feedback for the voxel.
// TODO: dynamic lights (which update their position, rotation, cookie or shadow at runtime)
// do not support reprojection and should neither read nor write to the history buffer.
// This will cause them to alias, but it is the only way to prevent ghosting.
_VBufferFeedback[voxelCoord] = clamp(normalizedBlendValue * float4(GetCurrentExposureMultiplier().xxx, 1), 0, HALF_MAX);
float4 linearizedBlendValue = normalizedBlendValue * dt;
float4 blendValue = DelinearizeRGBD(linearizedBlendValue);
#ifdef ENABLE_ANISOTROPY
// Estimate the influence of the phase function on the results of the current frame.
float3 phaseCurrFrame;
phaseCurrFrame.r = SafeDiv(aggregateLighting.radianceComplete.r, aggregateLighting.radianceNoPhase.r);
phaseCurrFrame.g = SafeDiv(aggregateLighting.radianceComplete.g, aggregateLighting.radianceNoPhase.g);
phaseCurrFrame.b = SafeDiv(aggregateLighting.radianceComplete.b, aggregateLighting.radianceNoPhase.b);
// Warning: in general, this does not work!
// For a voxel with a single light, 'phaseCurrFrame' is monochromatic, and since
// we don't jitter anisotropy, its value does not change from frame to frame
// for a static camera/scene. This is fine.
// If you have two lights per voxel, we compute:
// phaseCurrFrame = (phaseA * lightingA + phaseB * lightingB) / (lightingA + lightingB).
// 'phaseA' and 'phaseB' are still (different) constants for a static camera/scene.
// 'lightingA' and 'lightingB' are jittered, so they change from frame to frame.
// Therefore, 'phaseCurrFrame' becomes temporarily unstable and can cause flickering in practice. :-(
blendValue.rgb *= phaseCurrFrame;
#endif // ENABLE_ANISOTROPY
#else // NO REPROJECTION
#ifdef ENABLE_ANISOTROPY
float4 blendValue = float4(aggregateLighting.radianceComplete, extinction * dt);
#else
float4 blendValue = float4(aggregateLighting.radianceNoPhase, extinction * dt);
#endif // ENABLE_ANISOTROPY
#endif // ENABLE_REPROJECTION
// Compute the transmittance from the camera to 't0'.
float transmittance = TransmittanceFromOpticalDepth(opticalDepth);
#ifdef ENABLE_ANISOTROPY
float phase = _CornetteShanksConstant;
#else
float phase = IsotropicPhaseFunction();
#endif // ENABLE_ANISOTROPY
// Integrate the contribution of the probe over the interval.
// Integral{a, b}{Transmittance(0, t) * L_s(t) dt} = Transmittance(0, a) * Integral{a, b}{Transmittance(0, t - a) * L_s(t) dt}.
float3 probeRadiance = probeInScatteredRadiance * TransmittanceIntegralHomogeneousMedium(extinction, dt);
// Accumulate radiance along the ray.
totalRadiance += transmittance * scattering * (phase * blendValue.rgb + probeRadiance);
// Compute the optical depth up to the center of the interval.
opticalDepth += 0.5 * blendValue.a;
// Store the voxel data.
// Note: for correct filtering, the data has to be stored in the perceptual space.
// This means storing the tone mapped radiance and transmittance instead of optical depth.
// See "A Fresh Look at Generalized Sampling", p. 51.
// TODO: re-enable tone mapping after implementing pre-exposure.
_VBufferLighting[voxelCoord] = max(0, LinearizeRGBD(float4(/*FastTonemap*/(totalRadiance), opticalDepth)) * float4(GetCurrentExposureMultiplier().xxx, 1));
// Compute the optical depth up to the end of the interval.
opticalDepth += 0.5 * blendValue.a;
if (t0 * 0.99 > ray.maxDist)
{
break;
}
t0 = tNext;
}
for (; slice < _VBufferSliceCount; slice++)
{
uint3 voxelCoord = uint3(posInput.positionSS, slice + _VBufferSliceCount * unity_StereoEyeIndex);
_VBufferLighting[voxelCoord] = 0;
#ifdef ENABLE_REPROJECTION
_VBufferFeedback[voxelCoord] = 0;
#endif
}
}
[numthreads(GROUP_SIZE_1D, GROUP_SIZE_1D, 1)]
void VolumetricLighting(uint3 dispatchThreadId : SV_DispatchThreadID,
uint2 groupId : SV_GroupID,
uint2 groupThreadId : SV_GroupThreadID,
int groupIndex : SV_GroupIndex)
{
UNITY_XR_ASSIGN_VIEW_INDEX(dispatchThreadId.z);
uint2 groupOffset = groupId * GROUP_SIZE_1D;
uint2 voxelCoord = groupOffset + groupThreadId;
// Reminder: our voxels are sphere-capped right frustums (truncated right pyramids).
// The curvature of the front and back faces is quite gentle, so we can use
// the right frustum approximation (thus the front and the back faces are squares).
// Note, that since we still rely on the perspective camera model, pixels at the center
// of the screen correspond to larger solid angles than those at the edges.
// Basically, sizes of front and back faces depend on the XY coordinate.
// https://www.desmos.com/calculator/i3rkesvidk
float3 F = GetViewForwardDir();
float3 U = GetViewUpDir();
float3 R = cross(F, U);
float2 centerCoord = voxelCoord + float2(0.5, 0.5);
// Compute a ray direction s.t. ViewSpace(rayDirWS).z = 1.
float3 rayDirWS = mul(-float4(centerCoord, 1, 1), _VBufferCoordToViewDirWS[unity_StereoEyeIndex]).xyz;
float3 rightDirWS = cross(rayDirWS, U);
float rcpLenRayDir = rsqrt(dot(rayDirWS, rayDirWS));
float rcpLenRightDir = rsqrt(dot(rightDirWS, rightDirWS));
JitteredRay ray;
ray.originWS = GetCurrentViewPosition();
ray.centerDirWS = rayDirWS * rcpLenRayDir; // Normalize
float FdotD = dot(F, ray.centerDirWS);
float unitDistFaceSize = _VBufferUnitDepthTexelSpacing * FdotD * rcpLenRayDir;
ray.xDirDerivWS = rightDirWS * (rcpLenRightDir * unitDistFaceSize); // Normalize & rescale
ray.yDirDerivWS = cross(ray.xDirDerivWS, ray.centerDirWS); // Will have the length of 'unitDistFaceSize' by construction
#ifdef ENABLE_REPROJECTION
float2 sampleOffset = _VBufferSampleOffset.xy;
#else
float2 sampleOffset = 0;
#endif
ray.jitterDirWS = normalize(ray.centerDirWS + sampleOffset.x * ray.xDirDerivWS
+ sampleOffset.y * ray.yDirDerivWS);
float tStart = g_fNearPlane / dot(ray.jitterDirWS, F);
// We would like to determine the screen pixel (at the full resolution) which
// the jittered ray corresponds to. The exact solution can be obtained by intersecting
// the ray with the screen plane, e.i. (ViewSpace(jitterDirWS).z = 1). That's a little expensive.
// So, as an approximation, we ignore the curvature of the frustum.
uint2 pixelCoord = (uint2)((voxelCoord + 0.5 + sampleOffset) * _VBufferVoxelSize);
#ifdef VL_PRESET_OPTIMAL
// The entire thread group is within the same light tile.
uint2 tileCoord = groupOffset * VBUFFER_VOXEL_SIZE / TILE_SIZE_BIG_TILE;
#else
// No compile-time optimizations, no scalarization.
uint2 tileCoord = pixelCoord / TILE_SIZE_BIG_TILE;
#endif
uint tileIndex = tileCoord.x + _NumTileBigTileX * tileCoord.y;
// This clamp is important as _VBufferVoxelSize can have float value which can cause en overflow (Crash on Vulkan and Metal)
tileIndex = min(tileIndex, _NumTileBigTileX * _NumTileBigTileY);
// Do not jitter 'voxelCoord' else. It's expected to correspond to the center of the voxel.
PositionInputs posInput = GetPositionInput(voxelCoord, _VBufferViewportSize.zw, tileCoord);
ray.geomDist = FLT_INF;
ray.maxDist = FLT_INF;
#if USE_DEPTH_BUFFER
float deviceDepth = LoadCameraDepth(pixelCoord);
if (deviceDepth > 0) // Skip the skybox
{
// Convert it to distance along the ray. Doesn't work with tilt shift, etc.
float linearDepth = LinearEyeDepth(deviceDepth, _ZBufferParams);
ray.geomDist = linearDepth * rcp(dot(ray.jitterDirWS, F));
float2 UV = posInput.positionNDC * _RTHandleScale.xy;
// This should really be using a max sampler here. This is a bit overdilating given that it is already dilated.
// Better to be safer though.
float4 d = GATHER_RED_TEXTURE2D_X(_MaxZMaskTexture, s_point_clamp_sampler, UV) * rcp(dot(ray.jitterDirWS, F));
ray.maxDist = max(Max3(d.x, d.y, d.z), d.w);
}
#endif
// TODO
LightLoopContext context;
context.shadowContext = InitShadowContext();
uint featureFlags = 0xFFFFFFFF;
ApplyCameraRelativeXR(ray.originWS);
FillVolumetricLightingBuffer(context, featureFlags, posInput, tileIndex, groupIndex, ray, tStart);
}