Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebGPURenderer: Add PCFShadowMap support. #28926

Merged
merged 10 commits into from
Jul 23, 2024
Binary file modified examples/screenshots/webgpu_clipping.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/screenshots/webgpu_postprocessing_pixel.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions examples/webgpu_postprocessing_pixel.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@

const mesh = new THREE.Mesh( new THREE.BoxGeometry( boxSideLength, boxSideLength, boxSideLength ), boxMaterial );
mesh.castShadow = true;
//mesh.receiveShadow = true;
mesh.receiveShadow = true;
mesh.rotation.y = rotation;
mesh.position.y = boxSideLength / 2;
mesh.position.set( x, boxSideLength / 2 + .0001, z );
Expand Down Expand Up @@ -101,7 +101,7 @@
specular: 0xffffff
} )
);
//crystalMesh.receiveShadow = true;
crystalMesh.receiveShadow = true;
crystalMesh.castShadow = true;
scene.add( crystalMesh );

Expand All @@ -113,6 +113,7 @@
directionalLight.position.set( 100, 100, 100 );
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.set( 2048, 2048 );
directionalLight.shadow.bias = - 0.0001;
Copy link
Collaborator Author

@Mugen87 Mugen87 Jul 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the root cause yet but lights with WebGPURenderer seems to need a shadow bias more often. I suspect this is related to the shadow compare operation which is different compared to WebGLRenderer. The previous approach used MeshDepthMaterial with RGBA packing, the new approach uses a depth texture attachment.

Changing the depth texture type to THREE.FloatType did not help.

scene.add( directionalLight );

const spotLight = new THREE.SpotLight( 0xffc100, 10, 10, Math.PI / 16, .02, 2 );
Expand All @@ -121,11 +122,12 @@
scene.add( target );
target.position.set( 0, 0, 0 );
spotLight.castShadow = true;
spotLight.shadow.bias = - 0.001;
scene.add( spotLight );

renderer = new THREE.WebGPURenderer( { antialias: false } );
renderer = new THREE.WebGPURenderer();
renderer.shadowMap.enabled = true;
//renderer.setPixelRatio( window.devicePixelRatio );
renderer.shadowMap.type = THREE.BasicShadowMap;
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );
Expand Down
216 changes: 112 additions & 104 deletions src/nodes/lighting/AnalyticLightNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,66 @@ import LightingNode from './LightingNode.js';
import { NodeUpdateType } from '../core/constants.js';
import { uniform } from '../core/UniformNode.js';
import { addNodeClass } from '../core/Node.js';
import { vec3, vec4 } from '../shadernode/ShaderNode.js';
import { float, vec2, vec3, vec4 } from '../shadernode/ShaderNode.js';
import { reference } from '../accessors/ReferenceNode.js';
import { texture } from '../accessors/TextureNode.js';
import { positionWorld } from '../accessors/PositionNode.js';
import { normalWorld } from '../accessors/NormalNode.js';
import { mix } from '../math/MathNode.js';
//import { add } from '../math/OperatorNode.js';
import { add } from '../math/OperatorNode.js';
import { Color } from '../../math/Color.js';
import { DepthTexture } from '../../textures/DepthTexture.js';
import { NearestFilter, LessCompare, NoToneMapping, WebGPUCoordinateSystem } from '../../constants.js';
import { tslFn } from '../shadernode/ShaderNode.js';
import { LessCompare, WebGPUCoordinateSystem } from '../../constants.js';

const BasicShadowMap = tslFn( ( { depthTexture, shadowCoord } ) => {

return texture( depthTexture, shadowCoord.xy ).compare( shadowCoord.z );

} );

const PCFShadowMap = tslFn( ( { depthTexture, shadowCoord, shadow } ) => {

const depthCompare = ( uv, compare ) => texture( depthTexture, uv ).compare( compare );

const mapSize = reference( 'mapSize', 'vec2', shadow );
const radius = reference( 'radius', 'float', shadow );

const texelSize = vec2( 1 ).div( mapSize );
const dx0 = texelSize.x.negate().mul( radius );
const dy0 = texelSize.y.negate().mul( radius );
const dx1 = texelSize.x.mul( radius );
const dy1 = texelSize.y.mul( radius );
const dx2 = dx0.div( 2 );
const dy2 = dy0.div( 2 );
const dx3 = dx1.div( 2 );
const dy3 = dy1.div( 2 );

return add(
depthCompare( shadowCoord.xy.add( vec2( dx0, dy0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( 0, dy0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx1, dy0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx2, dy2 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( 0, dy2 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx3, dy2 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx0, 0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx2, 0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy, shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx3, 0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx1, 0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx2, dy3 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( 0, dy3 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx3, dy3 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx0, dy1 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( 0, dy1 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx1, dy1 ) ), shadowCoord.z )
).mul( 1 / 17 );

} );

const shadowFilterLib = [ BasicShadowMap, PCFShadowMap ];

//

let overrideMaterial = null;

Expand All @@ -25,15 +75,14 @@ class AnalyticLightNode extends LightingNode {

this.light = light;

this.rtt = null;
this.shadowNode = null;
this.shadowMaskNode = null;

this.color = new Color();
this._defaultColorNode = uniform( this.color );
this._shadowColorNode = null;
this.colorNode = uniform( this.color );

this.baseColorNode = null;

this.colorNode = this._defaultColorNode;
this.shadowMap = null;
this.shadowNode = null;
this.shadowColorNode = null;

this.isAnalyticLightNode = true;

Expand All @@ -53,19 +102,11 @@ class AnalyticLightNode extends LightingNode {

setupShadow( builder ) {

const { object } = builder;

if ( object.receiveShadow === false ) {
const { object, renderer } = builder;

this.colorNode = this._defaultColorNode;
let shadowColorNode = this.shadowColorNode;

return;

}

let shadowNode = this.shadowNode;

if ( shadowNode === null ) {
if ( shadowColorNode === null ) {

if ( overrideMaterial === null ) {

Expand All @@ -75,17 +116,12 @@ class AnalyticLightNode extends LightingNode {

}

const shadow = this.light.shadow;
const rtt = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height );

const depthTexture = new DepthTexture();
depthTexture.minFilter = NearestFilter;
depthTexture.magFilter = NearestFilter;
depthTexture.image.width = shadow.mapSize.width;
depthTexture.image.height = shadow.mapSize.height;
depthTexture.compareFunction = LessCompare;

rtt.depthTexture = depthTexture;
const shadow = this.light.shadow;
const shadowMap = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height );
shadowMap.depthTexture = depthTexture;

shadow.camera.updateProjectionMatrix();

Expand All @@ -100,15 +136,9 @@ class AnalyticLightNode extends LightingNode {
let shadowCoord = uniform( shadow.matrix ).mul( position.add( normalWorld.mul( normalBias ) ) );
shadowCoord = shadowCoord.xyz.div( shadowCoord.w );

const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
.and( shadowCoord.x.lessThanEqual( 1 ) )
.and( shadowCoord.y.greaterThanEqual( 0 ) )
.and( shadowCoord.y.lessThanEqual( 1 ) )
.and( shadowCoord.z.lessThanEqual( 1 ) );

let coordZ = shadowCoord.z.add( bias );

if ( builder.renderer.coordinateSystem === WebGPUCoordinateSystem ) {
if ( renderer.coordinateSystem === WebGPUCoordinateSystem ) {

coordZ = coordZ.mul( 2 ).sub( 1 ); // WebGPU: Convertion [ 0, 1 ] to [ - 1, 1 ]

Expand All @@ -120,94 +150,75 @@ class AnalyticLightNode extends LightingNode {
coordZ
);

const textureCompare = ( depthTexture, shadowCoord, compare ) => texture( depthTexture, shadowCoord ).compare( compare );
//const textureCompare = ( depthTexture, shadowCoord, compare ) => compare.step( texture( depthTexture, shadowCoord ) );

// BasicShadowMap

shadowNode = textureCompare( depthTexture, shadowCoord.xy, shadowCoord.z );

// PCFShadowMap
/*
const mapSize = reference( 'mapSize', 'vec2', shadow );
const radius = reference( 'radius', 'float', shadow );

const texelSize = vec2( 1 ).div( mapSize );
const dx0 = texelSize.x.negate().mul( radius );
const dy0 = texelSize.y.negate().mul( radius );
const dx1 = texelSize.x.mul( radius );
const dy1 = texelSize.y.mul( radius );
const dx2 = dx0.mul( 2 );
const dy2 = dy0.mul( 2 );
const dx3 = dx1.mul( 2 );
const dy3 = dy1.mul( 2 );

shadowNode = add(
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx0, dy0 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( 0, dy0 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx1, dy0 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx2, dy2 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( 0, dy2 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx3, dy2 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx0, 0 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx2, 0 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy, shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx3, 0 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx1, 0 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx2, dy3 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( 0, dy3 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx3, dy3 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx0, dy1 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( 0, dy1 ) ), shadowCoord.z ),
textureCompare( depthTexture, shadowCoord.xy.add( vec2( dx1, dy1 ) ), shadowCoord.z )
).mul( 1 / 17 );
*/
const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
.and( shadowCoord.x.lessThanEqual( 1 ) )
.and( shadowCoord.y.greaterThanEqual( 0 ) )
.and( shadowCoord.y.lessThanEqual( 1 ) )
.and( shadowCoord.z.lessThanEqual( 1 ) );

//

const shadowColor = texture( rtt.texture, shadowCoord );
const shadowMaskNode = frustumTest.mix( 1, shadowNode.mix( shadowColor.a.mix( 1, shadowColor ), 1 ) );
const filterFuncion = shadow.filterNode || shadowFilterLib[ renderer.shadowMap.type ] || null;

this.rtt = rtt;
this.colorNode = this.colorNode.mul( mix( 1, shadowMaskNode, shadowIntensity ) );
this._shadowColorNode = this.colorNode;
if ( filterFuncion === null ) {

this.shadowNode = shadowNode;
this.shadowMaskNode = shadowMaskNode;
throw new Error( 'THREE.WebGPURenderer: Shadow map type not supported yet.' );

//
}

this.updateBeforeType = NodeUpdateType.RENDER;
const shadowNode = frustumTest.cond( filterFuncion( { depthTexture, shadowCoord, shadow } ), float( 1 ) );

} else {
this.shadowMap = shadowMap;

this.colorNode = this._shadowColorNode;
this.shadowNode = shadowNode;
this.shadowColorNode = shadowColorNode = this.colorNode.mul( mix( 1, shadowNode, shadowIntensity ) );

this.baseColorNode = this.colorNode;

}

//

this.colorNode = shadowColorNode;

this.updateBeforeType = NodeUpdateType.RENDER;

}

setup( builder ) {

if ( this.light.castShadow ) this.setupShadow( builder );
else if ( this.shadowNode !== null ) this.disposeShadow();
this.colorNode = this.baseColorNode || this.colorNode;

if ( this.light.castShadow ) {

if ( builder.object.receiveShadow ) {

this.setupShadow( builder );

}

} else if ( this.shadowNode !== null ) {

this.disposeShadow();

}

}

updateShadow( frame ) {

const { rtt, light } = this;
const { shadowMap, light } = this;
const { renderer, scene, camera } = frame;

const currentOverrideMaterial = scene.overrideMaterial;

scene.overrideMaterial = overrideMaterial;

rtt.setSize( light.shadow.mapSize.width, light.shadow.mapSize.height );
shadowMap.setSize( light.shadow.mapSize.width, light.shadow.mapSize.height );

light.shadow.updateMatrices( light );
light.shadow.camera.layers.mask = camera.layers.mask;

const currentToneMapping = renderer.toneMapping;
const currentRenderTarget = renderer.getRenderTarget();
const currentRenderObjectFunction = renderer.getRenderObjectFunction();

Expand All @@ -221,30 +232,27 @@ class AnalyticLightNode extends LightingNode {

} );

renderer.setRenderTarget( rtt );
renderer.toneMapping = NoToneMapping;

renderer.setRenderTarget( shadowMap );
renderer.render( scene, light.shadow.camera );

renderer.setRenderTarget( currentRenderTarget );
renderer.setRenderObjectFunction( currentRenderObjectFunction );

renderer.toneMapping = currentToneMapping;

scene.overrideMaterial = currentOverrideMaterial;

}

disposeShadow() {

this.rtt.dispose();
this.shadowMap.dispose();
this.shadowMap = null;

this.shadowNode = null;
this.shadowMaskNode = null;
this._shadowColorNode = null;
this.rtt = null;
this.shadowColorNode = null;

this.baseColorNode = null;

this.colorNode = this._defaultColorNode;
this.updateBeforeType = NodeUpdateType.NONE;

}

Expand Down
3 changes: 1 addition & 2 deletions src/nodes/lighting/DirectionalLightNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ class DirectionalLightNode extends AnalyticLightNode {
lightingModel.direct( {
lightDirection,
lightColor,
reflectedLight,
shadowMask: this.shadowMaskNode
reflectedLight
}, builder.stack, builder );

}
Expand Down
3 changes: 1 addition & 2 deletions src/nodes/lighting/PointLightNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ class PointLightNode extends AnalyticLightNode {
lightingModel.direct( {
lightDirection,
lightColor,
reflectedLight,
shadowMask: this.shadowMaskNode
reflectedLight
}, builder.stack, builder );

}
Expand Down
3 changes: 1 addition & 2 deletions src/nodes/lighting/SpotLightNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ class SpotLightNode extends AnalyticLightNode {
lightingModel.direct( {
lightDirection,
lightColor,
reflectedLight,
shadowMask: this.shadowMaskNode
reflectedLight
}, builder.stack, builder );

}
Expand Down
Loading