@@ -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
5957vec3 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+
7572vec3 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-
8276vec2 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+
9389float 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-
121115float 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
138134float 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+
151270function 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+ / ( # i f d e f i n e d \( S H A D O W M A P _ T Y P E _ P C F \) \s + f l o a t g e t S h a d o w \( s a m p l e r 2 D S h a d o w s h a d o w M a p [ ^ } ] + \} ) / 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