Skip to content

Commit 256a385

Browse files
authored
feat(sss): add burley normalized sss (#1393)
1 parent 5c8f17f commit 256a385

File tree

5 files changed

+288
-52
lines changed

5 files changed

+288
-52
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#include "Common/GBuffer.hlsli"
2+
#include "Common/Game.hlsli"
3+
#include "Common/SharedData.hlsli"
4+
#include "Common/Random.hlsli"
5+
#include "Common/Math.hlsli"
6+
7+
// [Per H. Christensen, Brent Burley 2015, "Approximate Reflectance Profiles for Efficient Subsurface Scattering"]
8+
// https://graphics.pixar.com/library/ApproxBSSRDF/paper.pdf
9+
float3 GetBurleyCDF(float3 d, float3 r, float rand)
10+
{
11+
return 1 - 0.25 * exp(-r / d) - 0.75 * exp(-r / (3 * d)) - rand;
12+
}
13+
14+
float GetBurleyPDF(float r, float l, float s)
15+
{
16+
float d = l / s;
17+
float pdf = 0.25 / d * (exp(-r / d) + exp(-r / (3 * d))); // cdf dr
18+
return max(pdf, 1e-5f);
19+
}
20+
21+
// [Tiantian Xie et al. 2020, "Real-time subsurface scattering with single pass variance-guided adaptive importance sampling"]
22+
// https://thisistian.github.io/publication/spvg_xie_2020_I3D_small.pdf
23+
// Also check https://zero-radiance.github.io/post/sampling-diffusion/
24+
float RadiusApprox(float d, float rand)
25+
{
26+
// g(ξ) = d((2 − c)ξ − 2)log(1 − ξ)
27+
// minimal mean squared error when c = 2.5715
28+
return d * ((2 - 2.5715f) * rand - 2) * log(1 - rand);
29+
}
30+
31+
float3 GetBurleyProfile(float3 l, float3 s, float radius)
32+
{
33+
// R(d,r) = \frac{e^{-r/d}+e^{-r/(3d)}}{8*pi*d*r}
34+
float3 d = 1.f / s;
35+
float3 r = radius / l;
36+
float3 negRbyD = -r / d;
37+
return max((exp(negRbyD) + exp(negRbyD / 3.0f)) / (d * l * 8 * Math::PI), 1e-12f);
38+
}
39+
40+
float3 GetScalingFactor(float3 albedo)
41+
{
42+
// we have three methods for calculating the scaling factor
43+
// d = l / (1.85 − A + 7|A − 0.8|^3)
44+
// d = l / (1.9 − A + 3.5(A − 0.8)^2)
45+
// d = l / (3.5 + 100(A − 0.33)^4)
46+
// here we choose the third to use diffuse mean free path as parameter.
47+
float3 value = albedo - 0.33f;
48+
return 3.5f + 100.f * pow(abs(value), 4);
49+
}
50+
51+
float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssAmount, bool humanProfile)
52+
{
53+
float centerDepth = SharedData::GetScreenDepth(DepthTexture[DTid].x);
54+
55+
float4 centerColor = ColorTexture[DTid];
56+
if (sssAmount == 0 || centerDepth <= 0)
57+
{
58+
return centerColor;
59+
}
60+
61+
float4 surfaceAlbedo = AlbedoTexture[DTid];
62+
float3 originalColor = Color::GammaToLinear(centerColor.xyz / max(surfaceAlbedo.xyz, EPSILON_SSS_ALBEDO));
63+
64+
float4 diffuseMeanFreePath = humanProfile ? MeanFreePathHuman : MeanFreePathBase;
65+
diffuseMeanFreePath.xyz = float3(max(diffuseMeanFreePath.x, 1e-5f), max(diffuseMeanFreePath.y, 1e-5f), max(diffuseMeanFreePath.z, 1e-5f));
66+
diffuseMeanFreePath *= sssAmount;
67+
68+
float dmfpForSampling = diffuseMeanFreePath.w;
69+
float s = GetScalingFactor(surfaceAlbedo.www).x;
70+
float d = dmfpForSampling / s;
71+
float3 s3d = GetScalingFactor(surfaceAlbedo.xyz);
72+
float3 d3d = diffuseMeanFreePath.xyz * dmfpForSampling / s3d;
73+
74+
const float3 normalVS = GBuffer::DecodeNormal(NormalTexture[DTid].xy);
75+
const float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(normalVS, 0)).xyz);
76+
77+
float3 weightSum = 0.0f;
78+
float3 colorSum = 0.0f;
79+
80+
float2 uvScale = (GAME_UNIT_TO_CM * 0.1f * (0.5f / tan(0.5 * radians(SSSS_FOVY)))) / centerDepth; // Scale in mm
81+
82+
// center sample weight
83+
float centerRadius = 0.5f * (SharedData::BufferDim.z / uvScale.x + SharedData::BufferDim.w / uvScale.y);
84+
float centerRadiusCDF = GetBurleyCDF(d, centerRadius, 0).x;
85+
float3 centerWeight = GetBurleyCDF(d3d, centerRadius, 0);
86+
87+
int3 seed = int3(DTid.xy, 0);
88+
int seedStart = Random::pcg3d(int3(seed.xy, SharedData::FrameCount)).x;
89+
90+
[loop]
91+
for (int i = 0; i < BurleySamples; ++i)
92+
{
93+
seed.z = seedStart++;
94+
float2 rand = float2(Random::pcg3d(seed).xy) / 4294967296.0f; // to [0, 1)
95+
96+
rand.x = centerRadiusCDF + rand.x * (1.0f - centerRadiusCDF);
97+
98+
// generate radius & angle for sampling
99+
float radius = max(RadiusApprox(d, rand.x), 1e-5f);
100+
float theta = rand.y * 2.0f * Math::PI;
101+
102+
float pdf = GetBurleyPDF(radius, dmfpForSampling, s);
103+
104+
float2 uvOffset = uvScale * radius;
105+
uvOffset.x *= cos(theta);
106+
uvOffset.y *= sin(theta);
107+
108+
float2 sampleUV = texCoord + uvOffset;
109+
float2 clampedUV = clamp(sampleUV, float2(0.0f, 0.0f), float2(1.0f, 1.0f));
110+
uint2 samplePixcoord = uint2(clampedUV * SharedData::BufferDim.xy);
111+
float3 sampleColor = Color::GammaToLinear(ColorTexture[samplePixcoord].xyz / max(AlbedoTexture[samplePixcoord].xyz, EPSILON_SSS_ALBEDO));
112+
float sampleDepth = SharedData::GetScreenDepth(DepthTexture[samplePixcoord].x);
113+
float3 sampleNormalVS = GBuffer::DecodeNormal(NormalTexture[samplePixcoord].xy);
114+
float3 sampleNormalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(sampleNormalVS, 0)).xyz);
115+
116+
float deltaDepth = (sampleDepth - centerDepth) * 10.f / GAME_UNIT_TO_CM; // convert to mm
117+
float radiusSampledInMM = sqrt(radius * radius + deltaDepth * deltaDepth);
118+
119+
float maskSample = MaskTexture[samplePixcoord].x;
120+
bool mask = maskSample > 1e-5f;
121+
122+
float3 diffusionProfile = GetBurleyProfile(diffuseMeanFreePath.xyz, s3d, radiusSampledInMM);
123+
float normalWeight = sqrt(saturate(dot(sampleNormalWS, normalWS) * 0.5f + 0.5f));
124+
float3 sampleWeight = mask ? (diffusionProfile / pdf) * normalWeight : 0.0f;
125+
126+
colorSum += sampleWeight * sampleColor;
127+
weightSum += sampleWeight;
128+
}
129+
130+
colorSum *= any(weightSum == 0.0f) ? 0.0f : (1.0f / weightSum);
131+
colorSum = lerp(colorSum, originalColor, saturate(centerWeight));
132+
float3 color = Color::LinearToGamma(colorSum) * AlbedoTexture[DTid.xy].xyz;
133+
134+
float4 outColor = float4(color, ColorTexture[DTid.xy].w);
135+
return outColor;
136+
}

features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ RWTexture2D<float4> SSSRW : register(u0);
33
Texture2D<float4> ColorTexture : register(t0);
44
Texture2D<float4> DepthTexture : register(t1);
55
Texture2D<float4> MaskTexture : register(t2);
6+
Texture2D<float4> AlbedoTexture : register(t3);
7+
Texture2D<float4> NormalTexture : register(t4);
68

79
#define SSSS_N_SAMPLES 21
810

@@ -12,19 +14,36 @@ cbuffer PerFrameSSS : register(b1)
1214
float4 BaseProfile;
1315
float4 HumanProfile;
1416
float SSSS_FOVY;
17+
uint BurleySamples;
18+
uint2 pad;
19+
float4 MeanFreePathBase;
20+
float4 MeanFreePathHuman;
1521
};
1622

1723
#include "Common/Color.hlsli"
1824
#include "Common/Random.hlsli"
1925
#include "Common/SharedData.hlsli"
2026

21-
#include "SubsurfaceScattering/SeparableSSS.hlsli"
27+
#if defined(BURLEY)
28+
# include "SubsurfaceScattering/Burley.hlsli"
29+
#else
30+
# include "SubsurfaceScattering/SeparableSSS.hlsli"
31+
#endif
2232

2333
[numthreads(8, 8, 1)] void main(uint3 DTid
2434
: SV_DispatchThreadID) {
2535
float2 texCoord = (DTid.xy + 0.5) * SharedData::BufferDim.zw;
36+
uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(texCoord);
37+
38+
#if defined(BURLEY)
39+
40+
float sssAmount = MaskTexture[DTid.xy].x;
41+
bool humanProfile = MaskTexture[DTid.xy].y > 0.0;
42+
43+
float4 color = BurleyNormalizedSS(DTid.xy, texCoord, eyeIndex, sssAmount, humanProfile);
44+
SSSRW[DTid.xy] = max(0, color);
2645

27-
#if defined(HORIZONTAL)
46+
#elif defined(HORIZONTAL)
2847

2948
float sssAmount = MaskTexture[DTid.xy].x;
3049
bool humanProfile = MaskTexture[DTid.xy].y > 0.0;

package/Shaders/Common/Math.hlsli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef __MATH_DEPENDENCY_HLSL__
22
#define __MATH_DEPENDENCY_HLSL__
33

4+
#define EPSILON_SSS_ALBEDO 1e-3f // For albedo clamping in SSS calculations
45
#define EPSILON_DOT_CLAMP 1e-5f // For dot product clamping
56
#define EPSILON_DIVISION 1e-6f // For division to avoid division by zero
67

0 commit comments

Comments
 (0)