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+ }
0 commit comments