main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

View File

@@ -0,0 +1,456 @@
import {
Vector3,
Object3D,
ShadowBaseNode,
Plane,
Line3,
DepthTexture,
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 DepthTexture( shadowWidth, shadowHeight, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, tileCount );
depthTexture.compareFunction = LessCompare;
depthTexture.name = 'ShadowDepthArrayTexture';
const shadowMap = builder.createRenderTarget( shadowWidth, shadowHeight, { format: RedFormat, depth: tileCount } );
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 };

View File

@@ -0,0 +1,212 @@
import { Group, NodeMaterial, Mesh, PlaneGeometry, DoubleSide, CameraHelper } from 'three/webgpu';
import { Fn, vec4, vec3, texture, uv, positionLocal, vec2, float, screenSize } from 'three/tsl';
/**
* Helper class to manage and display debug visuals for TileShadowNode.
*
* @augments Group
* @three_import import { TileShadowNodeHelper } from 'three/addons/tsl/shadows/TileShadowNodeHelper.js';
*/
class TileShadowNodeHelper extends Group {
/**
* @param {TileShadowNode} tileShadowNode The TileShadowNode instance to debug.
*/
constructor( tileShadowNode ) {
super();
if ( ! tileShadowNode ) {
throw new Error( 'TileShadowNode instance is required for TileShadowNodeHelper.' );
}
this.tileShadowNode = tileShadowNode;
this.config = tileShadowNode.config;
this.tiles = tileShadowNode.tiles;
this._debugMeshes = [];
this._shadowCamHelpers = [];
this.initialized = false;
}
/**
* Initializes the debug displays (planes and camera helpers).
* Should be called after TileShadowNode has initialized its lights and shadow nodes.
*/
init() {
if ( this.tileShadowNode._shadowNodes.length !== this.tiles.length ) {
console.error( 'Cannot initialize TileShadowNodeHelper: Shadow nodes not ready or mismatch count.' );
return;
}
const tilesX = this.config.tilesX;
const tilesY = this.config.tilesY;
// Clear previous helpers if any (e.g., during a re-init)
this.dispose();
// Create a display for each shadow map tile
for ( let i = 0; i < this.tiles.length; i ++ ) {
// Create display plane
const display = new Mesh( new PlaneGeometry( 1, 1 ), new NodeMaterial() );
display.renderOrder = 9999999; // Ensure they appear on top
display.material.transparent = true;
display.frustumCulled = false;
display.side = DoubleSide;
display.material.depthTest = false; // Disable depth testing
display.material.depthWrite = false; // Disable depth writing
const col = i % tilesX;
const row = Math.floor( i / tilesX );
// Vertex shader logic for positioning the debug quad
display.material.vertexNode = Fn( () => {
const aspectRatio = screenSize.x.div( screenSize.y );
const maxTiles = Math.max( tilesX, tilesY );
const displaySize = float( 0.8 / maxTiles ); // Size adapts to number of tiles
const margin = float( 0.01 );
const cornerOffset = float( 0.05 );
// Position tiles left-to-right, top-to-bottom
const xBase = float( - 1.0 ).add( cornerOffset ).add(
displaySize.div( 2 ).div( aspectRatio )
).add( float( col ).mul( displaySize.div( aspectRatio ).add( margin ) ) );
const yBase = float( 1.0 ).sub( cornerOffset ).sub(
displaySize.div( 2 )
).sub( float( row ).mul( displaySize.add( margin ) ) );
const scaledPos = vec2(
positionLocal.x.mul( displaySize.div( aspectRatio ) ),
positionLocal.y.mul( displaySize )
);
const finalPos = vec2(
scaledPos.x.add( xBase ),
scaledPos.y.add( yBase )
);
return vec4( finalPos.x, finalPos.y, 0.0, 1.0 );
} )();
display.material.outputNode = Fn( () => {
// Ensure shadowMap and depthTexture are available
if ( ! this.tileShadowNode.shadowMap || ! this.tileShadowNode.shadowMap.depthTexture ) {
return vec4( 1, 0, 1, 1 ); // Magenta error color
}
const sampledDepth = texture( this.tileShadowNode.shadowMap.depthTexture )
.sample( uv().flipY() )
.depth( float( i ) ) // Sample correct layer
.compare( 0.9 ); // Example comparison value
// Simple tint based on index for visual distinction
const r = float( 0.5 + ( i % 3 ) * 0.16 );
const g = float( 0.5 + ( i % 2 ) * 0.25 );
const b = float( 0.7 + ( i % 4 ) * 0.075 );
return vec4(
vec3( r, g, b )
.mul( sampledDepth )
.saturate()
.rgb,
1.0
);
} )();
this.add( display );
this._debugMeshes.push( display );
if ( this.tileShadowNode._shadowNodes[ i ] && this.tileShadowNode._shadowNodes[ i ].shadow ) {
const camHelper = new CameraHelper( this.tileShadowNode._shadowNodes[ i ].shadow.camera );
camHelper.fog = false;
this.add( camHelper );
this._shadowCamHelpers.push( camHelper );
} else {
console.warn( `TileShadowNodeHelper: Could not create CameraHelper for tile index ${i}. Shadow node or camera missing.` );
this._shadowCamHelpers.push( null );
}
}
this.initialized = true;
}
/**
* Updates the debug visuals (specifically camera helpers).
* Should be called within TileShadowNode's update method.
*/
update() {
if ( this.initialized === false ) {
this.init();
}
for ( const helper of this._shadowCamHelpers ) {
if ( helper ) {
helper.update(); // Update CameraHelper matrices
helper.updateMatrixWorld( true ); // Ensure world matrix is current
}
}
}
/**
* Removes all debug objects (planes and helpers) from the scene.
*/
dispose() {
if ( this.scene ) {
for ( const mesh of this._debugMeshes ) {
mesh.geometry.dispose();
mesh.material.dispose();
this.scene.remove( mesh );
}
for ( const helper of this._shadowCamHelpers ) {
if ( helper ) {
this.scene.remove( helper );
}
}
}
this._debugMeshes = [];
this._shadowCamHelpers = [];
}
}
export { TileShadowNodeHelper };