diff --git a/examples/files.json b/examples/files.json index 9050a02677b991..7c4741cc6744c6 100644 --- a/examples/files.json +++ b/examples/files.json @@ -410,6 +410,7 @@ "webgpu_sandbox", "webgpu_shadertoy", "webgpu_shadowmap", + "webgpu_shadowmap_csm", "webgpu_shadowmap_opacity", "webgpu_shadowmap_vsm", "webgpu_skinning", diff --git a/examples/jsm/csm/CSM.js b/examples/jsm/csm/CSM.js index 649a0e5e9fb450..50e2cf026b74c7 100644 --- a/examples/jsm/csm/CSM.js +++ b/examples/jsm/csm/CSM.js @@ -11,7 +11,7 @@ import { CSMFrustum } from './CSMFrustum.js'; import { CSMShader } from './CSMShader.js'; const _cameraToLightMatrix = new Matrix4(); -const _lightSpaceFrustum = new CSMFrustum(); +const _lightSpaceFrustum = new CSMFrustum( { webGL: true } ); const _center = new Vector3(); const _bbox = new Box3(); const _uniformArray = []; @@ -38,7 +38,7 @@ export class CSM { this.lightMargin = data.lightMargin || 200; this.customSplitsCallback = data.customSplitsCallback; this.fade = false; - this.mainFrustum = new CSMFrustum(); + this.mainFrustum = new CSMFrustum( { webGL: true } ); this.frustums = []; this.breaks = []; diff --git a/examples/jsm/csm/CSMFrustum.js b/examples/jsm/csm/CSMFrustum.js index 2d968bebdb929f..cd5268d0cdd935 100644 --- a/examples/jsm/csm/CSMFrustum.js +++ b/examples/jsm/csm/CSMFrustum.js @@ -8,6 +8,8 @@ class CSMFrustum { data = data || {}; + this.zNear = data.webGL === true ? - 1 : 0; + this.vertices = { near: [ new Vector3(), @@ -33,6 +35,7 @@ class CSMFrustum { setFromProjectionMatrix( projectionMatrix, maxFar ) { + const zNear = this.zNear; const isOrthographic = projectionMatrix.elements[ 2 * 4 + 3 ] === 0; inverseProjectionMatrix.copy( projectionMatrix ).invert(); @@ -42,10 +45,10 @@ class CSMFrustum { // 2 --- 1 // clip space spans from [-1, 1] - this.vertices.near[ 0 ].set( 1, 1, - 1 ); - this.vertices.near[ 1 ].set( 1, - 1, - 1 ); - this.vertices.near[ 2 ].set( - 1, - 1, - 1 ); - this.vertices.near[ 3 ].set( - 1, 1, - 1 ); + this.vertices.near[ 0 ].set( 1, 1, zNear ); + this.vertices.near[ 1 ].set( 1, - 1, zNear ); + this.vertices.near[ 2 ].set( - 1, - 1, zNear ); + this.vertices.near[ 3 ].set( - 1, 1, zNear ); this.vertices.near.forEach( function ( v ) { v.applyMatrix4( inverseProjectionMatrix ); diff --git a/examples/jsm/csm/CSMHelper.js b/examples/jsm/csm/CSMHelper.js index d1cfed874bb836..c05ba4c8043795 100644 --- a/examples/jsm/csm/CSMHelper.js +++ b/examples/jsm/csm/CSMHelper.js @@ -78,6 +78,8 @@ class CSMHelper extends Group { const cascadePlanes = this.cascadePlanes; const shadowLines = this.shadowLines; + if ( camera === null ) return; + this.position.copy( camera.position ); this.quaternion.copy( camera.quaternion ); this.scale.copy( camera.scale ); diff --git a/examples/jsm/csm/CSMShadowNode.js b/examples/jsm/csm/CSMShadowNode.js new file mode 100644 index 00000000000000..d1d476f86dc786 --- /dev/null +++ b/examples/jsm/csm/CSMShadowNode.js @@ -0,0 +1,435 @@ +import { + Vector2, + Vector3, + MathUtils, + Matrix4, + Box3, + Object3D, + WebGLCoordinateSystem +} from 'three'; + +import { CSMFrustum } from './CSMFrustum.js'; +import { reference, uniform, int, float, vec4, vec2, If, Fn, min, renderGroup, positionView, Node, NodeUpdateType, shadow } from 'three/tsl'; + +const _cameraToLightMatrix = new Matrix4(); +const _lightSpaceFrustum = new CSMFrustum(); +const _center = new Vector3(); +const _bbox = new Box3(); +const _uniformArray = []; +const _logArray = []; +const _lightDirection = new Vector3(); +const _lightOrientationMatrix = new Matrix4(); +const _lightOrientationMatrixInverse = new Matrix4(); +const _up = new Vector3( 0, 1, 0 ); + +class LwLight extends Object3D { + + constructor() { + + super(); + + this.target = new Object3D(); + + } + +} + +class CSMShadowNode extends Node { + + constructor( light, data = {} ) { + + super(); + + this.light = light; + this.camera = null; + this.cascades = data.cascades || 3; + this.maxFar = data.maxFar || 100000; + this.mode = data.mode || 'practical'; + this.lightMargin = data.lightMargin || 200; + this.customSplitsCallback = data.customSplitsCallback; + + this.fade = false; + + this.breaks = []; + + this._cascades = []; + this.mainFrustum = null; + this.frustums = []; + this.updateBeforeType = NodeUpdateType.FRAME; + + this.lights = []; + + this._shadowNodes = []; + + } + + init( { camera, renderer } ) { + + this.camera = camera; + + const data = { webGL: renderer.coordinateSystem === WebGLCoordinateSystem }; + this.mainFrustum = new CSMFrustum( data ); + + const light = this.light; + const parent = light.parent; + + for ( let i = 0; i < this.cascades; i ++ ) { + + const lwLight = new LwLight(); + const lShadow = light.shadow.clone(); + + this.lights.push( lwLight ); + + parent.add( lwLight ); + parent.add( lwLight.target ); + + lwLight.shadow = lShadow; + + this._shadowNodes.push( shadow( lwLight, lShadow ) ); + + this._cascades.push( new Vector2() ); + + } + + this.updateFrustums(); + + } + + initCascades() { + + const camera = this.camera; + camera.updateProjectionMatrix(); + + this.mainFrustum.setFromProjectionMatrix( camera.projectionMatrix, this.maxFar ); + this.mainFrustum.split( this.breaks, this.frustums ); + + } + + getBreaks() { + + const camera = this.camera; + const far = Math.min( camera.far, this.maxFar ); + + this.breaks.length = 0; + + switch ( this.mode ) { + + case 'uniform': + uniformSplit( this.cascades, camera.near, far, this.breaks ); + break; + + case 'logarithmic': + logarithmicSplit( this.cascades, camera.near, far, this.breaks ); + break; + + case 'practical': + practicalSplit( this.cascades, camera.near, far, 0.5, this.breaks ); + break; + + case 'custom': + if ( this.customSplitsCallback === undefined ) console.error( 'CSM: Custom split scheme callback not defined.' ); + this.customSplitsCallback( this.cascades, camera.near, far, this.breaks ); + break; + + } + + function uniformSplit( amount, near, far, target ) { + + for ( let i = 1; i < amount; i ++ ) { + + target.push( ( near + ( far - near ) * i / amount ) / far ); + + } + + target.push( 1 ); + + } + + function logarithmicSplit( amount, near, far, target ) { + + for ( let i = 1; i < amount; i ++ ) { + + target.push( ( near * ( far / near ) ** ( i / amount ) ) / far ); + + } + + target.push( 1 ); + + } + + function practicalSplit( amount, near, far, lambda, target ) { + + _uniformArray.length = 0; + _logArray.length = 0; + logarithmicSplit( amount, near, far, _logArray ); + uniformSplit( amount, near, far, _uniformArray ); + + for ( let i = 1; i < amount; i ++ ) { + + target.push( MathUtils.lerp( _uniformArray[ i - 1 ], _logArray[ i - 1 ], lambda ) ); + + } + + target.push( 1 ); + + } + + } + + setLightBreaks() { + + for ( let i = 0, l = this.cascades; i < l; i ++ ) { + + const amount = this.breaks[ i ]; + const prev = this.breaks[ i - 1 ] || 0; + + this._cascades[ i ].set( prev, amount ); + + } + + } + + updateShadowBounds() { + + const frustums = this.frustums; + + for ( let i = 0; i < frustums.length; i ++ ) { + + const shadowCam = this.lights[ i ].shadow.camera; + const frustum = this.frustums[ i ]; + + // Get the two points that represent that furthest points on the frustum assuming + // that's either the diagonal across the far plane or the diagonal across the whole + // frustum itself. + const nearVerts = frustum.vertices.near; + const farVerts = frustum.vertices.far; + const point1 = farVerts[ 0 ]; + + let point2; + + if ( point1.distanceTo( farVerts[ 2 ] ) > point1.distanceTo( nearVerts[ 2 ] ) ) { + + point2 = farVerts[ 2 ]; + + } else { + + point2 = nearVerts[ 2 ]; + + } + + let squaredBBWidth = point1.distanceTo( point2 ); + + if ( this.fade ) { + + // expand the shadow extents by the fade margin if fade is enabled. + const camera = this.camera; + const far = Math.max( camera.far, this.maxFar ); + const linearDepth = frustum.vertices.far[ 0 ].z / ( far - camera.near ); + const margin = 0.25 * Math.pow( linearDepth, 2.0 ) * ( far - camera.near ); + + squaredBBWidth += margin; + + } + + shadowCam.left = - squaredBBWidth / 2; + shadowCam.right = squaredBBWidth / 2; + shadowCam.top = squaredBBWidth / 2; + shadowCam.bottom = - squaredBBWidth / 2; + shadowCam.updateProjectionMatrix(); + + } + + } + + updateFrustums() { + + this.getBreaks(); + this.initCascades(); + this.updateShadowBounds(); + this.setLightBreaks(); + + } + + setupFade() { + + const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup ).label( 'cameraNear' ); + const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).label( 'cacades' ); + + const shadowFar = uniform( 'float' ).setGroup( renderGroup ).label( 'shadowFar' ) + .onRenderUpdate( () => Math.min( this.maxFar, this.camera.far ) ); + + const linearDepth = positionView.z.div( shadowFar.sub( cameraNear ) ).negate().toVar( 'lDepth' ); + const lastCascade = this.cascades - 1; + + return Fn( () => { + + const ret = vec4( 1, 1, 1, 1 ).toVar( 'shadowValue' ); + + const cascade = vec2().toVar( 'cascade' ); + const cascadeCenter = float().toVar( 'cascadeCenter' ); + + const margin = float().toVar( 'margin' ); + + const csmX = float().toVar( 'csmX' ); + const csmY = float().toVar( 'csmY' ); + + for ( let i = 0; i < this.cascades; i ++ ) { + + const isLastCascade = i === lastCascade; + + cascade.assign( cascades.element( i ) ); + + cascadeCenter.assign( cascade.x.add( cascade.y ).div( 2.0 ) ); + + const closestEdge = linearDepth.lessThan( cascadeCenter ).select( cascade.x, cascade.y ); + + margin.assign( float( 0.25 ).mul( closestEdge.pow( 2.0 ) ) ); + + csmX.assign( cascade.x.sub( margin.div( 2.0 ) ) ); + + if ( isLastCascade ) { + + csmY.assign( cascade.y ); + + } else { + + csmY.assign( cascade.y.add( margin.div( 2.0 ) ) ); + + } + + const inRange = linearDepth.greaterThanEqual( csmX ).and( linearDepth.lessThan( csmY ) ); + + If( inRange, () => { + + const dist = min( linearDepth.sub( csmX ), csmY.sub( linearDepth ) ); + + let ratio = dist.div( margin ).clamp( 0.0, 1.0 ); + + if ( i === 0 ) { + + // dont fade at nearest edge + ratio = linearDepth.greaterThan( cascadeCenter ).select( ratio, 1 ); + + } + + ret.subAssign( this._shadowNodes[ i ].oneMinus().mul( ratio ) ); + + } ); + + } + + return ret; + + } )(); + + } + + setupStandard() { + + const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup ).label( 'cameraNear' ); + const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).label( 'cacades' ); + + const shadowFar = uniform( 'float' ).setGroup( renderGroup ).label( 'shadowFar' ) + .onRenderUpdate( () => Math.min( this.maxFar, this.camera.far ) ); + + const linearDepth = positionView.z.div( shadowFar.sub( cameraNear ) ).negate().toVar( 'lDepth' ); + const lastCascade = this.cascades - 1; + + return Fn( () => { + + const ret = vec4( 1, 1, 1, 1 ).toVar( 'shadowValue' ); + const cascade = vec2().toVar( 'cascade' ); + + for ( let i = 0; i < this.cascades; i ++ ) { + + cascade.assign( cascades.element( i ) ); + + If( linearDepth.greaterThanEqual( cascade.x ).and( linearDepth.lessThan( cascade.y ).or( int( i ).equal( float( lastCascade ) ) ) ), () => { + + linearDepth.lessThan( cascade.y ).select( ret.assign( this._shadowNodes[ i ] ) ); + + } ); + + } + + return ret; + + } )(); + + } + + setup( builder ) { + + if ( this.camera === null ) this.init( builder ); + + return this.fade === true ? this.setupFade() : this.setupStandard(); + + } + + updateBefore( /*builder*/ ) { + + const light = this.light; + const camera = this.camera; + const frustums = this.frustums; + + _lightDirection.subVectors( light.target.position, light.position ); + + // for each frustum we need to find its min-max box aligned with the light orientation + // the position in _lightOrientationMatrix does not matter, as we transform there and back + _lightOrientationMatrix.lookAt( light.position, light.target.position, _up ); + _lightOrientationMatrixInverse.copy( _lightOrientationMatrix ).invert(); + + for ( let i = 0; i < frustums.length; i ++ ) { + + const lwLight = this.lights[ i ]; + const shadow = lwLight.shadow; + const shadowCam = shadow.camera; + const texelWidth = ( shadowCam.right - shadowCam.left ) / shadow.mapSize.width; + const texelHeight = ( shadowCam.top - shadowCam.bottom ) / shadow.mapSize.height; + + _cameraToLightMatrix.multiplyMatrices( _lightOrientationMatrixInverse, camera.matrixWorld ); + frustums[ i ].toSpace( _cameraToLightMatrix, _lightSpaceFrustum ); + + const nearVerts = _lightSpaceFrustum.vertices.near; + const farVerts = _lightSpaceFrustum.vertices.far; + + _bbox.makeEmpty(); + + for ( let j = 0; j < 4; j ++ ) { + + _bbox.expandByPoint( nearVerts[ j ] ); + _bbox.expandByPoint( farVerts[ j ] ); + + } + + _bbox.getCenter( _center ); + _center.z = _bbox.max.z + this.lightMargin; + _center.x = Math.floor( _center.x / texelWidth ) * texelWidth; + _center.y = Math.floor( _center.y / texelHeight ) * texelHeight; + _center.applyMatrix4( _lightOrientationMatrix ); + + lwLight.position.copy( _center ); + lwLight.target.position.copy( _center ); + lwLight.target.position.add( _lightDirection ); + + } + + } + + dispose() { + + for ( let i = 0; i < this.lights.length; i ++ ) { + + const light = this.lights[ i ]; + const parent = light.parent; + + parent.remove( light.target ); + parent.remove( light ); + + } + + } + +} + +export { CSMShadowNode }; diff --git a/examples/screenshots/webgpu_shadowmap_csm.jpg b/examples/screenshots/webgpu_shadowmap_csm.jpg new file mode 100644 index 00000000000000..fe44d2d630946b Binary files /dev/null and b/examples/screenshots/webgpu_shadowmap_csm.jpg differ diff --git a/examples/webgpu_shadowmap_csm.html b/examples/webgpu_shadowmap_csm.html new file mode 100644 index 00000000000000..efa7151f83da46 --- /dev/null +++ b/examples/webgpu_shadowmap_csm.html @@ -0,0 +1,319 @@ + + + + three.js webgpu - cascaded shadow maps + + + + + + +
+
+ three.js webgpu - cascaded shadow maps
+ by StrandedKitty (original repository) +
+ + + + + + + diff --git a/src/nodes/lighting/AnalyticLightNode.js b/src/nodes/lighting/AnalyticLightNode.js index 71bd59b8d55975..26ed9bf7179c7c 100644 --- a/src/nodes/lighting/AnalyticLightNode.js +++ b/src/nodes/lighting/AnalyticLightNode.js @@ -5,6 +5,7 @@ import { Color } from '../../math/Color.js'; import { renderGroup } from '../core/UniformGroupNode.js'; import { hash } from '../core/NodeUtils.js'; import { shadow } from './ShadowNode.js'; +import { nodeObject } from '../tsl/TSLCore.js'; class AnalyticLightNode extends LightingNode { @@ -56,7 +57,19 @@ class AnalyticLightNode extends LightingNode { if ( shadowColorNode === null ) { - const shadowNode = shadow( this.light ); + const customShadowNode = this.light.shadow.shadowNode; + + let shadowNode; + + if ( customShadowNode !== undefined ) { + + shadowNode = nodeObject( customShadowNode ); + + } else { + + shadowNode = shadow( this.light ); + + } this.shadowNode = shadowNode;