Accueil / npm / three@0.186.1 / examples / jsm / tsl / shadows / TileShadowNodeHelper.js

TileShadowNodeHelper.js

import { Group, NodeMaterial, Mesh, PlaneGeometry, DoubleSide, CameraHelper } from 'three/webgpu';
import { Fn, vec4, vec3, texture, uv, positionLocal, vec2, float, int, 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( 'THREE.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( int( 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 };

Exemple