UNPKG

three

Version:

JavaScript 3D library

457 lines (325 loc) 12.3 kB
import { Vector3, Object3D, ShadowBaseNode, Plane, Line3, DepthArrayTexture, LessCompare, Vector2, RedFormat, ArrayCamera, VSMShadowMap, RendererUtils, Quaternion } from 'three/webgpu'; import { min, Fn, shadow, NodeUpdateType, getShadowMaterial, getShadowRenderObjectFunction } from 'three/tsl'; const { resetRendererAndSceneState, restoreRendererAndSceneState } = RendererUtils; let _rendererState; const _cameraLayers = []; const _vec3Temp1 = /*@__PURE__*/ new Vector3(); const _vec3Temp2 = /*@__PURE__*/ new Vector3(); const _vec3Temp3 = /*@__PURE__*/ new Vector3(); const _quatTemp1 = /*@__PURE__*/ new Quaternion(); class LwLight extends Object3D { constructor() { super(); this.target = new Object3D(); } } /** * A class that extends `ShadowBaseNode` to implement tiled shadow mapping. * This allows splitting a shadow map into multiple tiles, each with its own light and camera, * to improve shadow quality and performance for large scenes. * * **Note:** This class does not support `VSMShadowMap` at the moment. * * @class * @augments ShadowBaseNode * @three_import import { TileShadowNode } from 'three/addons/tsl/shadows/TileShadowNode.js'; */ class TileShadowNode extends ShadowBaseNode { /** * Creates an instance of `TileShadowNode`. * * @param {Light} light - The original light source used for shadow mapping. * @param {Object} [options={}] - Configuration options for the tiled shadow node. * @param {number} [options.tilesX=2] - The number of tiles along the X-axis. * @param {number} [options.tilesY=2] - The number of tiles along the Y-axis. * @param {Object} [options.resolution] - The resolution of the shadow map. * @param {boolean} [options.debug=false] - Whether to enable debug mode. */ constructor( light, options = {} ) { super( light ); // Default configuration with sensible defaults this.config = { tilesX: options.tilesX || 2, tilesY: options.tilesY || 2, resolution: options.resolution || light.shadow.mapSize, debug: options.debug !== undefined ? options.debug : false }; this.debug = this.config.debug; this.originalLight = light; this.lightPlane = new Plane( new Vector3( 0, 1, 0 ), 0 ); this.line = new Line3(); this.initialLightDirection = new Vector3(); this.updateLightDirection(); this._cameraFrameId = new WeakMap(); this.shadowSize = { top: light.shadow.camera.top, bottom: light.shadow.camera.bottom, left: light.shadow.camera.left, right: light.shadow.camera.right, }; this.lights = []; this._shadowNodes = []; this.tiles = this.generateTiles( this.config.tilesX, this.config.tilesY ); } /** * Generates the tiles for the shadow map based on the specified number of tiles along the X and Y axes. * * @param {number} tilesX - The number of tiles along the X-axis. * @param {number} tilesY - The number of tiles along the Y-axis. * @returns {Array<Object>} An array of tile objects, each containing the tile's bounds and index. */ generateTiles( tilesX, tilesY ) { const tiles = []; const tileWidth = 1 / tilesX; const tileHeight = 1 / tilesY; for ( let y = 0; y < tilesY; y ++ ) { for ( let x = 0; x < tilesX; x ++ ) { tiles.push( { x: [ x * tileWidth, ( x + 1 ) * tileWidth ], y: [ ( tilesY - 1 - y ) * tileHeight, ( tilesY - y ) * tileHeight ], // Start from top row index: y * tilesX + x } ); } } return tiles; } /** * Updates the initial light direction based on the light's target position. */ updateLightDirection() { this.initialLightDirection.subVectors( this.originalLight.target.getWorldPosition( new Vector3() ), this.originalLight.getWorldPosition( new Vector3() ) ).normalize(); } /** * Initializes the tiled shadow node by creating lights, cameras, and shadow maps for each tile. * * @param {Builder} builder - The builder used to create render targets and other resources. */ init( builder ) { const light = this.originalLight; const parent = light.parent; const width = this.shadowSize.right - this.shadowSize.left; const height = this.shadowSize.top - this.shadowSize.bottom; const tileCount = this.tiles.length; const shadowWidth = this.config.resolution.width; const shadowHeight = this.config.resolution.height; // Clear existing lights/nodes if re-initializing this.disposeLightsAndNodes(); const depthTexture = new DepthArrayTexture( shadowWidth, shadowHeight, tileCount ); depthTexture.compareFunction = LessCompare; depthTexture.name = 'ShadowDepthArrayTexture'; const shadowMap = builder.createRenderTargetArray( shadowWidth, shadowHeight, tileCount, { format: RedFormat } ); shadowMap.depthTexture = depthTexture; shadowMap.texture.name = 'ShadowTexture'; this.shadowMap = shadowMap; const cameras = []; // Create lights, one for each tile for ( let i = 0; i < tileCount; i ++ ) { const lwLight = new LwLight(); lwLight.castShadow = true; const lShadow = light.shadow.clone(); lShadow.filterNode = light.shadow.filterNode; const tile = this.tiles[ i ]; lShadow.camera.left = this.shadowSize.left + width * tile.x[ 0 ]; lShadow.camera.right = this.shadowSize.left + width * tile.x[ 1 ]; lShadow.camera.top = this.shadowSize.bottom + height * tile.y[ 1 ]; lShadow.camera.bottom = this.shadowSize.bottom + height * tile.y[ 0 ]; lShadow.bias = light.shadow.bias; lShadow.camera.near = light.shadow.camera.near; lShadow.camera.far = light.shadow.camera.far; lShadow.camera.userData.tileIndex = i; lwLight.shadow = lShadow; if ( parent ) { parent.add( lwLight ); parent.add( lwLight.target ); } else { console.warn( 'TileShadowNode: Original light has no parent during init. Tile lights not added to scene graph directly.' ); } this.syncLightTransformation( lwLight, light ); this.lights.push( lwLight ); lShadow.camera.updateMatrixWorld(); cameras.push( lShadow.camera ); const shadowNode = shadow( lwLight, lShadow ); shadowNode.depthLayer = i; shadowNode.updateBeforeType = NodeUpdateType.NONE; shadowNode.setupRenderTarget = () => { return { shadowMap, depthTexture }; }; this._shadowNodes.push( shadowNode ); } const cameraArray = new ArrayCamera( cameras ); this.cameraArray = cameraArray; } /** * Updates the light transformations and shadow cameras for each tile. */ update() { const light = this.originalLight; const shadowCam = light.shadow.camera; const lsMin = new Vector2( shadowCam.left, shadowCam.bottom ); const lsMax = new Vector2( shadowCam.right, shadowCam.top ); const fullWidth = lsMax.x - lsMin.x; const fullHeight = lsMax.y - lsMin.y; for ( let i = 0; i < this.lights.length; i ++ ) { const lwLight = this.lights[ i ]; const tile = this.tiles[ i ]; this.syncLightTransformation( lwLight, light ); const lShadow = lwLight.shadow; const tileLeft = lsMin.x + tile.x[ 0 ] * fullWidth; const tileRight = lsMin.x + tile.x[ 1 ] * fullWidth; const tileBottom = lsMin.y + tile.y[ 0 ] * fullHeight; const tileTop = lsMin.y + tile.y[ 1 ] * fullHeight; lShadow.camera.left = tileLeft; lShadow.camera.right = tileRight; lShadow.camera.bottom = tileBottom; lShadow.camera.top = tileTop; lShadow.camera.near = light.shadow.camera.near; lShadow.camera.far = light.shadow.camera.far; lShadow.camera.updateProjectionMatrix(); lShadow.camera.updateWorldMatrix( true, false ); lShadow.camera.updateMatrixWorld( true ); this._shadowNodes[ i ].shadow.needsUpdate = true; } } /** * Updates the shadow map rendering. * @param {NodeFrame} frame - A reference to the current node frame. */ updateShadow( frame ) { const { shadowMap, light } = this; const { renderer, scene, camera } = frame; const shadowType = renderer.shadowMap.type; const depthVersion = shadowMap.depthTexture.version; this._depthVersionCached = depthVersion; const currentRenderObjectFunction = renderer.getRenderObjectFunction(); const currentMRT = renderer.getMRT(); const useVelocity = currentMRT ? currentMRT.has( 'velocity' ) : false; _rendererState = resetRendererAndSceneState( renderer, scene, _rendererState ); scene.overrideMaterial = getShadowMaterial( light ); renderer.setRenderTarget( this.shadowMap ); for ( let index = 0; index < this.lights.length; index ++ ) { const light = this.lights[ index ]; const shadow = light.shadow; const _shadowCameraLayer = shadow.camera.layers.mask; _cameraLayers.push( _shadowCameraLayer ); if ( ( shadow.camera.layers.mask & 0xFFFFFFFE ) === 0 ) { shadow.camera.layers.mask = camera.layers.mask; } shadow.updateMatrices( light ); renderer.setRenderObjectFunction( getShadowRenderObjectFunction( renderer, shadow, shadowType, useVelocity ) ); this.shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height, shadowMap.depth ); } renderer.render( scene, this.cameraArray ); renderer.setRenderObjectFunction( currentRenderObjectFunction ); if ( light.isPointLight !== true && shadowType === VSMShadowMap ) { console.warn( 'THREE.TileShadowNode: VSM shadow map is not supported yet.' ); // this.vsmPass( renderer ); } restoreRendererAndSceneState( renderer, scene, _rendererState ); for ( let index = 0; index < this.lights.length; index ++ ) { const light = this.lights[ index ]; const shadow = light.shadow; shadow.camera.layers.mask = _cameraLayers[ index ]; } _cameraLayers.length = 0; } /** * The implementation performs the update of the shadow map if necessary. * * @param {NodeFrame} frame - A reference to the current node frame. */ updateBefore( frame ) { const shadow = this.originalLight.shadow; let needsUpdate = shadow.needsUpdate || shadow.autoUpdate; if ( needsUpdate ) { if ( this._cameraFrameId[ frame.camera ] === frame.frameId ) { needsUpdate = false; } this._cameraFrameId[ frame.camera ] = frame.frameId; } if ( needsUpdate ) { this.update(); this.updateShadow( frame ); if ( this.shadowMap.depthTexture.version === this._depthVersionCached ) { shadow.needsUpdate = false; } } } /** * Synchronizes the transformation of a tile light with the source light. * * @param {LwLight} lwLight - The tile light to synchronize. * @param {Light} sourceLight - The source light to copy transformations from. */ syncLightTransformation( lwLight, sourceLight ) { const sourceWorldPos = sourceLight.getWorldPosition( _vec3Temp1 ); const targetWorldPos = sourceLight.target.getWorldPosition( _vec3Temp2 ); const forward = _vec3Temp3.subVectors( targetWorldPos, sourceWorldPos ); const targetDistance = forward.length(); forward.normalize(); lwLight.position.copy( sourceWorldPos ); lwLight.target.position.copy( sourceWorldPos ).add( forward.multiplyScalar( targetDistance ) ); lwLight.quaternion.copy( sourceLight.getWorldQuaternion( _quatTemp1 ) ); lwLight.scale.copy( sourceLight.scale ); lwLight.updateMatrix(); lwLight.updateMatrixWorld( true ); lwLight.target.updateMatrix(); lwLight.target.updateMatrixWorld( true ); } /** * Sets up the shadow node for rendering. * * @param {Builder} builder - The builder used to set up the shadow node. * @returns {Node} A node representing the shadow value. */ setup( builder ) { if ( this.lights.length === 0 ) { this.init( builder ); } return Fn( ( builder ) => { this.setupShadowPosition( builder ); return min( ...this._shadowNodes ).toVar( 'shadowValue' ); } )(); } /** * Helper method to remove lights and associated nodes/targets. * Used internally during dispose and potential re-initialization. */ disposeLightsAndNodes() { for ( const light of this.lights ) { const parent = light.parent; if ( parent ) { parent.remove( light.target ); parent.remove( light ); } } this.lights = []; this._shadowNodes = []; if ( this.shadowMap ) { this.shadowMap.dispose(); // Disposes render target and textures this.shadowMap = null; } } dispose() { // Dispose lights, nodes, and shadow map this.disposeLightsAndNodes(); super.dispose(); } } export { TileShadowNode };