Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: physically based water scattering #291

Merged
merged 6 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 111 additions & 49 deletions features/Skylighting/Shaders/Skylighting/Skylighting.hlsli
Original file line number Diff line number Diff line change
Expand Up @@ -20,75 +20,137 @@ Texture2D<float> SkylightingTexture : register(t82);

#if defined(WATER) || defined(EFFECT)

float3 GetVL(float3 worldPosition, float3 viewPosition, float rawDepth, float depth, float2 screenPosition)
float3 GetShadow(float3 positionWS)
{
const static uint nSteps = 16;
const static float step = rcp(nSteps);

uint noiseIdx = dot(screenPosition, float2(1, lightingData[0].BufferDim.x)) + lightingData[0].Timer * 1000;
float noise = frac(0.5 + noiseIdx * 0.38196601125);

float3 endPointWS = worldPosition * depth / viewPosition.z;

PerGeometry sD = perShadow[0];
sD.EndSplitDistances.x = GetScreenDepth(sD.EndSplitDistances.x);
sD.EndSplitDistances.y = GetScreenDepth(sD.EndSplitDistances.y);
sD.EndSplitDistances.z = GetScreenDepth(sD.EndSplitDistances.z);
sD.EndSplitDistances.w = GetScreenDepth(sD.EndSplitDistances.w);

float3 volumetric = 0.0;
for (int i = 0; i < nSteps; ++i) {
float3 samplePositionWS = lerp(worldPosition, endPointWS, (i + noise) * step);
float shadowMapDepth = length(samplePositionWS.xyz);

half cascadeIndex = 0;
half4x3 lightProjectionMatrix = sD.ShadowMapProj[0];
half shadowMapThreshold = sD.AlphaTestRef.y;

[flatten] if (2.5 < sD.EndSplitDistances.w && sD.EndSplitDistances.y < shadowMapDepth)
{
lightProjectionMatrix = sD.ShadowMapProj[2];
shadowMapThreshold = sD.AlphaTestRef.z;
cascadeIndex = 2;
}
else if (sD.EndSplitDistances.x < shadowMapDepth)
{
lightProjectionMatrix = sD.ShadowMapProj[1];
shadowMapThreshold = sD.AlphaTestRef.z;
cascadeIndex = 1;
}
float shadowMapDepth = length(positionWS.xyz);

half3 samplePositionLS = mul(transpose(lightProjectionMatrix), half4(samplePositionWS.xyz, 1)).xyz;
float sampleShadowDepth = TexShadowMapSampler.SampleLevel(LinearSampler, half3(samplePositionLS.xy, cascadeIndex), 0);
half cascadeIndex = 0;
half4x3 lightProjectionMatrix = sD.ShadowMapProj[0];
half shadowMapThreshold = sD.AlphaTestRef.y;

if (sampleShadowDepth > samplePositionLS.z - shadowMapThreshold)
volumetric += 1;
[flatten] if (2.5 < sD.EndSplitDistances.w && sD.EndSplitDistances.y < shadowMapDepth)
{
lightProjectionMatrix = sD.ShadowMapProj[2];
shadowMapThreshold = sD.AlphaTestRef.z;
cascadeIndex = 2;
}
else if (sD.EndSplitDistances.x < shadowMapDepth)
{
lightProjectionMatrix = sD.ShadowMapProj[1];
shadowMapThreshold = sD.AlphaTestRef.z;
cascadeIndex = 1;
}

half3 positionLS = mul(transpose(lightProjectionMatrix), half4(positionWS.xyz, 1)).xyz;
float deltaZ = positionLS.z - shadowMapThreshold;

volumetric /= 16.0;
float4 depths = TexShadowMapSampler.GatherRed(LinearSampler, half3(positionLS.xy, cascadeIndex), 0);

return volumetric;
float shadow = dot(depths > deltaZ, 0.25);

return shadow;
}

float3 GetShadow(float3 positionWS)
float phaseHenyeyGreenstein(float cosTheta, float g)
{
PerGeometry sD = perShadow[0];
static const float scale = .25 / 3.1415926535;
const float g2 = g * g;

half cascadeIndex = 0;
half4x3 lightProjectionMatrix = sD.ShadowMapProj[0];
half shadowMapThreshold = sD.AlphaTestRef.y;
float num = (1.0 - g2);
float denom = pow(abs(1.0 + g2 - 2.0 * g * cosTheta), 1.5);

lightProjectionMatrix = sD.ShadowMapProj[1];
shadowMapThreshold = sD.AlphaTestRef.z;
cascadeIndex = 1;
return scale * num / denom;
}

half3 positionLS = mul(transpose(lightProjectionMatrix), half4(positionWS.xyz, 1)).xyz;
void GetVL(float3 startPosWS, float3 endPosWS, float2 screenPosition, out float3 scatter, out float3 transmittance)
{
const static uint nSteps = 16;
const static float step = rcp(nSteps);

float depth = TexShadowMapSampler.SampleLevel(LinearSampler, half3(positionLS.xy, cascadeIndex), 0);
// https://s.campbellsci.com/documents/es/technical-papers/obs_light_absorption.pdf
// clear water: 0.002 cm^-1 * 1.428 cm/game unit
float3 hue = normalize(rcp(ShallowColor));
const float3 scatterCoeff = hue * 0.002 * 2 * 1.428;
const float3 absorpCoeff = hue * 0.0002 * 2 * 1.428;
const float3 extinction = scatterCoeff + absorpCoeff;

float3 worldDir = endPosWS - startPosWS;
float dist = length(worldDir);
worldDir = worldDir / dist;
// float phase = .25 / 3.1415926535;
float phase = phaseHenyeyGreenstein(dot(SunDir.xyz, worldDir), 0.5);
float distRatio = abs(SunDir.z / worldDir.z);

uint noiseIdx = dot(screenPosition, float2(1, lightingData[0].BufferDim.x)) + lightingData[0].Timer * 100;
float noise = frac(0.5 + noiseIdx * 0.38196601125);

const float cutoffTransmittance = 1e-2; // don't go deeper than this
# if defined(UNDERWATER)
const float cutoffDist = -log(cutoffTransmittance) / max(extinction.x, max(extinction.y, extinction.z));
# else
const float cutoffDist = -log(cutoffTransmittance) / ((1 + distRatio) * max(extinction.x, max(extinction.y, extinction.z)));
# endif

float marchDist = min(dist, cutoffDist);
float sunMarchDist = marchDist * distRatio;

if (depth > positionLS.z - shadowMapThreshold)
return 1;
PerGeometry sD = perShadow[0];
sD.EndSplitDistances.x = GetScreenDepth(sD.EndSplitDistances.x);
sD.EndSplitDistances.y = GetScreenDepth(sD.EndSplitDistances.y);
sD.EndSplitDistances.z = GetScreenDepth(sD.EndSplitDistances.z);
sD.EndSplitDistances.w = GetScreenDepth(sD.EndSplitDistances.w);

scatter = 0;
transmittance = 1;
for (uint i = 0; i < nSteps; ++i) {
float t = saturate((i + noise) * step);

float sampleTransmittance = exp(-step * marchDist * extinction);
transmittance *= sampleTransmittance;

// scattering
float shadow = 0;
{
float3 samplePositionWS = startPosWS + worldDir * t * marchDist;
float shadowMapDepth = length(samplePositionWS.xyz);

half cascadeIndex = 0;
half4x3 lightProjectionMatrix = sD.ShadowMapProj[0];
half shadowMapThreshold = sD.AlphaTestRef.y;

[flatten] if (2.5 < sD.EndSplitDistances.w && sD.EndSplitDistances.y < shadowMapDepth)
{
lightProjectionMatrix = sD.ShadowMapProj[2];
shadowMapThreshold = sD.AlphaTestRef.z;
cascadeIndex = 2;
}
else if (sD.EndSplitDistances.x < shadowMapDepth)
{
lightProjectionMatrix = sD.ShadowMapProj[1];
shadowMapThreshold = sD.AlphaTestRef.z;
cascadeIndex = 1;
}

half3 samplePositionLS = mul(transpose(lightProjectionMatrix), half4(samplePositionWS.xyz, 1)).xyz;
float deltaZ = samplePositionLS.z - shadowMapThreshold;

float4 depths = TexShadowMapSampler.GatherRed(LinearSampler, half3(samplePositionLS.xy, cascadeIndex), 0);

shadow = dot(depths > deltaZ, 0.25);
}

float sunTransmittance = exp(-sunMarchDist * t * extinction) * shadow; // assuming water surface is always level
float inScatter = scatterCoeff * phase * sunTransmittance;
scatter += inScatter * (1 - sampleTransmittance) / extinction * transmittance;
}

return 0;
scatter *= SunColor * 3;
transmittance = exp(-dist * (1 + distRatio) * extinction);
}
#endif
44 changes: 26 additions & 18 deletions package/Shaders/Water.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -578,9 +578,9 @@ float GetScreenDepthWater(float2 screenPosition, uint a_useVR = 0)
{
float depth = DepthTex.Load(float3(screenPosition, 0)).x;
return
# if defined(VR) // VR appears to use hard coded values
# if defined(VR) // VR appears to use hard coded values
a_useVR ? depth * 1.01 + -0.01 :
# endif
# endif
(CameraData.w / (-depth * CameraData.z + CameraData.x));
}
//# endif
Expand All @@ -607,17 +607,17 @@ float GetFresnelValue(float3 normal, float3 viewDirection)
return (1 - FresnelRI.x) * pow(viewAngle, 5) + FresnelRI.x;
}

# if defined(SKYLIGHTING)
# define LinearSampler Normals01Sampler
# include "Skylighting/Skylighting.hlsli"
# endif
# if defined(SKYLIGHTING)
# define LinearSampler Normals01Sampler
# include "Skylighting/Skylighting.hlsli"
# endif

# if defined(WATER_CAUSTICS)
float3 GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDirection,
float4 distanceMul, float refractionsDepthFactor, float fresnel, float3 caustics, uint a_eyeIndex, float3 viewPosition, inout float vl)
float4 distanceMul, float refractionsDepthFactor, float fresnel, float3 caustics, uint a_eyeIndex, float3 viewPosition, inout float3 scatter, inout float3 transmittance)
# else
float3 GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDirection,
float4 distanceMul, float refractionsDepthFactor, float fresnel, uint a_eyeIndex, float3 viewPosition, inout float vl)
float4 distanceMul, float refractionsDepthFactor, float fresnel, uint a_eyeIndex, float3 viewPosition, inout float3 scatter, inout float3 transmittance)
# endif
{
# if defined(REFRACTIONS)
Expand Down Expand Up @@ -653,10 +653,14 @@ float3 GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDirection,
float3 refractionColor = RefractionTex.Sample(RefractionSampler, refractionUV).xyz;
float3 refractionDiffuseColor = lerp(ShallowColor.xyz, DeepColor.xyz, distanceMul.y);

# if defined(SKYLIGHTING)
vl = GetVL(input.WPosition, viewPosition, DepthTex.Load(float3(screenPosition, 0)).x, depth, screenPosition);
refractionDiffuseColor = refractionDiffuseColor * lerp(vl, 1.0, 0.25);
# endif
# ifdef SKYLIGHTING
float4 refractionWorldPosition = mul(
CameraViewProjInverse[a_eyeIndex],
float4((refractionUV * 2 - 1) * float2(1, -1), DepthTex.Load(float3(refractionScreenPosition, 0)).x, 1));
refractionWorldPosition.xyz /= refractionWorldPosition.w;
// float3 refractionWorldPosition = input.WPosition.xyz * depth / viewPosition.z; // this is without refraction
GetVL(input.WPosition.xyz, refractionWorldPosition.xyz, screenPosition, scatter, transmittance);
# endif

# if defined(UNDERWATER)
float refractionMul = 0;
Expand All @@ -671,6 +675,7 @@ float3 GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDirection,
# endif

return lerp(refractionColor * WaterParams.w, refractionDiffuseColor, refractionMul);
// return refractionColor * transmittance; // this is physically accurate
# else
return lerp(ShallowColor.xyz, DeepColor.xyz, fresnel) * GetLdotN(normal);
# endif
Expand Down Expand Up @@ -787,18 +792,18 @@ PS_OUTPUT main(PS_INPUT input)
isSpecular = true;
# else

float vl = 1.0;

float3 specularColor = GetWaterSpecularColor(input, normal, viewDirection, distanceFactor, depthControl.y, eyeIndex);

float3 scatter = 0;
float3 transmittance = 1;
# if defined(WATER_CAUSTICS)
float3 diffuseColor = GetWaterDiffuseColor(input, normal, viewDirection, distanceMul, depthControl.y, fresnel, caustics, eyeIndex, viewPosition, vl);
float3 diffuseColor = GetWaterDiffuseColor(input, normal, viewDirection, distanceMul, depthControl.y, fresnel, caustics, eyeIndex, viewPosition, scatter, transmittance);
# else
float3 diffuseColor = GetWaterDiffuseColor(input, normal, viewDirection, distanceMul, depthControl.y, fresnel, eyeIndex, viewPosition, vl);
float3 diffuseColor = GetWaterDiffuseColor(input, normal, viewDirection, distanceMul, depthControl.y, fresnel, eyeIndex, viewPosition, scatter, transmittance);
# endif

float3 specularLighting = 0;


# if defined(LIGHT_LIMIT_FIX)
uint lightCount = 0;

Expand Down Expand Up @@ -835,9 +840,12 @@ PS_OUTPUT main(PS_INPUT input)
((1 - fresnel) * (diffuseColor - finalSpecularColor)) +
finalSpecularColor;
# else
float3 sunColor = GetSunColor(normal, viewDirection) * vl;
float3 sunColor = GetSunColor(normal, viewDirection);
float specularFraction = lerp(1, fresnel * depthControl.x, distanceFactor);
float3 finalColorPreFog = lerp(diffuseColor, specularColor, specularFraction) + sunColor * depthControl.w;
# if defined(SKYLIGHTING)
finalColorPreFog += scatter;
# endif
float3 finalColor = lerp(finalColorPreFog, input.FogParam.xyz, input.FogParam.w);
# endif
# endif
Expand Down
Loading