Skip to content

Commit 7f5b01e

Browse files
committed
fix(soba): rewrite soft-shadows shader injection for Three.js r182+ compatibility
1 parent a168b41 commit 7f5b01e

File tree

1 file changed

+161
-25
lines changed

1 file changed

+161
-25
lines changed

libs/soba/misc/src/lib/soft-shadows.ts

Lines changed: 161 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,11 @@ const defaultOptions: NgtsSoftShadowsOptions = {
3838
focus: 0,
3939
};
4040

41-
function pcss(options: NgtsSoftShadowsOptions): string {
41+
/**
42+
* Generates PCSS shader code for Three.js < r182 (uses RGBA-packed depth)
43+
*/
44+
function pcssLegacy(options: NgtsSoftShadowsOptions): string {
4245
const { focus, size, samples } = options;
43-
// Three.js r182 removed unpackRGBAToDepth and switched to native depth textures
44-
const useNativeDepth = getVersion() >= 182;
45-
const sampleDepth = useNativeDepth
46-
? 'texture2D( shadowMap, uv + offset ).r'
47-
: 'unpackRGBAToDepth( texture2D( shadowMap, uv + offset ) )';
4846
return `
4947
#define PENUMBRA_FILTER_SIZE float(${size})
5048
#define RGB_NOISE_FUNCTION(uv) (randRGB(uv))
@@ -57,8 +55,6 @@ vec3 randRGB(vec2 uv) {
5755
}
5856
5957
vec3 lowPassRandRGB(vec2 uv) {
60-
// 3x3 convolution (average)
61-
// can be implemented as separable with an extra buffer for a total of 6 samples instead of 9
6258
vec3 result = vec3(0);
6359
result += RGB_NOISE_FUNCTION(uv + vec2(-1.0, -1.0));
6460
result += RGB_NOISE_FUNCTION(uv + vec2(-1.0, 0.0));
@@ -69,40 +65,39 @@ vec3 lowPassRandRGB(vec2 uv) {
6965
result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, -1.0));
7066
result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, 0.0));
7167
result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, +1.0));
72-
result *= 0.111111111; // 1.0 / 9.0
68+
result *= 0.111111111;
7369
return result;
7470
}
71+
7572
vec3 highPassRandRGB(vec2 uv) {
76-
// by subtracting the low-pass signal from the original signal, we're being left with the high-pass signal
77-
// hp(x) = x - lp(x)
7873
return RGB_NOISE_FUNCTION(uv) - lowPassRandRGB(uv) + 0.5;
7974
}
8075
81-
8276
vec2 vogelDiskSample(int sampleIndex, int sampleCount, float angle) {
83-
const float goldenAngle = 2.399963f; // radians
77+
const float goldenAngle = 2.399963f;
8478
float r = sqrt(float(sampleIndex) + 0.5f) / sqrt(float(sampleCount));
8579
float theta = float(sampleIndex) * goldenAngle + angle;
8680
float sine = sin(theta);
8781
float cosine = cos(theta);
8882
return vec2(cosine, sine) * r;
8983
}
90-
float penumbraSize( const in float zReceiver, const in float zBlocker ) { // Parallel plane estimation
84+
85+
float penumbraSize( const in float zReceiver, const in float zBlocker ) {
9186
return (zReceiver - zBlocker) / zBlocker;
9287
}
88+
9389
float findBlocker(sampler2D shadowMap, vec2 uv, float compare, float angle) {
9490
float texelSize = 1.0 / float(textureSize(shadowMap, 0).x);
9591
float blockerDepthSum = float(${focus});
9692
float blockers = 0.0;
97-
9893
int j = 0;
9994
vec2 offset = vec2(0.);
10095
float depth = 0.;
10196
10297
#pragma unroll_loop_start
10398
for(int i = 0; i < ${samples}; i ++) {
10499
offset = (vogelDiskSample(j, ${samples}, angle) * texelSize) * 2.0 * PENUMBRA_FILTER_SIZE;
105-
depth = ${sampleDepth};
100+
depth = unpackRGBAToDepth( texture2D( shadowMap, uv + offset ) );
106101
if (depth < compare) {
107102
blockerDepthSum += depth;
108103
blockers++;
@@ -117,27 +112,28 @@ float findBlocker(sampler2D shadowMap, vec2 uv, float compare, float angle) {
117112
return -1.0;
118113
}
119114
120-
121115
float vogelFilter(sampler2D shadowMap, vec2 uv, float zReceiver, float filterRadius, float angle) {
122116
float texelSize = 1.0 / float(textureSize(shadowMap, 0).x);
123117
float shadow = 0.0f;
124118
int j = 0;
125119
vec2 vogelSample = vec2(0.0);
126120
vec2 offset = vec2(0.0);
121+
127122
#pragma unroll_loop_start
128123
for (int i = 0; i < ${samples}; i++) {
129124
vogelSample = vogelDiskSample(j, ${samples}, angle) * texelSize;
130125
offset = vogelSample * (1.0 + filterRadius * float(${size}));
131-
shadow += step( zReceiver, ${sampleDepth} );
126+
shadow += step( zReceiver, unpackRGBAToDepth( texture2D( shadowMap, uv + offset ) ) );
132127
j++;
133128
}
134129
#pragma unroll_loop_end
130+
135131
return shadow * 1.0 / ${samples}.0;
136132
}
137133
138134
float PCSS (sampler2D shadowMap, vec4 coords) {
139135
vec2 uv = coords.xy;
140-
float zReceiver = coords.z; // Assumed to be eye-space z in this code
136+
float zReceiver = coords.z;
141137
float angle = highPassRandRGB(gl_FragCoord.xy).r * PI2;
142138
float avgBlockerDepth = findBlocker(shadowMap, uv, zReceiver, angle);
143139
if (avgBlockerDepth == -1.0) {
@@ -148,6 +144,129 @@ float PCSS (sampler2D shadowMap, vec4 coords) {
148144
}`;
149145
}
150146

147+
/**
148+
* Generates PCSS shader code for Three.js >= r182 (uses native depth textures)
149+
*/
150+
function pcssModern(options: NgtsSoftShadowsOptions): string {
151+
const { focus, size, samples } = options;
152+
return `
153+
#define PENUMBRA_FILTER_SIZE float(${size})
154+
#define RGB_NOISE_FUNCTION(uv) (randRGB(uv))
155+
vec3 randRGB(vec2 uv) {
156+
return vec3(
157+
fract(sin(dot(uv, vec2(12.75613, 38.12123))) * 13234.76575),
158+
fract(sin(dot(uv, vec2(19.45531, 58.46547))) * 43678.23431),
159+
fract(sin(dot(uv, vec2(23.67817, 78.23121))) * 93567.23423)
160+
);
161+
}
162+
163+
vec3 lowPassRandRGB(vec2 uv) {
164+
vec3 result = vec3(0);
165+
result += RGB_NOISE_FUNCTION(uv + vec2(-1.0, -1.0));
166+
result += RGB_NOISE_FUNCTION(uv + vec2(-1.0, 0.0));
167+
result += RGB_NOISE_FUNCTION(uv + vec2(-1.0, +1.0));
168+
result += RGB_NOISE_FUNCTION(uv + vec2( 0.0, -1.0));
169+
result += RGB_NOISE_FUNCTION(uv + vec2( 0.0, 0.0));
170+
result += RGB_NOISE_FUNCTION(uv + vec2( 0.0, +1.0));
171+
result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, -1.0));
172+
result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, 0.0));
173+
result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, +1.0));
174+
result *= 0.111111111;
175+
return result;
176+
}
177+
178+
vec3 highPassRandRGB(vec2 uv) {
179+
return RGB_NOISE_FUNCTION(uv) - lowPassRandRGB(uv) + 0.5;
180+
}
181+
182+
vec2 pcssVogelDiskSample(int sampleIndex, int sampleCount, float angle) {
183+
const float goldenAngle = 2.399963f;
184+
float r = sqrt(float(sampleIndex) + 0.5f) / sqrt(float(sampleCount));
185+
float theta = float(sampleIndex) * goldenAngle + angle;
186+
float sine = sin(theta);
187+
float cosine = cos(theta);
188+
return vec2(cosine, sine) * r;
189+
}
190+
191+
float penumbraSize( const in float zReceiver, const in float zBlocker ) {
192+
return (zReceiver - zBlocker) / zBlocker;
193+
}
194+
195+
float findBlocker(sampler2D shadowMap, vec2 uv, float compare, float angle) {
196+
float texelSize = 1.0 / float(textureSize(shadowMap, 0).x);
197+
float blockerDepthSum = float(${focus});
198+
float blockers = 0.0;
199+
int j = 0;
200+
vec2 offset = vec2(0.);
201+
float depth = 0.;
202+
203+
#pragma unroll_loop_start
204+
for(int i = 0; i < ${samples}; i ++) {
205+
offset = (pcssVogelDiskSample(j, ${samples}, angle) * texelSize) * 2.0 * PENUMBRA_FILTER_SIZE;
206+
depth = texture2D( shadowMap, uv + offset ).r;
207+
if (depth < compare) {
208+
blockerDepthSum += depth;
209+
blockers++;
210+
}
211+
j++;
212+
}
213+
#pragma unroll_loop_end
214+
215+
if (blockers > 0.0) {
216+
return blockerDepthSum / blockers;
217+
}
218+
return -1.0;
219+
}
220+
221+
float pcssVogelFilter(sampler2D shadowMap, vec2 uv, float zReceiver, float filterRadius, float angle) {
222+
float texelSize = 1.0 / float(textureSize(shadowMap, 0).x);
223+
float shadow = 0.0f;
224+
int j = 0;
225+
vec2 vogelSample = vec2(0.0);
226+
vec2 offset = vec2(0.0);
227+
228+
#pragma unroll_loop_start
229+
for (int i = 0; i < ${samples}; i++) {
230+
vogelSample = pcssVogelDiskSample(j, ${samples}, angle) * texelSize;
231+
offset = vogelSample * (1.0 + filterRadius * float(${size}));
232+
shadow += step( zReceiver, texture2D( shadowMap, uv + offset ).r );
233+
j++;
234+
}
235+
#pragma unroll_loop_end
236+
237+
return shadow * 1.0 / ${samples}.0;
238+
}
239+
240+
float PCSS (sampler2D shadowMap, vec4 coords, float shadowIntensity) {
241+
vec2 uv = coords.xy;
242+
float zReceiver = coords.z;
243+
float angle = highPassRandRGB(gl_FragCoord.xy).r * PI2;
244+
float avgBlockerDepth = findBlocker(shadowMap, uv, zReceiver, angle);
245+
if (avgBlockerDepth == -1.0) {
246+
return 1.0;
247+
}
248+
float penumbraRatio = penumbraSize(zReceiver, avgBlockerDepth);
249+
float shadow = pcssVogelFilter(shadowMap, uv, zReceiver, 1.25 * penumbraRatio, angle);
250+
return mix( 1.0, shadow, shadowIntensity );
251+
}`;
252+
}
253+
254+
/**
255+
* Generates the replacement getShadow function for r182+
256+
*/
257+
function getShadowReplacement(): string {
258+
return `float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {
259+
shadowCoord.xyz /= shadowCoord.w;
260+
shadowCoord.z += shadowBias;
261+
bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;
262+
bool frustumTest = inFrustum && shadowCoord.z <= 1.0;
263+
if ( frustumTest ) {
264+
return PCSS( shadowMap, shadowCoord, shadowIntensity );
265+
}
266+
return 1.0;
267+
}`;
268+
}
269+
151270
function reset(gl: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera): void {
152271
scene.traverse((object) => {
153272
if ((object as THREE.Mesh).material) {
@@ -180,14 +299,31 @@ export class NgtsSoftShadows {
180299
effect((onCleanup) => {
181300
const { gl, scene, camera } = store.snapshot;
182301
const options = this.options();
302+
const version = getVersion();
183303

184304
const original = THREE.ShaderChunk.shadowmap_pars_fragment;
185-
THREE.ShaderChunk.shadowmap_pars_fragment = THREE.ShaderChunk.shadowmap_pars_fragment
186-
.replace('#ifdef USE_SHADOWMAP', '#ifdef USE_SHADOWMAP\n' + pcss(options))
187-
.replace(
188-
'#if defined( SHADOWMAP_TYPE_PCF )',
189-
'\nreturn PCSS(shadowMap, shadowCoord);\n#if defined( SHADOWMAP_TYPE_PCF )',
190-
);
305+
306+
if (version >= 182) {
307+
// Three.js r182+ uses native depth textures and has a different shader structure
308+
// We need to replace the getShadow function entirely
309+
const pcssCode = pcssModern(options);
310+
311+
// Find and replace the PCF getShadow function
312+
const getShadowRegex =
313+
/(#if defined\( SHADOWMAP_TYPE_PCF \)\s+float getShadow\( sampler2DShadow shadowMap[^}]+\})/s;
314+
315+
THREE.ShaderChunk.shadowmap_pars_fragment = THREE.ShaderChunk.shadowmap_pars_fragment
316+
.replace('#ifdef USE_SHADOWMAP', '#ifdef USE_SHADOWMAP\n' + pcssCode)
317+
.replace(getShadowRegex, `#if defined( SHADOWMAP_TYPE_PCF )\n\t\t${getShadowReplacement()}`);
318+
} else {
319+
// Three.js < r182 uses RGBA-packed depth
320+
THREE.ShaderChunk.shadowmap_pars_fragment = THREE.ShaderChunk.shadowmap_pars_fragment
321+
.replace('#ifdef USE_SHADOWMAP', '#ifdef USE_SHADOWMAP\n' + pcssLegacy(options))
322+
.replace(
323+
'#if defined( SHADOWMAP_TYPE_PCF )',
324+
'\nreturn PCSS(shadowMap, shadowCoord);\n#if defined( SHADOWMAP_TYPE_PCF )',
325+
);
326+
}
191327

192328
reset(gl, scene, camera);
193329

0 commit comments

Comments
 (0)