Skip to content

Commit 77ebf27

Browse files
authored
PointShadowNode: Refactor to use native cube depth texture (#32379)
1 parent 24a10b7 commit 77ebf27

File tree

7 files changed

+146
-183
lines changed

7 files changed

+146
-183
lines changed

src/nodes/accessors/CubeTextureNode.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,19 @@ class CubeTextureNode extends TextureNode {
4747
}
4848

4949
/**
50-
* Overwrites the default implementation to return a fixed value `'cubeTexture'`.
50+
* Overwrites the default implementation to return the appropriate cube texture type.
5151
*
5252
* @param {NodeBuilder} builder - The current node builder.
5353
* @return {string} The input type.
5454
*/
5555
getInputType( /*builder*/ ) {
5656

57+
if ( this.value.isDepthTexture === true ) {
58+
59+
return 'cubeDepthTexture';
60+
61+
}
62+
5763
return 'cubeTexture';
5864

5965
}
@@ -105,6 +111,19 @@ class CubeTextureNode extends TextureNode {
105111

106112
const texture = this.value;
107113

114+
// Depth textures (shadow maps) - no environment rotation, Y flip for WebGPU
115+
if ( texture.isDepthTexture === true ) {
116+
117+
if ( builder.renderer.coordinateSystem === WebGPUCoordinateSystem ) {
118+
119+
return vec3( uvNode.x, uvNode.y.negate(), uvNode.z );
120+
121+
}
122+
123+
return uvNode;
124+
125+
}
126+
108127
if ( builder.renderer.coordinateSystem === WebGPUCoordinateSystem || ! texture.isRenderTargetTexture ) {
109128

110129
uvNode = vec3( uvNode.x.negate(), uvNode.yz );

src/nodes/lighting/PointShadowNode.js

Lines changed: 77 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -2,165 +2,66 @@ import ShadowNode from './ShadowNode.js';
22
import { uniform } from '../core/UniformNode.js';
33
import { float, vec2, If, Fn, nodeObject } from '../tsl/TSLBase.js';
44
import { reference } from '../accessors/ReferenceNode.js';
5-
import { texture } from '../accessors/TextureNode.js';
6-
import { max, abs, sign } from '../math/MathNode.js';
7-
import { sub, div } from '../math/OperatorNode.js';
5+
import { cubeTexture } from '../accessors/CubeTextureNode.js';
86
import { renderGroup } from '../core/UniformGroupNode.js';
97
import { Matrix4 } from '../../math/Matrix4.js';
10-
import { Vector2 } from '../../math/Vector2.js';
118
import { Vector3 } from '../../math/Vector3.js';
12-
import { Vector4 } from '../../math/Vector4.js';
139
import { Color } from '../../math/Color.js';
14-
import { BasicShadowMap } from '../../constants.js';
10+
import { BasicShadowMap, LessCompare, WebGPUCoordinateSystem } from '../../constants.js';
11+
import { CubeDepthTexture } from '../../textures/CubeDepthTexture.js';
1512

1613
const _clearColor = /*@__PURE__*/ new Color();
1714
const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
1815
const _lightPositionWorld = /*@__PURE__*/ new Vector3();
1916
const _lookTarget = /*@__PURE__*/ new Vector3();
2017

21-
// These viewports map a cube-map onto a 2D texture with the
22-
// following orientation:
23-
//
24-
// xzXZ
25-
// y Y
26-
//
27-
// X - Positive x direction
28-
// x - Negative x direction
29-
// Y - Positive y direction
30-
// y - Negative y direction
31-
// Z - Positive z direction
32-
// z - Negative z direction
33-
34-
const _frameExtents = /*@__PURE__*/ new Vector2( 4, 2 );
35-
36-
const _viewports = [
37-
// positive X
38-
/*@__PURE__*/ new Vector4( 2, 1, 1, 1 ),
39-
// negative X
40-
/*@__PURE__*/ new Vector4( 0, 1, 1, 1 ),
41-
// positive Z
42-
/*@__PURE__*/ new Vector4( 3, 1, 1, 1 ),
43-
// negative Z
44-
/*@__PURE__*/ new Vector4( 1, 1, 1, 1 ),
45-
// positive Y
46-
/*@__PURE__*/ new Vector4( 3, 0, 1, 1 ),
47-
// negative Y
48-
/*@__PURE__*/ new Vector4( 1, 0, 1, 1 )
18+
// Cube map face directions and up vectors for point light shadows
19+
// Face order: +X, -X, +Y, -Y, +Z, -Z
20+
// WebGPU coordinate system - Y faces swapped to match texture sampling convention
21+
const _cubeDirectionsWebGPU = [
22+
/*@__PURE__*/ new Vector3( 1, 0, 0 ), /*@__PURE__*/ new Vector3( - 1, 0, 0 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 ),
23+
/*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 0, - 1 )
4924
];
5025

51-
const _cubeDirections = [
52-
/*@__PURE__*/ new Vector3( 1, 0, 0 ), /*@__PURE__*/ new Vector3( - 1, 0, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ),
53-
/*@__PURE__*/ new Vector3( 0, 0, - 1 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 )
26+
const _cubeUpsWebGPU = [
27+
/*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, - 1 ),
28+
/*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 )
5429
];
5530

56-
const _cubeUps = [
57-
/*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ),
58-
/*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 0, - 1 )
31+
// WebGL coordinate system - standard OpenGL convention
32+
const _cubeDirectionsWebGL = [
33+
/*@__PURE__*/ new Vector3( 1, 0, 0 ), /*@__PURE__*/ new Vector3( - 1, 0, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ),
34+
/*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 0, - 1 )
5935
];
6036

61-
// cubeToUV() maps a 3D direction vector suitable for cube texture mapping to a 2D
62-
// vector suitable for 2D texture mapping. This code uses the following layout for the
63-
// 2D texture:
64-
//
65-
// xzXZ
66-
// y Y
67-
//
68-
// Y - Positive y direction
69-
// y - Negative y direction
70-
// X - Positive x direction
71-
// x - Negative x direction
72-
// Z - Positive z direction
73-
// z - Negative z direction
74-
//
75-
// Source and test bed:
76-
// https://gist.github.com/tschw/da10c43c467ce8afd0c4
77-
78-
export const cubeToUV = /*@__PURE__*/ Fn( ( [ pos, texelSizeY ] ) => {
79-
80-
const v = pos.toVar();
81-
82-
// Number of texels to avoid at the edge of each square
83-
84-
const absV = abs( v );
85-
86-
// Intersect unit cube
87-
88-
const scaleToCube = div( 1.0, max( absV.x, max( absV.y, absV.z ) ) );
89-
absV.mulAssign( scaleToCube );
90-
91-
// Apply scale to avoid seams
92-
93-
// two texels less per square (one texel will do for NEAREST)
94-
v.mulAssign( scaleToCube.mul( texelSizeY.mul( 2 ).oneMinus() ) );
95-
96-
// Unwrap
97-
98-
// space: -1 ... 1 range for each square
99-
//
100-
// #X## dim := ( 4 , 2 )
101-
// # # center := ( 1 , 1 )
102-
103-
const planar = vec2( v.xy ).toVar();
104-
105-
const almostATexel = texelSizeY.mul( 1.5 );
106-
const almostOne = almostATexel.oneMinus();
107-
108-
If( absV.z.greaterThanEqual( almostOne ), () => {
109-
110-
If( v.z.greaterThan( 0.0 ), () => {
111-
112-
planar.x.assign( sub( 4.0, v.x ) );
113-
114-
} );
115-
116-
} ).ElseIf( absV.x.greaterThanEqual( almostOne ), () => {
117-
118-
const signX = sign( v.x );
119-
planar.x.assign( v.z.mul( signX ).add( signX.mul( 2.0 ) ) );
120-
121-
} ).ElseIf( absV.y.greaterThanEqual( almostOne ), () => {
122-
123-
const signY = sign( v.y );
124-
planar.x.assign( v.x.add( signY.mul( 2.0 ) ).add( 2.0 ) );
125-
planar.y.assign( v.z.mul( signY ).sub( 2.0 ) );
126-
127-
} );
128-
129-
// Transform to UV space
130-
131-
// scale := 0.5 / dim
132-
// translate := ( center + 0.5 ) / dim
133-
return vec2( 0.125, 0.25 ).mul( planar ).add( vec2( 0.375, 0.75 ) ).flipY();
134-
135-
} ).setLayout( {
136-
name: 'cubeToUV',
137-
type: 'vec2',
138-
inputs: [
139-
{ name: 'pos', type: 'vec3' },
140-
{ name: 'texelSizeY', type: 'float' }
141-
]
142-
} );
37+
const _cubeUpsWebGL = [
38+
/*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ),
39+
/*@__PURE__*/ new Vector3( 0, 0, - 1 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 )
40+
];
14341

144-
export const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, texelSize } ) => {
42+
export const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) => {
14543

146-
return texture( depthTexture, cubeToUV( bd3D, texelSize.y ) ).compare( dp );
44+
return cubeTexture( depthTexture, bd3D ).compare( dp );
14745

14846
} );
14947

150-
export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, texelSize, shadow } ) => {
48+
export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => {
15149

15250
const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
153-
const offset = vec2( - 1.0, 1.0 ).mul( radius ).mul( texelSize.y );
154-
155-
return texture( depthTexture, cubeToUV( bd3D.add( offset.xyy ), texelSize.y ) ).compare( dp )
156-
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yyy ), texelSize.y ) ).compare( dp ) )
157-
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xyx ), texelSize.y ) ).compare( dp ) )
158-
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yyx ), texelSize.y ) ).compare( dp ) )
159-
.add( texture( depthTexture, cubeToUV( bd3D, texelSize.y ) ).compare( dp ) )
160-
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xxy ), texelSize.y ) ).compare( dp ) )
161-
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yxy ), texelSize.y ) ).compare( dp ) )
162-
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xxx ), texelSize.y ) ).compare( dp ) )
163-
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yxx ), texelSize.y ) ).compare( dp ) )
51+
const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
52+
53+
const texelSize = float( 1 ).div( mapSize.x );
54+
const offset = vec2( - 1.0, 1.0 ).mul( radius ).mul( texelSize );
55+
56+
return cubeTexture( depthTexture, bd3D.add( offset.xyy ) ).compare( dp )
57+
.add( cubeTexture( depthTexture, bd3D.add( offset.yyy ) ).compare( dp ) )
58+
.add( cubeTexture( depthTexture, bd3D.add( offset.xyx ) ).compare( dp ) )
59+
.add( cubeTexture( depthTexture, bd3D.add( offset.yyx ) ).compare( dp ) )
60+
.add( cubeTexture( depthTexture, bd3D ).compare( dp ) )
61+
.add( cubeTexture( depthTexture, bd3D.add( offset.xxy ) ).compare( dp ) )
62+
.add( cubeTexture( depthTexture, bd3D.add( offset.yxy ) ).compare( dp ) )
63+
.add( cubeTexture( depthTexture, bd3D.add( offset.xxx ) ).compare( dp ) )
64+
.add( cubeTexture( depthTexture, bd3D.add( offset.yxx ) ).compare( dp ) )
16465
.mul( 1.0 / 9.0 );
16566

16667
} );
@@ -175,7 +76,6 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo
17576
const cameraNearLocal = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near );
17677
const cameraFarLocal = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far );
17778
const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
178-
const mapSize = uniform( shadow.mapSize ).setGroup( renderGroup );
17979

18080
const result = float( 1.0 ).toVar();
18181

@@ -185,23 +85,18 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo
18585
const dp = lightToPositionLength.sub( cameraNearLocal ).div( cameraFarLocal.sub( cameraNearLocal ) ).toVar(); // need to clamp?
18686
dp.addAssign( bias );
18787

188-
// bd3D = base direction 3D
88+
// bd3D = base direction 3D (direction from light to fragment)
18989
const bd3D = lightToPosition.normalize();
190-
const texelSize = vec2( 1.0 ).div( mapSize.mul( vec2( 4.0, 2.0 ) ) );
19190

192-
// percentage-closer filtering
193-
result.assign( filterFn( { depthTexture, bd3D, dp, texelSize, shadow } ) );
91+
// percentage-closer filtering using cube texture sampling
92+
result.assign( filterFn( { depthTexture, bd3D, dp, shadow } ) );
19493

19594
} );
19695

19796
return result;
19897

19998
} );
20099

201-
const _viewport = /*@__PURE__*/ new Vector4();
202-
const _viewportSize = /*@__PURE__*/ new Vector2();
203-
const _shadowMapSize = /*@__PURE__*/ new Vector2();
204-
205100

206101
/**
207102
* Represents the shadow implementation for point light nodes.
@@ -261,15 +156,35 @@ class PointShadowNode extends ShadowNode {
261156
* @param {NodeBuilder} builder - A reference to the current node builder.
262157
* @param {Object} inputs - A configuration object that defines the shadow filtering.
263158
* @param {Function} inputs.filterFn - This function defines the filtering type of the shadow map e.g. PCF.
264-
* @param {Texture} inputs.shadowTexture - A reference to the shadow map's texture.
265-
* @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
159+
* @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's depth texture.
266160
* @param {Node<vec3>} inputs.shadowCoord - Shadow coordinates which are used to sample from the shadow map.
267161
* @param {LightShadow} inputs.shadow - The light shadow.
268162
* @return {Node<float>} The result node of the shadow filtering.
269163
*/
270-
setupShadowFilter( builder, { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } ) {
164+
setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow } ) {
165+
166+
return pointShadowFilter( { filterFn, depthTexture, shadowCoord, shadow } );
167+
168+
}
169+
170+
/**
171+
* Overwrites the default implementation to create a CubeRenderTarget with CubeDepthTexture.
172+
*
173+
* @param {LightShadow} shadow - The light shadow object.
174+
* @param {NodeBuilder} builder - A reference to the current node builder.
175+
* @return {Object} An object containing the shadow map and depth texture.
176+
*/
177+
setupRenderTarget( shadow, builder ) {
178+
179+
const depthTexture = new CubeDepthTexture( shadow.mapSize.width );
180+
depthTexture.name = 'PointShadowDepthTexture';
181+
depthTexture.compareFunction = LessCompare;
271182

272-
return pointShadowFilter( { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } );
183+
const shadowMap = builder.createCubeRenderTarget( shadow.mapSize.width );
184+
shadowMap.texture.name = 'PointShadowMap';
185+
shadowMap.depthTexture = depthTexture;
186+
187+
return { shadowMap, depthTexture };
273188

274189
}
275190

@@ -287,12 +202,12 @@ class PointShadowNode extends ShadowNode {
287202
const camera = shadow.camera;
288203
const shadowMatrix = shadow.matrix;
289204

290-
_shadowMapSize.copy( shadow.mapSize );
291-
_shadowMapSize.multiply( _frameExtents );
292-
293-
shadowMap.setSize( _shadowMapSize.width, _shadowMapSize.height );
205+
// Select cube directions/ups based on coordinate system
206+
const isWebGPU = renderer.coordinateSystem === WebGPUCoordinateSystem;
207+
const cubeDirections = isWebGPU ? _cubeDirectionsWebGPU : _cubeDirectionsWebGL;
208+
const cubeUps = isWebGPU ? _cubeUpsWebGPU : _cubeUpsWebGL;
294209

295-
_viewportSize.copy( shadow.mapSize );
210+
shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.width );
296211

297212
//
298213

@@ -303,23 +218,13 @@ class PointShadowNode extends ShadowNode {
303218

304219
renderer.autoClear = false;
305220
renderer.setClearColor( shadow.clearColor, shadow.clearAlpha );
306-
renderer.clear();
307-
308-
for ( let vp = 0; vp < 6; vp ++ ) {
309-
310-
const viewport = _viewports[ vp ];
311-
312-
const x = _viewportSize.x * viewport.x;
313-
const y = _shadowMapSize.y - _viewportSize.y - ( _viewportSize.y * viewport.y );
314221

315-
_viewport.set(
316-
x,
317-
y,
318-
_viewportSize.x * viewport.z,
319-
_viewportSize.y * viewport.w
320-
);
222+
// Render each cube face
223+
for ( let face = 0; face < 6; face ++ ) {
321224

322-
shadowMap.viewport.copy( _viewport );
225+
// Set render target to the specific cube face
226+
renderer.setRenderTarget( shadowMap, face );
227+
renderer.clear();
323228

324229
// Update shadow camera matrices for this face
325230

@@ -336,8 +241,8 @@ class PointShadowNode extends ShadowNode {
336241
camera.position.copy( _lightPositionWorld );
337242

338243
_lookTarget.copy( camera.position );
339-
_lookTarget.add( _cubeDirections[ vp ] );
340-
camera.up.copy( _cubeUps[ vp ] );
244+
_lookTarget.add( cubeDirections[ face ] );
245+
camera.up.copy( cubeUps[ face ] );
341246
camera.lookAt( _lookTarget );
342247
camera.updateMatrixWorld();
343248

@@ -350,7 +255,7 @@ class PointShadowNode extends ShadowNode {
350255

351256
const currentSceneName = scene.name;
352257

353-
scene.name = `Point Light Shadow [ ${ light.name || 'ID: ' + light.id } ] - Face ${ vp + 1 }`;
258+
scene.name = `Point Light Shadow [ ${ light.name || 'ID: ' + light.id } ] - Face ${ face + 1 }`;
354259

355260
renderer.render( scene, camera );
356261

0 commit comments

Comments
 (0)