UNPKG

three

Version:

JavaScript 3D library

313 lines (217 loc) 8.82 kB
import { Compatibility, DepthTexture, GreaterEqualCompare, LessEqualCompare, LinearFilter, NearestFilter, PCFShadowMap, ShadowNode, Vector2, VSMShadowMap } from 'three/webgpu'; import { Fn, If, float, mix, normalWorld, positionView, reference, renderGroup, shadowPositionWorld, smoothstep, texture, uniform, vec4 } from 'three/tsl'; let _vsmWarned = false; /** * Represents the cascaded shadow map of a {@link SunLight}. * * The two cascade cameras are fitted by {@link SunLightShadow} and rendered * into the viewports of a single shadow map atlas. Each fragment walks the * cascades back to front, blending across the fade bands between them. * * @augments ShadowNode * @three_import import { sunShadow } from 'three/addons/lights/SunShadowNode.js'; */ class SunShadowNode extends ShadowNode { static get type() { return 'SunShadowNode'; } /** * Constructs a new sun shadow node. * * @param {SunLight} light - The shadow casting sun light. * @param {?SunLightShadow} [shadow=null] - An optional sun light shadow. */ constructor( light, shadow = null ) { super( light, shadow ); /** * The size of the shadow map atlas. * * @type {Vector2} * @private */ this._atlasSize = new Vector2(); const lightShadow = this.shadow; /** * Shadow filters read their texel offsets from `mapSize`, so they must * be given the atlas size rather than the cascade map size. This proxy * stands in for the shadow when the filter function is set up. * * @type {Object} * @private */ this._filterShadow = { mapSize: this._atlasSize, get radius() { return lightShadow.radius; } }; } /** * Overwrites the default implementation to size the render target as the cascade atlas. * * @param {SunLightShadow} shadow - The light shadow object. * @param {NodeBuilder} builder - A reference to the current node builder. * @return {Object} An object containing the shadow map and depth texture. */ setupRenderTarget( shadow, builder ) { const frameExtents = shadow.getFrameExtents(); const width = shadow.mapSize.width * frameExtents.x; const height = shadow.mapSize.height * frameExtents.y; const depthTexture = new DepthTexture( width, height ); depthTexture.name = 'SunShadowDepthTexture'; depthTexture.compareFunction = builder.renderer.reversedDepthBuffer ? GreaterEqualCompare : LessEqualCompare; const shadowMap = builder.createRenderTarget( width, height ); shadowMap.texture.name = 'SunShadowMap'; shadowMap.texture.type = shadow.mapType; shadowMap.depthTexture = depthTexture; return { shadowMap, depthTexture }; } /** * Builds the per-fragment cascade blending. * * @private * @param {NodeBuilder} builder - A reference to the current node builder. * @param {Function} filterFn - The shadow filtering function. * @return {Node<float>} The shadow value node. */ _setupCascades( builder, filterFn ) { // Full node updates refresh resized atlas bindings, even for stationary objects. builder.observer.hasNode = true; const { shadow, shadowMap } = this; const depthTexture = shadowMap.depthTexture; const shadowIntensity = reference( 'intensity', 'float', shadow ).setGroup( renderGroup ); const normalBias = reference( 'normalBias', 'float', shadow ).setGroup( renderGroup ); // evaluated outside the cascade branches so nodes shared with the rest of // the shader, like the world normal, are not trapped in a branch scope const shadowPosition = vec4( shadowPositionWorld.add( normalWorld.mul( normalBias ) ), 1 ).toVar(); const viewDepth = positionView.z.negate().toVar(); const shadowValue = float( 1 ).toVar( 'shadowValue' ); // walk the cascades back to front so each fade band can blend with the shadow behind it for ( let i = shadow.getViewportCount() - 1; i >= 0; i -- ) { const shadowMatrix = uniform( 'mat4' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.getMatrix( i ) ); // ( begin, end, fade start ) view depths of the cascade const cascade = uniform( 'vec4' ).setGroup( renderGroup ).onRenderUpdate( () => shadow._cascadeData[ i ] ); If( viewDepth.greaterThanEqual( cascade.x ).and( viewDepth.lessThan( cascade.y ) ), () => { const shadowCoord = this.setupShadowCoord( builder, shadowMatrix.mul( shadowPosition ) ); const cascadeShadow = this.setupShadowFilter( builder, { filterFn, shadowTexture: shadowMap.texture, depthTexture, shadowCoord, shadow: this._filterShadow, depthLayer: this.depthLayer } ); shadowValue.assign( mix( cascadeShadow, shadowValue, smoothstep( cascade.z, cascade.y, viewDepth ) ) ); } ); } return mix( 1, shadowValue, shadowIntensity ); } /** * Sets up the atlas render target and shadow output node. * * @param {NodeBuilder} builder - A reference to the current node builder. * @return {Node<float>} The shadow output node. */ setupShadow( builder ) { const { renderer, camera } = builder; const { light, shadow } = this; let shadowMapType = renderer.shadowMap.type; if ( shadowMapType === VSMShadowMap ) { // the cascade atlas cannot be blurred as one map if ( _vsmWarned === false ) { console.warn( 'THREE.SunShadowNode: VSM is not supported, falling back to PCF.' ); _vsmWarned = true; } shadowMapType = PCFShadowMap; } const filterFn = shadow.filterNode || this.getShadowFilterFn( shadowMapType ) || null; if ( filterFn === null ) { throw new Error( 'THREE.SunShadowNode: Shadow map type not supported.' ); } const { depthTexture, shadowMap } = this.setupRenderTarget( shadow, builder ); const hasTextureCompare = renderer.hasCompatibility( Compatibility.TEXTURE_COMPARE ); if ( shadowMapType === PCFShadowMap && hasTextureCompare ) { depthTexture.minFilter = LinearFilter; depthTexture.magFilter = LinearFilter; } else { depthTexture.minFilter = NearestFilter; depthTexture.magFilter = NearestFilter; } this.shadowMap = shadowMap; shadow.map = shadowMap; this._atlasSize.set( shadowMap.width, shadowMap.height ); // the cascade cameras inherit the coordinate system and depth mode from the shadow camera shadow.camera.coordinateSystem = camera.coordinateSystem; shadow.camera._reversedDepth = renderer.reversedDepthBuffer; // rebuild the cascade statements for each shader so their variables // and branches belong to that shader's scope const node = Fn( ( builder ) => this._setupCascades( builder, filterFn ) )().toVar(); const inspectName = `${ light.type } Shadow [ ${ light.name || 'ID: ' + light.id } ]`; // the cascade cameras are orthographic, so the stored depth is linear already return node.toInspector( `${ inspectName } / Depth`, () => texture( depthTexture ).r.oneMinus() ); } /** * Renders the two cascades into the viewports of the shadow map atlas. * * @param {NodeFrame} frame - A reference to the current node frame. */ renderShadow( frame ) { const { shadow, shadowMap, light } = this; const { renderer, scene, camera } = frame; shadow.updateMatrices( light, camera ); const tileWidth = shadow.mapSize.width; const tileHeight = shadow.mapSize.height; const frameExtents = shadow.getFrameExtents(); const width = tileWidth * frameExtents.x; const height = tileHeight * frameExtents.y; shadowMap.setSize( width, height ); this._atlasSize.set( width, height ); const currentSceneName = scene.name; scene.name = `Shadow Map [ ${ light.name || 'ID: ' + light.id } ]`; // clear once, the cascades share the atlas renderer.autoClear = false; renderer.clear(); const cascadeCount = shadow.getViewportCount(); for ( let i = 0; i < cascadeCount; i ++ ) { const viewport = shadow.getViewport( i ); // viewports are measured from the top: flip the rows so the shadow // matrices, which bake their offsets bottom-up, sample the correct tile shadowMap.viewport.set( tileWidth * viewport.x, shadowMap.height - tileHeight * ( viewport.y + viewport.w ), tileWidth * viewport.z, tileHeight * viewport.w ); renderer.render( scene, shadow.getCamera( i ) ); } scene.name = currentSceneName; } /** * Overwritten as a no-op since VSM is not supported for cascaded shadow maps. * * @param {Renderer} renderer - A reference to the current renderer. */ vsmPass( /*renderer*/ ) { } } export { SunShadowNode }; /** * TSL function for creating an instance of `SunShadowNode`. * * @tsl * @function * @param {SunLight} light - The shadow casting sun light. * @param {?SunLightShadow} [shadow=null] - An optional sun light shadow. * @return {SunShadowNode} The created sun shadow node. */ export const sunShadow = ( light, shadow ) => new SunShadowNode( light, shadow );