Просмотр исходного кода

WebGPURenderer: Introduce Shadow Map Array (#30830)

* WebGPURenderer: Introduce Shadow Map Array

* WebGPURenderer: Introduce Shadow Map Array

* fixes

* cleanup

* fix webgl backend

* works on both windows & macos so ignore puppeteer failing

* cannot optimize fbo with redformat or it breaks transparent shadows

* Add TileShadowNodeHelper.js

* improved example

* Introduce FrustumArray: Fix CameraArray frustum culling

* cleanup

* cleanup

* more cleanup

* Add FrustumArray to Core

* feedbacks

* fix formatting issues

* fix CI

* fix volume example

* add needsUpdate support and fix light shadow clone missing properties

* reduce gc

* cleanup

* js-docs

* rename

* Update ShadowNode.js

---------
Renaud Rohlinger 1 год назад
Родитель
Сommit
8cccca77a5
31 измененных файлов с 2499 добавлено и 400 удалено
  1. 1 0
      examples/files.json
  2. 433 0
      examples/jsm/tsl/shadows/TileShadowNode.js
  3. 209 0
      examples/jsm/tsl/shadows/TileShadowNodeHelper.js
  4. BIN
      examples/screenshots/webgpu_shadowmap_array.jpg
  5. 2 1
      examples/tags.json
  6. 420 0
      examples/webgpu_shadowmap_array.html
  7. 2 0
      src/Three.Core.js
  8. 3 0
      src/Three.TSL.js
  9. 5 0
      src/lights/LightShadow.js
  10. 253 0
      src/math/FrustumArray.js
  11. 1 0
      src/nodes/TSL.js
  12. 28 2
      src/nodes/accessors/Camera.js
  13. 6 0
      src/nodes/accessors/StorageBufferNode.js
  14. 3 0
      src/nodes/accessors/TextureNode.js
  15. 20 1
      src/nodes/core/NodeBuilder.js
  16. 274 0
      src/nodes/lighting/ShadowFilterNode.js
  17. 139 243
      src/nodes/lighting/ShadowNode.js
  18. 6 3
      src/objects/BatchedMesh.js
  19. 18 4
      src/renderers/common/Renderer.js
  20. 1 0
      src/renderers/common/Textures.js
  21. 105 11
      src/renderers/webgl-fallback/WebGLBackend.js
  22. 19 1
      src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js
  23. 11 6
      src/renderers/webgl-fallback/utils/WebGLTextureUtils.js
  24. 421 121
      src/renderers/webgpu/WebGPUBackend.js
  25. 8 2
      src/renderers/webgpu/nodes/WGSLNodeBuilder.js
  26. 3 0
      src/renderers/webgpu/nodes/WGSLNodeFunction.js
  27. 2 2
      src/renderers/webgpu/utils/WebGPUBindingUtils.js
  28. 3 2
      src/renderers/webgpu/utils/WebGPUPipelineUtils.js
  29. 1 1
      src/renderers/webgpu/utils/WebGPUTextureUtils.js
  30. 101 0
      src/textures/DepthArrayTexture.js
  31. 1 0
      test/e2e/puppeteer.js

+ 1 - 0
examples/files.json

@@ -418,6 +418,7 @@
 		"webgpu_sandbox",
 		"webgpu_shadertoy",
 		"webgpu_shadowmap",
+		"webgpu_shadowmap_array",
 		"webgpu_shadowmap_csm",
 		"webgpu_shadowmap_opacity",
 		"webgpu_shadowmap_progressive",

+ 433 - 0
examples/jsm/tsl/shadows/TileShadowNode.js

@@ -0,0 +1,433 @@
+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 _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
+ * @extends ShadowBaseNode
+ */
+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;
+			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 );
+
+	}
+
+	/**
+	 * 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.lights[ 0 ].shadow; // so far test only the first light
+
+		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 };

+ 209 - 0
examples/jsm/tsl/shadows/TileShadowNodeHelper.js

@@ -0,0 +1,209 @@
+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.
+ */
+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 };

BIN
examples/screenshots/webgpu_shadowmap_array.jpg


+ 2 - 1
examples/tags.json

@@ -155,5 +155,6 @@
 	"webgpu_tonemapping": [ "gltf" ],
 	"webgpu_tsl_compute_attractors_particles": [ "gpgpu" ],
 	"webgpu_ocean": [ "water" ],
-	"webgpu_video_frame": [ "webcodecs" ]
+	"webgpu_video_frame": [ "webcodecs" ],
+	"webgpu_shadowmap_array": [ "tile" ]
 }

+ 420 - 0
examples/webgpu_shadowmap_array.html

@@ -0,0 +1,420 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - shadow map array tile demo</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+		<div id="info">
+		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - tile shadow using shadow map array demonstration
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/Three.WebGPU.js",
+					"three/webgpu": "../build/Three.WebGPU.js",
+					"three/tsl": "../build/Three.TSL.js",
+					"three/addons/": "./jsm/",
+					"stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js"
+
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { mx_fractal_noise_vec3, positionWorld, Fn, color } from 'three/tsl';
+
+			import { TileShadowNode } from 'three/addons/tsl/shadows/TileShadowNode.js';
+			import { TileShadowNodeHelper } from 'three/addons/tsl/shadows/TileShadowNodeHelper.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import Stats from 'stats-gl';
+
+			let camera, scene, renderer, clock;
+			let dirLight, stats;
+			let torusKnot, dirGroup;
+			let tsmHelper;
+
+			init();
+
+			async function init() {
+
+				// Renderer setup
+				renderer = new THREE.WebGPURenderer( { antialias: true, forceWebGL: false } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+
+				renderer.shadowMap.enabled = true;
+				renderer.shadowMap.type = THREE.BasicShadowMap;
+				// renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				renderer.toneMappingExposure = 1.2;
+				document.body.appendChild( renderer.domElement );
+			
+				await renderer.init();
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.set( 45, 60, 100 );
+
+				scene = new THREE.Scene();
+				scene.backgroundNode = color( 0xCCCCFF ); // Brighter blue sky
+				scene.fog = new THREE.Fog( 0xCCCCFF, 700, 1000 );
+
+				// Enhanced lighting for a brighter scene
+				scene.add( new THREE.AmbientLight( 0xCCCCFF, 3 ) );
+
+				// Main directional light (sun)
+				dirLight = new THREE.DirectionalLight( 0xFFFFAA, 5 );
+				dirLight.position.set( 0, 80, 30 );
+				dirLight.castShadow = true;
+				dirLight.shadow.camera.near = 1;
+				dirLight.shadow.camera.far = 200;
+				dirLight.shadow.camera.right = 180;
+				dirLight.shadow.camera.left = - 180;
+				dirLight.shadow.camera.top	= 180;
+				dirLight.shadow.camera.bottom = - 160;
+				dirLight.shadow.mapSize.width = 1024 * 4;
+				dirLight.shadow.mapSize.height = 1024 * 4;
+				dirLight.shadow.radius = 1;
+				dirLight.shadow.bias = - 0.005;
+
+				// Set up the tile shadow mapping
+				const tsm = new TileShadowNode( dirLight, {
+					tilesX: 2,
+					tilesY: 2,
+					debug: true
+				} );
+
+
+				dirLight.shadow.shadowNode = tsm;
+				scene.add( dirLight );
+
+				tsmHelper = new TileShadowNodeHelper( tsm );
+				scene.add( tsmHelper );
+
+
+				dirGroup = new THREE.Group();
+				dirGroup.add( dirLight );
+				scene.add( dirGroup );
+
+				// Create the ground with enhanced texture
+				const planeGeometry = new THREE.PlaneGeometry( 1500, 1500, 2, 2 );
+				const planeMaterial = new THREE.MeshPhongMaterial( {
+					color: 0x88AA44,
+					shininess: 5,
+					specular: 0x222222
+				} );
+
+				planeMaterial.colorNode = Fn( () => {
+
+					const noise = mx_fractal_noise_vec3( positionWorld.mul( 0.05 ) ).saturate();
+					// Mix of greens and browns for a more natural ground
+					const green = color( 0.4, 0.7, 0.3 );
+					const brown = color( 0.6, 0.5, 0.3 );
+					return noise.x.mix( green, brown );
+			
+				} )();
+
+				const ground = new THREE.Mesh( planeGeometry, planeMaterial );
+				ground.rotation.x = - Math.PI / 2;
+				ground.receiveShadow = true;
+				scene.add( ground );
+
+
+			
+				// Spread various objects across the scene
+				createScenery();
+
+				// Camera controls
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 5, 0 );
+				controls.minDistance = 0.01;
+				controls.maxDistance = 400;
+				controls.maxPolarAngle = Math.PI / 2 - 0.1; // Prevent camera from going below ground
+				controls.update();
+
+
+				stats = new Stats( {
+					precision: 3,
+					horizontal: false,
+					trackGPU: true
+				} );
+				stats.init( renderer );
+				document.body.appendChild( stats.dom );
+
+
+				clock = new THREE.Clock();
+
+				window.addEventListener( 'resize', resize );
+
+			}
+
+			function createScenery() {
+
+				// 1. Columns using instanced mesh
+				const columnGeometry = new THREE.CylinderGeometry( 0.8, 1, 1, 16 );
+				const columnMaterial = new THREE.MeshPhongMaterial( {
+					color: 0xDDDDDD,
+					shininess: 20
+				} );
+
+				const columnPositions = [];
+				const columnScales = [];
+
+				for ( let x = - 100; x <= 100; x += 40 ) {
+
+					for ( let z = - 100; z <= 100; z += 40 ) {
+
+						if ( Math.random() > 0.3 ) {
+
+							const height = 5 + Math.random() * 10;
+							const posX = x + ( Math.random() * 10 - 5 );
+							const posY = height / 2;
+							const posZ = z + ( Math.random() * 10 - 5 );
+
+							columnPositions.push( posX, posY, posZ );
+							columnScales.push( 1, height, 1 ); // Only scale Y to match height
+
+						}
+
+					}
+
+				}
+
+				const columnCount = columnPositions.length / 3;
+				const columnInstancedMesh = new THREE.InstancedMesh(
+					columnGeometry,
+					columnMaterial,
+					columnCount
+				);
+
+				const matrix = new THREE.Matrix4();
+				for ( let i = 0; i < columnCount; i ++ ) {
+
+					const x = columnPositions[ i * 3 ];
+					const y = columnPositions[ i * 3 + 1 ];
+					const z = columnPositions[ i * 3 + 2 ];
+					const scaleY = columnScales[ i * 3 + 1 ];
+
+					matrix.makeScale( 1, scaleY, 1 );
+					matrix.setPosition( x, y, z );
+					columnInstancedMesh.setMatrixAt( i, matrix );
+
+				}
+
+				columnInstancedMesh.castShadow = true;
+				columnInstancedMesh.receiveShadow = true;
+				scene.add( columnInstancedMesh );
+
+				// 2. Add a central feature - the torus knot (kept as regular mesh for animation)
+				const torusKnotGeometry = new THREE.TorusKnotGeometry( 25, 8, 100, 30 );
+				const torusKnotMaterial = new THREE.MeshPhongNodeMaterial( {
+					color: 0xFF6347, // Tomato color
+					shininess: 30,
+				} );
+
+				torusKnot = new THREE.Mesh( torusKnotGeometry, torusKnotMaterial );
+				torusKnot.scale.multiplyScalar( 1 / 18 );
+				torusKnot.position.x = 5;
+				torusKnot.position.y = 5;
+				torusKnot.castShadow = true;
+				torusKnot.receiveShadow = true;
+				scene.add( torusKnot );
+
+				// 3. Cubes using instanced mesh
+				const cubeGeometry = new THREE.BoxGeometry( 3, 3, 3 );
+				const cubeMaterials = [
+					new THREE.MeshPhongMaterial( { color: 0x6699CC, shininess: 20 } ),
+					new THREE.MeshPhongMaterial( { color: 0xCC6666, shininess: 20 } ),
+					new THREE.MeshPhongMaterial( { color: 0xCCCC66, shininess: 20 } )
+				];
+
+				const cubeCount = 10;
+				const cubeInstances = cubeMaterials.map( material => {
+
+					return new THREE.InstancedMesh( cubeGeometry, material, cubeCount );
+
+				} );
+
+				for ( let i = 0; i < 30; i ++ ) {
+
+					const materialIndex = i % 3;
+					const instanceIndex = Math.floor( i / 3 );
+
+					const x = Math.random() * 300 - 150;
+					const y = 1.5;
+					const z = Math.random() * 300 - 150;
+					const rotY = Math.random() * Math.PI * 2;
+
+					matrix.makeRotationY( rotY );
+					matrix.setPosition( x, y, z );
+
+					cubeInstances[ materialIndex ].setMatrixAt( instanceIndex, matrix );
+
+				}
+
+				cubeInstances.forEach( instance => {
+
+					instance.castShadow = true;
+					instance.receiveShadow = true;
+					scene.add( instance );
+
+				} );
+
+				// 4. Spheres using instanced mesh
+				const sphereGeometry = new THREE.SphereGeometry( 2, 32, 32 );
+				const sphereMaterial = new THREE.MeshPhongMaterial( {
+					color: 0x88CCAA,
+					shininess: 40
+				} );
+
+				const sphereCount = 25;
+				const sphereInstancedMesh = new THREE.InstancedMesh(
+					sphereGeometry,
+					sphereMaterial,
+					sphereCount
+				);
+
+				for ( let i = 0; i < sphereCount; i ++ ) {
+
+					const x = Math.random() * 180 - 90;
+					const y = 2;
+					const z = Math.random() * 180 - 90;
+
+					matrix.makeScale( 1, 1, 1 );
+					matrix.setPosition( x, y, z );
+					sphereInstancedMesh.setMatrixAt( i, matrix );
+
+				}
+
+				sphereInstancedMesh.castShadow = true;
+				sphereInstancedMesh.receiveShadow = true;
+				scene.add( sphereInstancedMesh );
+
+				// 5. Trees using instanced mesh for trunks and tops separately
+				const trunkGeometry = new THREE.CylinderGeometry( 0.5, 0.5, 2, 8 );
+				const topGeometry = new THREE.ConeGeometry( 2, 8, 8 );
+				const treeMaterial = new THREE.MeshPhongMaterial( {
+					vertexColors: true,
+					shininess: 5
+				} );
+
+				const treeCount = 40;
+				const totalInstanceCount = treeCount * 2;
+
+				const trunkVertexCount = trunkGeometry.attributes.position.count;
+				const trunkIndexCount = trunkGeometry.index ? trunkGeometry.index.count : 0;
+				const topVertexCount = topGeometry.attributes.position.count;
+				const topIndexCount = topGeometry.index ? topGeometry.index.count : 0;
+
+
+				const totalVertexCount = ( trunkVertexCount + topVertexCount ) * 2; // Multiple for safety
+				const totalIndexCount = ( trunkIndexCount + topIndexCount ) * 2;
+				const treeBatchedMesh = new THREE.BatchedMesh( totalInstanceCount, totalVertexCount, totalIndexCount, treeMaterial );
+				treeBatchedMesh.castShadow = true;
+				treeBatchedMesh.perObjectFrustumCulled = false;
+				const trunkGeometryId = treeBatchedMesh.addGeometry( trunkGeometry );
+				const topGeometryId = treeBatchedMesh.addGeometry( topGeometry );
+
+				const trunkColor = new THREE.Color( 0x8B4513 );
+				const topColor = new THREE.Color( 0x336633 );
+
+
+				for ( let i = 0; i < treeCount; i ++ ) {
+
+					const x = Math.random() * 300 - 150;
+					const z = Math.random() * 300 - 150;
+
+					const trunkId = treeBatchedMesh.addInstance( trunkGeometryId );
+					matrix.makeScale( 1, 1, 1 );
+					matrix.setPosition( x, 1, z );
+					treeBatchedMesh.setMatrixAt( trunkId, matrix );
+					treeBatchedMesh.setColorAt( trunkId, trunkColor );
+
+					const topId = treeBatchedMesh.addInstance( topGeometryId );
+					matrix.makeScale( 1, 1, 1 );
+					matrix.setPosition( x, 6, z );
+					treeBatchedMesh.setMatrixAt( topId, matrix );
+					treeBatchedMesh.setColorAt( topId, topColor );
+
+				}
+
+				scene.add( treeBatchedMesh );
+
+
+				// 6. Torus shapes using instanced mesh
+				const torusGeometry = new THREE.TorusGeometry( 3, 1, 16, 50 );
+				const torusMaterial = new THREE.MeshPhongMaterial( {
+					color: 0xFF99CC,
+					shininess: 30
+				} );
+
+				const torusCount = 15;
+				const torusInstancedMesh = new THREE.InstancedMesh(
+					torusGeometry,
+					torusMaterial,
+					torusCount
+				);
+
+				for ( let i = 0; i < torusCount; i ++ ) {
+
+					const x = Math.random() * 320 - 160;
+					const y = 2;
+					const z = Math.random() * 320 - 160;
+					const rotZ = Math.random() * Math.PI * 2;
+
+					// Apply rotation (PI/2 on X-axis and random on Z-axis)
+					matrix.makeRotationX( Math.PI / 2 );
+					const rotMatrix = new THREE.Matrix4().makeRotationZ( rotZ );
+					matrix.multiply( rotMatrix );
+					matrix.setPosition( x, y, z );
+
+					torusInstancedMesh.setMatrixAt( i, matrix );
+
+				}
+
+				torusInstancedMesh.castShadow = true;
+				torusInstancedMesh.receiveShadow = true;
+				scene.add( torusInstancedMesh );
+
+			}
+
+			function resize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			async function animate( time ) {
+
+				const delta = clock.getDelta();
+
+				// Rotate the central torus knot
+				torusKnot.rotation.x += 0.25 * delta;
+				torusKnot.rotation.y += 0.5 * delta;
+				torusKnot.rotation.z += 1 * delta;
+
+				dirLight.position.x = Math.sin( time * 0.0001 ) * 30;
+				dirLight.position.z = Math.cos( time * 0.0001 ) * 30;
+
+				renderer.render( scene, camera );
+
+				tsmHelper.update();
+
+				await renderer.resolveTimestampsAsync();
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 2 - 0
src/Three.Core.js

@@ -34,6 +34,7 @@ export { CompressedCubeTexture } from './textures/CompressedCubeTexture.js';
 export { CubeTexture } from './textures/CubeTexture.js';
 export { CanvasTexture } from './textures/CanvasTexture.js';
 export { DepthTexture } from './textures/DepthTexture.js';
+export { DepthArrayTexture } from './textures/DepthArrayTexture.js';
 export { Texture } from './textures/Texture.js';
 export * from './geometries/Geometries.js';
 export * from './materials/Materials.js';
@@ -115,6 +116,7 @@ export { Spherical } from './math/Spherical.js';
 export { Cylindrical } from './math/Cylindrical.js';
 export { Plane } from './math/Plane.js';
 export { Frustum } from './math/Frustum.js';
+export { FrustumArray } from './math/FrustumArray.js';
 export { Sphere } from './math/Sphere.js';
 export { Ray } from './math/Ray.js';
 export { Matrix4 } from './math/Matrix4.js';

+ 3 - 0
src/Three.TSL.js

@@ -183,6 +183,8 @@ export const getScreenPosition = TSL.getScreenPosition;
 export const getShIrradianceAt = TSL.getShIrradianceAt;
 export const getTextureIndex = TSL.getTextureIndex;
 export const getViewPosition = TSL.getViewPosition;
+export const getShadowMaterial = TSL.getShadowMaterial;
+export const getShadowRenderObjectFunction = TSL.getShadowRenderObjectFunction;
 export const glsl = TSL.glsl;
 export const glslFn = TSL.glslFn;
 export const grayscale = TSL.grayscale;
@@ -217,6 +219,7 @@ export const lengthSq = TSL.lengthSq;
 export const lessThan = TSL.lessThan;
 export const lessThanEqual = TSL.lessThanEqual;
 export const lightPosition = TSL.lightPosition;
+export const lightShadowMatrix = TSL.lightShadowMatrix;
 export const lightTargetDirection = TSL.lightTargetDirection;
 export const lightTargetPosition = TSL.lightTargetPosition;
 export const lightViewPosition = TSL.lightViewPosition;

+ 5 - 0
src/lights/LightShadow.js

@@ -266,6 +266,11 @@ class LightShadow {
 		this.bias = source.bias;
 		this.radius = source.radius;
 
+		this.autoUpdate = source.autoUpdate;
+		this.needsUpdate = source.needsUpdate;
+		this.normalBias = source.normalBias;
+		this.blurSamples = source.blurSamples;
+
 		this.mapSize.copy( source.mapSize );
 
 		return this;

+ 253 - 0
src/math/FrustumArray.js

@@ -0,0 +1,253 @@
+import { WebGLCoordinateSystem } from '../constants.js';
+import { Frustum } from './Frustum.js';
+import { Matrix4 } from './Matrix4.js';
+
+const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
+const _frustum = /*@__PURE__*/ new Frustum();
+
+/**
+ * FrustumArray is used to determine if an object is visible in at least one camera
+ * from an array of cameras. This is particularly useful for multi-view renderers.
+*/
+class FrustumArray {
+
+	/**
+	 * Constructs a new frustum array.
+	 *
+	 */
+	constructor() {
+
+		/**
+		 * The coordinate system to use.
+		 *
+		 * @type {WebGLCoordinateSystem|WebGPUCoordinateSystem}
+		 * @default WebGLCoordinateSystem
+		 */
+		this.coordinateSystem = WebGLCoordinateSystem;
+
+	}
+
+	/**
+	 * Returns `true` if the 3D object's bounding sphere is intersecting any frustum
+	 * from the camera array.
+	 *
+	 * @param {Object3D} object - The 3D object to test.
+	 * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.
+	 * @return {boolean} Whether the 3D object is visible in any camera.
+	 */
+	intersectsObject( object, cameraArray ) {
+
+		if ( ! cameraArray.isArrayCamera || cameraArray.cameras.length === 0 ) {
+
+			return false;
+
+		}
+
+		for ( let i = 0; i < cameraArray.cameras.length; i ++ ) {
+
+			const camera = cameraArray.cameras[ i ];
+
+			_projScreenMatrix.multiplyMatrices(
+				camera.projectionMatrix,
+				camera.matrixWorldInverse
+			);
+
+			_frustum.setFromProjectionMatrix(
+				_projScreenMatrix,
+				this.coordinateSystem
+			);
+
+			if ( _frustum.intersectsObject( object ) ) {
+
+				return true; // Object is visible in at least one camera
+
+			}
+
+		}
+
+		return false; // Not visible in any camera
+
+	}
+
+	/**
+	 * Returns `true` if the given sprite is intersecting any frustum
+	 * from the camera array.
+	 *
+	 * @param {Sprite} sprite - The sprite to test.
+	 * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.
+	 * @return {boolean} Whether the sprite is visible in any camera.
+	 */
+	intersectsSprite( sprite, cameraArray ) {
+
+		if ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) {
+
+			return false;
+
+		}
+
+		for ( let i = 0; i < cameraArray.cameras.length; i ++ ) {
+
+			const camera = cameraArray.cameras[ i ];
+
+			_projScreenMatrix.multiplyMatrices(
+				camera.projectionMatrix,
+				camera.matrixWorldInverse
+			);
+
+			_frustum.setFromProjectionMatrix(
+				_projScreenMatrix,
+				this.coordinateSystem
+			);
+
+			if ( _frustum.intersectsSprite( sprite ) ) {
+
+				return true; // Sprite is visible in at least one camera
+
+			}
+
+		}
+
+		return false; // Not visible in any camera
+
+	}
+
+	/**
+	 * Returns `true` if the given bounding sphere is intersecting any frustum
+	 * from the camera array.
+	 *
+	 * @param {Sphere} sphere - The bounding sphere to test.
+	 * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.
+	 * @return {boolean} Whether the sphere is visible in any camera.
+	 */
+	intersectsSphere( sphere, cameraArray ) {
+
+		if ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) {
+
+			return false;
+
+		}
+
+		for ( let i = 0; i < cameraArray.cameras.length; i ++ ) {
+
+			const camera = cameraArray.cameras[ i ];
+
+			_projScreenMatrix.multiplyMatrices(
+				camera.projectionMatrix,
+				camera.matrixWorldInverse
+			);
+
+			_frustum.setFromProjectionMatrix(
+				_projScreenMatrix,
+				this.coordinateSystem
+			);
+
+			if ( _frustum.intersectsSphere( sphere ) ) {
+
+				return true; // Sphere is visible in at least one camera
+
+			}
+
+		}
+
+		return false; // Not visible in any camera
+
+	}
+
+	/**
+	 * Returns `true` if the given bounding box is intersecting any frustum
+	 * from the camera array.
+	 *
+	 * @param {Box3} box - The bounding box to test.
+	 * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.
+	 * @return {boolean} Whether the box is visible in any camera.
+	 */
+	intersectsBox( box, cameraArray ) {
+
+		if ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) {
+
+			return false;
+
+		}
+
+		for ( let i = 0; i < cameraArray.cameras.length; i ++ ) {
+
+			const camera = cameraArray.cameras[ i ];
+
+			_projScreenMatrix.multiplyMatrices(
+				camera.projectionMatrix,
+				camera.matrixWorldInverse
+			);
+
+			_frustum.setFromProjectionMatrix(
+				_projScreenMatrix,
+				this.coordinateSystem
+			);
+
+			if ( _frustum.intersectsBox( box ) ) {
+
+				return true; // Box is visible in at least one camera
+
+			}
+
+		}
+
+		return false; // Not visible in any camera
+
+	}
+
+	/**
+	 * Returns `true` if the given point lies within any frustum
+	 * from the camera array.
+	 *
+	 * @param {Vector3} point - The point to test.
+	 * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.
+	 * @return {boolean} Whether the point is visible in any camera.
+	 */
+	containsPoint( point, cameraArray ) {
+
+		if ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) {
+
+			return false;
+
+		}
+
+		for ( let i = 0; i < cameraArray.cameras.length; i ++ ) {
+
+			const camera = cameraArray.cameras[ i ];
+
+			_projScreenMatrix.multiplyMatrices(
+				camera.projectionMatrix,
+				camera.matrixWorldInverse
+			);
+
+			_frustum.setFromProjectionMatrix(
+				_projScreenMatrix,
+				this.coordinateSystem
+			);
+
+			if ( _frustum.containsPoint( point ) ) {
+
+				return true; // Point is visible in at least one camera
+
+			}
+
+		}
+
+		return false; // Not visible in any camera
+
+	}
+
+	/**
+	 * Returns a new frustum array with copied values from this instance.
+	 *
+	 * @return {FrustumArray} A clone of this instance.
+	 */
+	clone() {
+
+		return new FrustumArray();
+
+	}
+
+}
+
+export { FrustumArray };

+ 1 - 0
src/nodes/TSL.js

@@ -134,6 +134,7 @@ export * from './lighting/LightsNode.js';
 export * from './lighting/LightingContextNode.js';
 export * from './lighting/ShadowBaseNode.js';
 export * from './lighting/ShadowNode.js';
+export * from './lighting/ShadowFilterNode.js';
 export * from './lighting/PointShadowNode.js';
 export * from './lighting/PointLightNode.js';
 

+ 28 - 2
src/nodes/accessors/Camera.js

@@ -10,7 +10,7 @@ import { uniformArray } from './UniformArrayNode.js';
  * @tsl
  * @type {UniformNode<uint>}
  */
-export const cameraIndex = /*@__PURE__*/ uniform( 0, 'uint' ).setGroup( sharedUniformGroup( 'cameraIndex' ) ).toVarying( 'v_cameraIndex' );
+export const cameraIndex = /*@__PURE__*/ uniform( 0, 'uint' ).label( 'u_cameraIndex' ).setGroup( sharedUniformGroup( 'cameraIndex' ) ).toVarying( 'v_cameraIndex' );
 
 /**
  * TSL object that represents the `near` value of the camera used for the current render.
@@ -68,7 +68,33 @@ export const cameraProjectionMatrix = /*@__PURE__*/ ( Fn( ( { camera } ) => {
  * @tsl
  * @type {UniformNode<mat4>}
  */
-export const cameraProjectionMatrixInverse = /*@__PURE__*/ uniform( 'mat4' ).label( 'cameraProjectionMatrixInverse' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.projectionMatrixInverse );
+export const cameraProjectionMatrixInverse = /*@__PURE__*/ ( Fn( ( { camera } ) => {
+
+	let cameraProjectionMatrixInverse;
+
+	if ( camera.isArrayCamera && camera.cameras.length > 0 ) {
+
+		const matrices = [];
+
+		for ( const subCamera of camera.cameras ) {
+
+			matrices.push( subCamera.projectionMatrixInverse );
+
+		}
+
+		const cameraProjectionMatricesInverse = uniformArray( matrices ).setGroup( renderGroup ).label( 'cameraProjectionMatricesInverse' );
+
+		cameraProjectionMatrixInverse = cameraProjectionMatricesInverse.element( cameraIndex ).toVar( 'cameraProjectionMatrixInverse' );
+
+	} else {
+
+		cameraProjectionMatrixInverse = uniform( 'mat4' ).label( 'cameraProjectionMatrixInverse' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.projectionMatrixInverse );
+
+	}
+
+	return cameraProjectionMatrixInverse;
+
+} ).once() )();
 
 /**
  * TSL object that represents the view matrix of the camera used for the current render.

+ 6 - 0
src/nodes/accessors/StorageBufferNode.js

@@ -60,6 +60,12 @@ class StorageBufferNode extends BufferNode {
 			nodeType = 'struct';
 			structTypeNode = bufferType.layout;
 
+			if ( value.isStorageBufferAttribute || value.isStorageInstancedBufferAttribute ) {
+
+				bufferCount = value.count;
+
+			}
+
 		} else if ( bufferType === null && ( value.isStorageBufferAttribute || value.isStorageInstancedBufferAttribute ) ) {
 
 			nodeType = getTypeFromLength( value.itemSize );

+ 3 - 0
src/nodes/accessors/TextureNode.js

@@ -720,6 +720,9 @@ class TextureNode extends UniformNode {
 
 		const newNode = new this.constructor( this.value, this.uvNode, this.levelNode, this.biasNode );
 		newNode.sampler = this.sampler;
+		newNode.depthNode = this.depthNode;
+		newNode.compareNode = this.compareNode;
+		newNode.gradNode = this.gradNode;
 
 		return newNode;
 

+ 20 - 1
src/nodes/core/NodeBuilder.js

@@ -26,6 +26,7 @@ import BindGroup from '../../renderers/common/BindGroup.js';
 
 import { REVISION, IntType, UnsignedIntType, LinearFilter, LinearMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapLinearFilter } from '../../constants.js';
 import { RenderTarget } from '../../core/RenderTarget.js';
+import { RenderTargetArray } from '../../core/RenderTargetArray.js';
 import { Color } from '../../math/Color.js';
 import { Vector2 } from '../../math/Vector2.js';
 import { Vector3 } from '../../math/Vector3.js';
@@ -457,6 +458,22 @@ class NodeBuilder {
 
 	}
 
+	/**
+	 * Factory method for creating an instance of {@link RenderTargetArray} with the given
+	 * dimensions and options.
+	 *
+	 * @param {number} width - The width of the render target.
+	 * @param {number} height - The height of the render target.
+	 * @param {number} depth - The depth of the render target.
+	 * @param {Object} options - The options of the render target.
+	 * @return {RenderTargetArray} The render target.
+	 */
+	createRenderTargetArray( width, height, depth, options ) {
+
+		return new RenderTargetArray( width, height, depth, options );
+
+	}
+
 	/**
 	 * Factory method for creating an instance of {@link CubeRenderTarget} with the given
 	 * dimensions and options.
@@ -1883,7 +1900,6 @@ class NodeBuilder {
 
 		}
 
-		declarations[ name ] = node;
 
 		if ( index > 1 ) {
 
@@ -1893,6 +1909,9 @@ class NodeBuilder {
 
 		}
 
+
+		declarations[ name ] = node;
+
 	}
 
 	/**

+ 274 - 0
src/nodes/lighting/ShadowFilterNode.js

@@ -0,0 +1,274 @@
+import { float, vec2, vec4, If, Fn } from '../tsl/TSLBase.js';
+import { reference } from '../accessors/ReferenceNode.js';
+import { texture } from '../accessors/TextureNode.js';
+import { mix, fract, step, max, clamp } from '../math/MathNode.js';
+import { add, sub } from '../math/OperatorNode.js';
+import { renderGroup } from '../core/UniformGroupNode.js';
+import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
+import { objectPosition } from '../accessors/Object3DNode.js';
+import { positionWorld } from '../accessors/Position.js';
+
+const shadowMaterialLib = /*@__PURE__*/ new WeakMap();
+
+/**
+ * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map
+ * with a binary `[0,1]` result.
+ *
+ * @method
+ * @param {Object} inputs - The input parameter object.
+ * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
+ * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
+ * @return {Node<float>} The filtering result.
+ */
+export const BasicShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, depthLayer } ) => {
+
+	let basic = texture( depthTexture, shadowCoord.xy ).label( 't_basic' );
+
+	if ( depthTexture.isDepthArrayTexture ) {
+
+		basic = basic.depth( depthLayer );
+
+	}
+
+	return basic.compare( shadowCoord.z );
+
+} );
+
+/**
+ * A shadow filtering function performing PCF filtering.
+ *
+ * @method
+ * @param {Object} inputs - The input parameter object.
+ * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
+ * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
+ * @param {LightShadow} inputs.shadow - The light shadow.
+ * @return {Node<float>} The filtering result.
+ */
+export const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => {
+
+	const depthCompare = ( uv, compare ) => {
+
+		let depth = texture( depthTexture, uv );
+
+		if ( depthTexture.isDepthArrayTexture ) {
+
+			depth = depth.depth( depthLayer );
+
+		}
+
+		return depth.compare( compare );
+
+	};
+
+	const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
+	const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
+
+	const texelSize = vec2( 1 ).div( mapSize );
+	const dx0 = texelSize.x.negate().mul( radius );
+	const dy0 = texelSize.y.negate().mul( radius );
+	const dx1 = texelSize.x.mul( radius );
+	const dy1 = texelSize.y.mul( radius );
+	const dx2 = dx0.div( 2 );
+	const dy2 = dy0.div( 2 );
+	const dx3 = dx1.div( 2 );
+	const dy3 = dy1.div( 2 );
+
+	return add(
+		depthCompare( shadowCoord.xy.add( vec2( dx0, dy0 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( 0, dy0 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx1, dy0 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx2, dy2 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( 0, dy2 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx3, dy2 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx0, 0 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx2, 0 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy, shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx3, 0 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx1, 0 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx2, dy3 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( 0, dy3 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx3, dy3 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx0, dy1 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( 0, dy1 ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vec2( dx1, dy1 ) ), shadowCoord.z )
+	).mul( 1 / 17 );
+
+} );
+
+/**
+ * A shadow filtering function performing PCF soft filtering.
+ *
+ * @method
+ * @param {Object} inputs - The input parameter object.
+ * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
+ * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
+ * @param {LightShadow} inputs.shadow - The light shadow.
+ * @return {Node<float>} The filtering result.
+ */
+export const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => {
+
+	const depthCompare = ( uv, compare ) => {
+
+		let depth = texture( depthTexture, uv );
+
+		if ( depthTexture.isDepthArrayTexture ) {
+
+			depth = depth.depth( depthLayer );
+
+		}
+
+		return depth.compare( compare );
+
+	};
+
+
+	const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
+
+	const texelSize = vec2( 1 ).div( mapSize );
+	const dx = texelSize.x;
+	const dy = texelSize.y;
+
+	const uv = shadowCoord.xy;
+	const f = fract( uv.mul( mapSize ).add( 0.5 ) );
+	uv.subAssign( f.mul( texelSize ) );
+
+	return add(
+		depthCompare( uv, shadowCoord.z ),
+		depthCompare( uv.add( vec2( dx, 0 ) ), shadowCoord.z ),
+		depthCompare( uv.add( vec2( 0, dy ) ), shadowCoord.z ),
+		depthCompare( uv.add( texelSize ), shadowCoord.z ),
+		mix(
+			depthCompare( uv.add( vec2( dx.negate(), 0 ) ), shadowCoord.z ),
+			depthCompare( uv.add( vec2( dx.mul( 2 ), 0 ) ), shadowCoord.z ),
+			f.x
+		),
+		mix(
+			depthCompare( uv.add( vec2( dx.negate(), dy ) ), shadowCoord.z ),
+			depthCompare( uv.add( vec2( dx.mul( 2 ), dy ) ), shadowCoord.z ),
+			f.x
+		),
+		mix(
+			depthCompare( uv.add( vec2( 0, dy.negate() ) ), shadowCoord.z ),
+			depthCompare( uv.add( vec2( 0, dy.mul( 2 ) ) ), shadowCoord.z ),
+			f.y
+		),
+		mix(
+			depthCompare( uv.add( vec2( dx, dy.negate() ) ), shadowCoord.z ),
+			depthCompare( uv.add( vec2( dx, dy.mul( 2 ) ) ), shadowCoord.z ),
+			f.y
+		),
+		mix(
+			mix(
+				depthCompare( uv.add( vec2( dx.negate(), dy.negate() ) ), shadowCoord.z ),
+				depthCompare( uv.add( vec2( dx.mul( 2 ), dy.negate() ) ), shadowCoord.z ),
+				f.x
+			),
+			mix(
+				depthCompare( uv.add( vec2( dx.negate(), dy.mul( 2 ) ) ), shadowCoord.z ),
+				depthCompare( uv.add( vec2( dx.mul( 2 ), dy.mul( 2 ) ) ), shadowCoord.z ),
+				f.x
+			),
+			f.y
+		)
+	).mul( 1 / 9 );
+
+} );
+
+/**
+ * A shadow filtering function performing VSM filtering.
+ *
+ * @method
+ * @param {Object} inputs - The input parameter object.
+ * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
+ * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
+ * @return {Node<float>} The filtering result.
+ */
+export const VSMShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, depthLayer } ) => {
+
+	const occlusion = float( 1 ).toVar();
+
+	let distribution = texture( depthTexture ).sample( shadowCoord.xy );
+
+	if ( depthTexture.isDepthArrayTexture || depthTexture.isDataArrayTexture ) {
+
+		distribution = distribution.depth( depthLayer );
+
+	}
+
+	distribution = distribution.rg;
+
+	const hardShadow = step( shadowCoord.z, distribution.x );
+
+	If( hardShadow.notEqual( float( 1.0 ) ), () => {
+
+		const distance = shadowCoord.z.sub( distribution.x );
+		const variance = max( 0, distribution.y.mul( distribution.y ) );
+		let softnessProbability = variance.div( variance.add( distance.mul( distance ) ) ); // Chebeyshevs inequality
+		softnessProbability = clamp( sub( softnessProbability, 0.3 ).div( 0.95 - 0.3 ) );
+		occlusion.assign( clamp( max( hardShadow, softnessProbability ) ) );
+
+	} );
+
+	return occlusion;
+
+} );
+
+//
+
+const linearDistance = /*@__PURE__*/ Fn( ( [ position, cameraNear, cameraFar ] ) => {
+
+	let dist = positionWorld.sub( position ).length();
+	dist = dist.sub( cameraNear ).div( cameraFar.sub( cameraNear ) );
+	dist = dist.saturate(); // clamp to [ 0, 1 ]
+
+	return dist;
+
+} );
+
+const linearShadowDistance = ( light ) => {
+
+	const camera = light.shadow.camera;
+
+	const nearDistance = reference( 'near', 'float', camera ).setGroup( renderGroup );
+	const farDistance = reference( 'far', 'float', camera ).setGroup( renderGroup );
+
+	const referencePosition = objectPosition( light );
+
+	return linearDistance( referencePosition, nearDistance, farDistance );
+
+};
+
+/**
+ * Retrieves or creates a shadow material for the given light source.
+ *
+ * This function checks if a shadow material already exists for the provided light.
+ * If not, it creates a new `NodeMaterial` configured for shadow rendering and stores it
+ * in the `shadowMaterialLib` for future use.
+ *
+ * @param {Light} light - The light source for which the shadow material is needed.
+ *                         If the light is a point light, a depth node is calculated
+ *                         using the linear shadow distance.
+ * @returns {NodeMaterial} The shadow material associated with the given light.
+ */
+export const getShadowMaterial = ( light ) => {
+
+	let material = shadowMaterialLib.get( light );
+
+	if ( material === undefined ) {
+
+		const depthNode = light.isPointLight ? linearShadowDistance( light ) : null;
+
+		material = new NodeMaterial();
+		material.colorNode = vec4( 0, 0, 0, 1 );
+		material.depthNode = depthNode;
+		material.isShadowPassMaterial = true; // Use to avoid other overrideMaterial override material.colorNode unintentionally when using material.shadowNode
+		material.name = 'ShadowMaterial';
+		material.fog = false;
+
+		shadowMaterialLib.set( light, material );
+
+	}
+
+	return material;
+
+};

+ 139 - 243
src/nodes/lighting/ShadowNode.js

@@ -1,11 +1,10 @@
 import ShadowBaseNode, { shadowPositionWorld } from './ShadowBaseNode.js';
-import { float, vec2, vec3, vec4, If, int, Fn, nodeObject } from '../tsl/TSLBase.js';
+import { float, vec2, vec3, int, Fn, nodeObject } from '../tsl/TSLBase.js';
 import { reference } from '../accessors/ReferenceNode.js';
 import { texture } from '../accessors/TextureNode.js';
-import { positionWorld } from '../accessors/Position.js';
 import { transformedNormalWorld } from '../accessors/Normal.js';
-import { mix, fract, step, max, clamp, sqrt } from '../math/MathNode.js';
-import { add, sub } from '../math/OperatorNode.js';
+import { mix, sqrt } from '../math/MathNode.js';
+import { add } from '../math/OperatorNode.js';
 import { DepthTexture } from '../../textures/DepthTexture.js';
 import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
 import QuadMesh from '../../renderers/common/QuadMesh.js';
@@ -14,219 +13,52 @@ import { screenCoordinate } from '../display/ScreenNode.js';
 import { HalfFloatType, LessCompare, RGFormat, VSMShadowMap, WebGPUCoordinateSystem } from '../../constants.js';
 import { renderGroup } from '../core/UniformGroupNode.js';
 import { viewZToLogarithmicDepth } from '../display/ViewportDepthNode.js';
-import { objectPosition } from '../accessors/Object3DNode.js';
 import { lightShadowMatrix } from '../accessors/Lights.js';
 import { resetRendererAndSceneState, restoreRendererAndSceneState } from '../../renderers/common/RendererUtils.js';
 import { getDataFromObject } from '../core/NodeUtils.js';
-
-const shadowMaterialLib = /*@__PURE__*/ new WeakMap();
-const linearDistance = /*@__PURE__*/ Fn( ( [ position, cameraNear, cameraFar ] ) => {
-
-	let dist = positionWorld.sub( position ).length();
-	dist = dist.sub( cameraNear ).div( cameraFar.sub( cameraNear ) );
-	dist = dist.saturate(); // clamp to [ 0, 1 ]
-
-	return dist;
-
-} );
-
-const linearShadowDistance = ( light ) => {
-
-	const camera = light.shadow.camera;
-
-	const nearDistance = reference( 'near', 'float', camera ).setGroup( renderGroup );
-	const farDistance = reference( 'far', 'float', camera ).setGroup( renderGroup );
-
-	const referencePosition = objectPosition( light );
-
-	return linearDistance( referencePosition, nearDistance, farDistance );
-
-};
-
-const getShadowMaterial = ( light ) => {
-
-	let material = shadowMaterialLib.get( light );
-
-	if ( material === undefined ) {
-
-		const depthNode = light.isPointLight ? linearShadowDistance( light ) : null;
-
-		material = new NodeMaterial();
-		material.colorNode = vec4( 0, 0, 0, 1 );
-		material.depthNode = depthNode;
-		material.isShadowPassMaterial = true; // Use to avoid other overrideMaterial override material.colorNode unintentionally when using material.shadowNode
-		material.name = 'ShadowMaterial';
-		material.fog = false;
-
-		shadowMaterialLib.set( light, material );
-
-	}
-
-	return material;
-
-};
+import { getShadowMaterial, BasicShadowFilter, PCFShadowFilter, PCFSoftShadowFilter, VSMShadowFilter } from './ShadowFilterNode.js';
 
 /**
- * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map
- * with a binary `[0,1]` result.
+ * Creates a function to render shadow objects in a scene.
  *
- * @method
- * @param {Object} inputs - The input parameter object.
- * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
- * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
- * @return {Node<float>} The filtering result.
- */
-export const BasicShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord } ) => {
-
-	return texture( depthTexture, shadowCoord.xy ).compare( shadowCoord.z );
-
-} );
-
-/**
- * A shadow filtering function performing PCF filtering.
+ * @param {Renderer} renderer - The renderer.
+ * @param {LightShadow} shadow - The light shadow object containing shadow properties.
+ * @param {number} shadowType - The type of shadow map (e.g., BasicShadowMap).
+ * @param {boolean} useVelocity - Whether to use velocity data for rendering.
+ * @return {Function} A function that renders shadow objects.
  *
- * @method
- * @param {Object} inputs - The input parameter object.
- * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
- * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
- * @param {LightShadow} inputs.shadow - The light shadow.
- * @return {Node<float>} The filtering result.
+ * The returned function has the following parameters:
+ * @param {Object3D} object - The 3D object to render.
+ * @param {Scene} scene - The scene containing the object.
+ * @param {Camera} _camera - The camera used for rendering.
+ * @param {BufferGeometry} geometry - The geometry of the object.
+ * @param {Material} material - The material of the object.
+ * @param {Group} group - The group the object belongs to.
+ * @param {...any} params - Additional parameters for rendering.
  */
-export const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow } ) => {
-
-	const depthCompare = ( uv, compare ) => texture( depthTexture, uv ).compare( compare );
-
-	const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
-	const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
-
-	const texelSize = vec2( 1 ).div( mapSize );
-	const dx0 = texelSize.x.negate().mul( radius );
-	const dy0 = texelSize.y.negate().mul( radius );
-	const dx1 = texelSize.x.mul( radius );
-	const dy1 = texelSize.y.mul( radius );
-	const dx2 = dx0.div( 2 );
-	const dy2 = dy0.div( 2 );
-	const dx3 = dx1.div( 2 );
-	const dy3 = dy1.div( 2 );
-
-	return add(
-		depthCompare( shadowCoord.xy.add( vec2( dx0, dy0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( 0, dy0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx1, dy0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx2, dy2 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( 0, dy2 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx3, dy2 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx0, 0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx2, 0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy, shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx3, 0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx1, 0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx2, dy3 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( 0, dy3 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx3, dy3 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx0, dy1 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( 0, dy1 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx1, dy1 ) ), shadowCoord.z )
-	).mul( 1 / 17 );
-
-} );
+export const getShadowRenderObjectFunction = ( renderer, shadow, shadowType, useVelocity ) => {
 
-/**
- * A shadow filtering function performing PCF soft filtering.
- *
- * @method
- * @param {Object} inputs - The input parameter object.
- * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
- * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
- * @param {LightShadow} inputs.shadow - The light shadow.
- * @return {Node<float>} The filtering result.
- */
-export const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow } ) => {
-
-	const depthCompare = ( uv, compare ) => texture( depthTexture, uv ).compare( compare );
-
-	const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
-
-	const texelSize = vec2( 1 ).div( mapSize );
-	const dx = texelSize.x;
-	const dy = texelSize.y;
-
-	const uv = shadowCoord.xy;
-	const f = fract( uv.mul( mapSize ).add( 0.5 ) );
-	uv.subAssign( f.mul( texelSize ) );
-
-	return add(
-		depthCompare( uv, shadowCoord.z ),
-		depthCompare( uv.add( vec2( dx, 0 ) ), shadowCoord.z ),
-		depthCompare( uv.add( vec2( 0, dy ) ), shadowCoord.z ),
-		depthCompare( uv.add( texelSize ), shadowCoord.z ),
-		mix(
-			depthCompare( uv.add( vec2( dx.negate(), 0 ) ), shadowCoord.z ),
-			depthCompare( uv.add( vec2( dx.mul( 2 ), 0 ) ), shadowCoord.z ),
-			f.x
-		),
-		mix(
-			depthCompare( uv.add( vec2( dx.negate(), dy ) ), shadowCoord.z ),
-			depthCompare( uv.add( vec2( dx.mul( 2 ), dy ) ), shadowCoord.z ),
-			f.x
-		),
-		mix(
-			depthCompare( uv.add( vec2( 0, dy.negate() ) ), shadowCoord.z ),
-			depthCompare( uv.add( vec2( 0, dy.mul( 2 ) ) ), shadowCoord.z ),
-			f.y
-		),
-		mix(
-			depthCompare( uv.add( vec2( dx, dy.negate() ) ), shadowCoord.z ),
-			depthCompare( uv.add( vec2( dx, dy.mul( 2 ) ) ), shadowCoord.z ),
-			f.y
-		),
-		mix(
-			mix(
-				depthCompare( uv.add( vec2( dx.negate(), dy.negate() ) ), shadowCoord.z ),
-				depthCompare( uv.add( vec2( dx.mul( 2 ), dy.negate() ) ), shadowCoord.z ),
-				f.x
-			),
-			mix(
-				depthCompare( uv.add( vec2( dx.negate(), dy.mul( 2 ) ) ), shadowCoord.z ),
-				depthCompare( uv.add( vec2( dx.mul( 2 ), dy.mul( 2 ) ) ), shadowCoord.z ),
-				f.x
-			),
-			f.y
-		)
-	).mul( 1 / 9 );
+	return ( object, scene, _camera, geometry, material, group, ...params ) => {
 
-} );
+		if ( object.castShadow === true || ( object.receiveShadow && shadowType === VSMShadowMap ) ) {
 
-/**
- * A shadow filtering function performing VSM filtering.
- *
- * @method
- * @param {Object} inputs - The input parameter object.
- * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
- * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
- * @return {Node<float>} The filtering result.
- */
-export const VSMShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord } ) => {
+			if ( useVelocity ) {
 
-	const occlusion = float( 1 ).toVar();
+				getDataFromObject( object ).useVelocity = true;
 
-	const distribution = texture( depthTexture ).sample( shadowCoord.xy ).rg;
+			}
 
-	const hardShadow = step( shadowCoord.z, distribution.x );
+			object.onBeforeShadow( renderer, object, _camera, shadow.camera, geometry, scene.overrideMaterial, group );
 
-	If( hardShadow.notEqual( float( 1.0 ) ), () => {
+			renderer.renderObject( object, scene, _camera, geometry, material, group, ...params );
 
-		const distance = shadowCoord.z.sub( distribution.x );
-		const variance = max( 0, distribution.y.mul( distribution.y ) );
-		let softnessProbability = variance.div( variance.add( distance.mul( distance ) ) ); // Chebeyshevs inequality
-		softnessProbability = clamp( sub( softnessProbability, 0.3 ).div( 0.95 - 0.3 ) );
-		occlusion.assign( clamp( max( hardShadow, softnessProbability ) ) );
+			object.onAfterShadow( renderer, object, _camera, shadow.camera, geometry, scene.overrideMaterial, group );
 
-	} );
+		}
 
-	return occlusion;
+	};
 
-} );
+};
 
 /**
  * Represents the shader code for the first VSM render pass.
@@ -239,10 +71,10 @@ export const VSMShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord }
  * @param {TextureNode} inputs.shadowPass - A reference to the render target's depth data.
  * @return {Node<vec2>} The VSM output.
  */
-const VSMPassVertical = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass } ) => {
+const VSMPassVertical = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass, depthLayer } ) => {
 
-	const mean = float( 0 ).toVar();
-	const squaredMean = float( 0 ).toVar();
+	const mean = float( 0 ).toVar( 'meanVertical' );
+	const squaredMean = float( 0 ).toVar( 'squareMeanVertical' );
 
 	const uvStride = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( 2 ).div( samples.sub( 1 ) ) );
 	const uvStart = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( - 1 ) );
@@ -251,7 +83,16 @@ const VSMPassVertical = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass
 
 		const uvOffset = uvStart.add( float( i ).mul( uvStride ) );
 
-		const depth = shadowPass.sample( add( screenCoordinate.xy, vec2( 0, uvOffset ).mul( radius ) ).div( size ) ).x;
+		let depth = shadowPass.sample( add( screenCoordinate.xy, vec2( 0, uvOffset ).mul( radius ) ).div( size ) );
+
+		if ( shadowPass.value.isDepthArrayTexture || shadowPass.value.isDataArrayTexture ) {
+
+			depth = depth.depth( depthLayer );
+
+		}
+
+		depth = depth.x;
+
 		mean.addAssign( depth );
 		squaredMean.addAssign( depth.mul( depth ) );
 
@@ -276,10 +117,10 @@ const VSMPassVertical = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass
  * @param {TextureNode} inputs.shadowPass - The result of the first VSM render pass.
  * @return {Node<vec2>} The VSM output.
  */
-const VSMPassHorizontal = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass } ) => {
+const VSMPassHorizontal = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass, depthLayer } ) => {
 
-	const mean = float( 0 ).toVar();
-	const squaredMean = float( 0 ).toVar();
+	const mean = float( 0 ).toVar( 'meanHorizontal' );
+	const squaredMean = float( 0 ).toVar( 'squareMeanHorizontal' );
 
 	const uvStride = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( 2 ).div( samples.sub( 1 ) ) );
 	const uvStart = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( - 1 ) );
@@ -288,7 +129,14 @@ const VSMPassHorizontal = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPas
 
 		const uvOffset = uvStart.add( float( i ).mul( uvStride ) );
 
-		const distribution = shadowPass.sample( add( screenCoordinate.xy, vec2( uvOffset, 0 ).mul( radius ) ).div( size ) );
+		let distribution = shadowPass.sample( add( screenCoordinate.xy, vec2( uvOffset, 0 ).mul( radius ) ).div( size ) );
+
+		if ( shadowPass.value.isDepthArrayTexture || shadowPass.value.isDataArrayTexture ) {
+
+			distribution = distribution.depth( depthLayer );
+
+		}
+
 		mean.addAssign( distribution.x );
 		squaredMean.addAssign( add( distribution.y.mul( distribution.y ), distribution.x.mul( distribution.x ) ) );
 
@@ -406,6 +254,15 @@ class ShadowNode extends ShadowBaseNode {
 		 */
 		this.isShadowNode = true;
 
+		/**
+		 * This index can be used when overriding setupRenderTarget with a RenderTarget Array to specify the depth layer.
+		 *
+		 * @type {number}
+		 * @readonly
+		 * @default true
+		 */
+		this.depthLayer = 0;
+
 	}
 
 	/**
@@ -419,7 +276,7 @@ class ShadowNode extends ShadowBaseNode {
 	 * @param {LightShadow} inputs.shadow - The light shadow.
 	 * @return {Node<float>} The result node of the shadow filtering.
 	 */
-	setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow } ) {
+	setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow, depthLayer } ) {
 
 		const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
 			.and( shadowCoord.x.lessThanEqual( 1 ) )
@@ -427,7 +284,7 @@ class ShadowNode extends ShadowBaseNode {
 			.and( shadowCoord.y.lessThanEqual( 1 ) )
 			.and( shadowCoord.z.lessThanEqual( 1 ) );
 
-		const shadowNode = filterFn( { depthTexture, shadowCoord, shadow } );
+		const shadowNode = filterFn( { depthTexture, shadowCoord, shadow, depthLayer } );
 
 		return frustumTest.select( shadowNode, float( 1 ) );
 
@@ -499,6 +356,21 @@ class ShadowNode extends ShadowBaseNode {
 
 	}
 
+
+	setupRenderTarget( shadow, builder ) {
+
+		const depthTexture = new DepthTexture( shadow.mapSize.width, shadow.mapSize.height );
+		depthTexture.name = 'ShadowDepthTexture';
+		depthTexture.compareFunction = LessCompare;
+
+		const shadowMap = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height );
+		shadowMap.texture.name = 'ShadowMap';
+		shadowMap.depthTexture = depthTexture;
+
+		return { shadowMap, depthTexture };
+
+	}
+
 	/**
 	 * Setups the shadow output node.
 	 *
@@ -513,11 +385,7 @@ class ShadowNode extends ShadowBaseNode {
 
 		const shadowMapType = renderer.shadowMap.type;
 
-		const depthTexture = new DepthTexture( shadow.mapSize.width, shadow.mapSize.height );
-		depthTexture.compareFunction = LessCompare;
-
-		const shadowMap = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height );
-		shadowMap.depthTexture = depthTexture;
+		const { depthTexture, shadowMap } = this.setupRenderTarget( shadow, builder );
 
 		shadow.camera.updateProjectionMatrix();
 
@@ -527,22 +395,60 @@ class ShadowNode extends ShadowBaseNode {
 
 			depthTexture.compareFunction = null; // VSM does not use textureSampleCompare()/texture2DCompare()
 
-			this.vsmShadowMapVertical = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType } );
-			this.vsmShadowMapHorizontal = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType } );
+			if ( shadowMap.isRenderTargetArray ) {
+
+				if ( ! shadowMap._vsmShadowMapVertical ) {
+
+					shadowMap._vsmShadowMapVertical = builder.createRenderTargetArray( shadow.mapSize.width, shadow.mapSize.height, shadowMap.depth, { format: RGFormat, type: HalfFloatType, depthBuffer: false } );
+					shadowMap._vsmShadowMapVertical.texture.name = 'VSMVertical';
+
+				}
+
+				this.vsmShadowMapVertical = shadowMap._vsmShadowMapVertical;
+
+				if ( ! shadowMap._vsmShadowMapHorizontal ) {
+
+					shadowMap._vsmShadowMapHorizontal = builder.createRenderTargetArray( shadow.mapSize.width, shadow.mapSize.height, shadowMap.depth, { format: RGFormat, type: HalfFloatType, depthBuffer: false } );
+					shadowMap._vsmShadowMapHorizontal.texture.name = 'VSMHorizontal';
+
+				}
+
+				this.vsmShadowMapHorizontal = shadowMap._vsmShadowMapHorizontal;
+
+			} else {
+
+				this.vsmShadowMapVertical = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType, depthBuffer: false } );
+				this.vsmShadowMapHorizontal = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType, depthBuffer: false } );
+
+			}
+
+
+			let shadowPassVertical = texture( depthTexture );
+
+			if ( depthTexture.isDepthArrayTexture ) {
 
-			const shadowPassVertical = texture( depthTexture );
-			const shadowPassHorizontal = texture( this.vsmShadowMapVertical.texture );
+				shadowPassVertical = shadowPassVertical.depth( this.depthLayer );
+
+			}
+
+			let shadowPassHorizontal = texture( this.vsmShadowMapVertical.texture );
+
+			if ( depthTexture.isDepthArrayTexture ) {
+
+				shadowPassHorizontal = shadowPassHorizontal.depth( this.depthLayer );
+
+			}
 
 			const samples = reference( 'blurSamples', 'float', shadow ).setGroup( renderGroup );
 			const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
 			const size = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
 
 			let material = this.vsmMaterialVertical || ( this.vsmMaterialVertical = new NodeMaterial() );
-			material.fragmentNode = VSMPassVertical( { samples, radius, size, shadowPass: shadowPassVertical } ).context( builder.getSharedContext() );
+			material.fragmentNode = VSMPassVertical( { samples, radius, size, shadowPass: shadowPassVertical, depthLayer: this.depthLayer } ).context( builder.getSharedContext() );
 			material.name = 'VSMVertical';
 
 			material = this.vsmMaterialHorizontal || ( this.vsmMaterialHorizontal = new NodeMaterial() );
-			material.fragmentNode = VSMPassHorizontal( { samples, radius, size, shadowPass: shadowPassHorizontal } ).context( builder.getSharedContext() );
+			material.fragmentNode = VSMPassHorizontal( { samples, radius, size, shadowPass: shadowPassHorizontal, depthLayer: this.depthLayer } ).context( builder.getSharedContext() );
 			material.name = 'VSMHorizontal';
 
 		}
@@ -567,9 +473,16 @@ class ShadowNode extends ShadowBaseNode {
 
 		const shadowDepthTexture = ( shadowMapType === VSMShadowMap ) ? this.vsmShadowMapHorizontal.texture : depthTexture;
 
-		const shadowNode = this.setupShadowFilter( builder, { filterFn, shadowTexture: shadowMap.texture, depthTexture: shadowDepthTexture, shadowCoord, shadow } );
+		const shadowNode = this.setupShadowFilter( builder, { filterFn, shadowTexture: shadowMap.texture, depthTexture: shadowDepthTexture, shadowCoord, shadow, depthLayer: this.depthLayer } );
+
+		let shadowColor = texture( shadowMap.texture, shadowCoord );
+
+		if ( depthTexture.isDepthArrayTexture ) {
+
+			shadowColor = shadowColor.depth( this.depthLayer );
+
+		}
 
-		const shadowColor = texture( shadowMap.texture, shadowCoord );
 		const shadowOutput = mix( 1, shadowNode.rgb.mix( shadowColor, 1 ), shadowIntensity.mul( shadowColor.a ) ).toVar();
 
 		this.shadowMap = shadowMap;
@@ -635,7 +548,7 @@ class ShadowNode extends ShadowBaseNode {
 
 		shadow.updateMatrices( light );
 
-		shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height );
+		shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height, shadowMap.depth );
 
 		renderer.render( scene, shadow.camera );
 
@@ -667,25 +580,7 @@ class ShadowNode extends ShadowBaseNode {
 
 		scene.overrideMaterial = getShadowMaterial( light );
 
-		renderer.setRenderObjectFunction( ( object, scene, _camera, geometry, material, group, ...params ) => {
-
-			if ( object.castShadow === true || ( object.receiveShadow && shadowType === VSMShadowMap ) ) {
-
-				if ( useVelocity ) {
-
-					getDataFromObject( object ).useVelocity = true;
-
-				}
-
-				object.onBeforeShadow( renderer, object, camera, shadow.camera, geometry, scene.overrideMaterial, group );
-
-				renderer.renderObject( object, scene, _camera, geometry, material, group, ...params );
-
-				object.onAfterShadow( renderer, object, camera, shadow.camera, geometry, scene.overrideMaterial, group );
-
-			}
-
-		} );
+		renderer.setRenderObjectFunction( getShadowRenderObjectFunction( renderer, shadow, shadowType, useVelocity ) );
 
 		renderer.setRenderTarget( shadowMap );
 
@@ -714,8 +609,9 @@ class ShadowNode extends ShadowBaseNode {
 
 		const { shadow } = this;
 
-		this.vsmShadowMapVertical.setSize( shadow.mapSize.width, shadow.mapSize.height );
-		this.vsmShadowMapHorizontal.setSize( shadow.mapSize.width, shadow.mapSize.height );
+		const depth = this.shadowMap.depth;
+		this.vsmShadowMapVertical.setSize( shadow.mapSize.width, shadow.mapSize.height, depth );
+		this.vsmShadowMapHorizontal.setSize( shadow.mapSize.width, shadow.mapSize.height, depth );
 
 		renderer.setRenderTarget( this.vsmShadowMapVertical );
 		_quadMesh.material = this.vsmMaterialVertical;

+ 6 - 3
src/objects/BatchedMesh.js

@@ -10,6 +10,7 @@ import { Sphere } from '../math/Sphere.js';
 import { Frustum } from '../math/Frustum.js';
 import { Vector3 } from '../math/Vector3.js';
 import { Color } from '../math/Color.js';
+import { FrustumArray } from '../math/FrustumArray.js';
 
 function ascIdSort( a, b ) {
 
@@ -79,6 +80,7 @@ class MultiDrawRenderList {
 const _matrix = /*@__PURE__*/ new Matrix4();
 const _whiteColor = /*@__PURE__*/ new Color( 1, 1, 1 );
 const _frustum = /*@__PURE__*/ new Frustum();
+const _frustumArray = /*@__PURE__*/ new FrustumArray();
 const _box = /*@__PURE__*/ new Box3();
 const _sphere = /*@__PURE__*/ new Sphere();
 const _vector = /*@__PURE__*/ new Vector3();
@@ -1511,8 +1513,9 @@ class BatchedMesh extends Mesh {
 		const indirectTexture = this._indirectTexture;
 		const indirectArray = indirectTexture.image.data;
 
+		const frustum = camera.isArrayCamera ? _frustumArray : _frustum;
 		// prepare the frustum in the local frame
-		if ( perObjectFrustumCulled ) {
+		if ( perObjectFrustumCulled && ! camera.isArrayCamera ) {
 
 			_matrix
 				.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse )
@@ -1546,7 +1549,7 @@ class BatchedMesh extends Mesh {
 					let culled = false;
 					if ( perObjectFrustumCulled ) {
 
-						culled = ! _frustum.intersectsSphere( _sphere );
+						culled = ! frustum.intersectsSphere( _sphere, camera );
 
 					}
 
@@ -1603,7 +1606,7 @@ class BatchedMesh extends Mesh {
 						// get the bounds in world space
 						this.getMatrixAt( i, _matrix );
 						this.getBoundingSphereAt( geometryId, _sphere ).applyMatrix4( _matrix );
-						culled = ! _frustum.intersectsSphere( _sphere );
+						culled = ! frustum.intersectsSphere( _sphere, camera );
 
 					}
 

+ 18 - 4
src/renderers/common/Renderer.js

@@ -22,6 +22,7 @@ import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
 
 import { Scene } from '../../scenes/Scene.js';
 import { Frustum } from '../../math/Frustum.js';
+import { FrustumArray } from '../../math/FrustumArray.js';
 import { Matrix4 } from '../../math/Matrix4.js';
 import { Vector2 } from '../../math/Vector2.js';
 import { Vector4 } from '../../math/Vector4.js';
@@ -32,6 +33,8 @@ const _scene = /*@__PURE__*/ new Scene();
 const _drawingBufferSize = /*@__PURE__*/ new Vector2();
 const _screen = /*@__PURE__*/ new Vector4();
 const _frustum = /*@__PURE__*/ new Frustum();
+const _frustumArray = /*@__PURE__*/ new FrustumArray();
+
 const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
 const _vector4 = /*@__PURE__*/ new Vector4();
 
@@ -1341,8 +1344,14 @@ class Renderer {
 
 		//
 
-		_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
-		_frustum.setFromProjectionMatrix( _projScreenMatrix, coordinateSystem );
+		const frustum = camera.isArrayCamera ? _frustumArray : _frustum;
+
+		if ( ! camera.isArrayCamera ) {
+
+			_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
+			frustum.setFromProjectionMatrix( _projScreenMatrix, coordinateSystem );
+
+		}
 
 		const renderList = this._renderLists.get( scene, camera );
 		renderList.begin();
@@ -1396,6 +1405,7 @@ class Renderer {
 
 		//
 
+		renderContext.camera = camera;
 		this.backend.beginRender( renderContext );
 
 		// process render lists
@@ -2545,7 +2555,9 @@ class Renderer {
 
 			} else if ( object.isSprite ) {
 
-				if ( ! object.frustumCulled || _frustum.intersectsSprite( object ) ) {
+				const frustum = camera.isArrayCamera ? _frustumArray : _frustum;
+
+				if ( ! object.frustumCulled || frustum.intersectsSprite( object, camera ) ) {
 
 					if ( this.sortObjects === true ) {
 
@@ -2569,7 +2581,9 @@ class Renderer {
 
 			} else if ( object.isMesh || object.isLine || object.isPoints ) {
 
-				if ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) {
+				const frustum = camera.isArrayCamera ? _frustumArray : _frustum;
+
+				if ( ! object.frustumCulled || frustum.intersectsObject( object, camera ) ) {
 
 					const { geometry, material } = object;
 

+ 1 - 0
src/renderers/common/Textures.js

@@ -95,6 +95,7 @@ class Textures extends DataMap {
 				depthTexture.needsUpdate = true;
 				depthTexture.image.width = mipWidth;
 				depthTexture.image.height = mipHeight;
+				depthTexture.image.depth = depthTexture.isDepthArrayTexture ? depthTexture.image.depth : 1;
 
 			}
 

+ 105 - 11
src/renderers/webgl-fallback/WebGLBackend.js

@@ -960,6 +960,20 @@ class WebGLBackend extends Backend {
 
 	}
 
+	/**
+	 * Internal to determine if the current render target is a render target array with depth 2D array texture.
+	 *
+	 * @param {RenderContext} renderContext - The render context.
+	 * @return {boolean} Whether the render target is a render target array with depth 2D array texture.
+	 *
+	 * @private
+	 */
+	_isRenderCameraDepthArray( renderContext ) {
+
+		return renderContext.depthTexture && renderContext.depthTexture.isDepthArrayTexture && renderContext.camera.isArrayCamera;
+
+	}
+
 	/**
 	 * Executes a draw command for the given render object.
 	 *
@@ -1159,25 +1173,71 @@ class WebGLBackend extends Backend {
 			const cameraIndexData = this.get( cameraIndex );
 			const pixelRatio = this.renderer.getPixelRatio();
 
+			const renderTarget = this._currentContext.renderTarget;
+			const isRenderCameraDepthArray = this._isRenderCameraDepthArray( this._currentContext );
+			const prevActiveCubeFace = this._currentContext.activeCubeFace;
+
+			if ( isRenderCameraDepthArray ) {
+
+				// Clear the depth texture
+				const textureData = this.get( renderTarget.depthTexture );
+
+				if ( textureData.clearedRenderId !== this.renderer._nodes.nodeFrame.renderId ) {
+
+					textureData.clearedRenderId = this.renderer._nodes.nodeFrame.renderId;
+
+					const { stencilBuffer } = renderTarget;
+
+					for ( let i = 0, len = cameras.length; i < len; i ++ ) {
+
+						this.renderer._activeCubeFace = i;
+						this._currentContext.activeCubeFace = i;
+
+						this._setFramebuffer( this._currentContext );
+						this.clear( false, true, stencilBuffer, this._currentContext, false );
+
+					}
+
+					this.renderer._activeCubeFace = prevActiveCubeFace;
+					this._currentContext.activeCubeFace = prevActiveCubeFace;
+
+				}
+
+			}
+
 			for ( let i = 0, len = cameras.length; i < len; i ++ ) {
 
 				const subCamera = cameras[ i ];
 
 				if ( object.layers.test( subCamera.layers ) ) {
 
+					if ( isRenderCameraDepthArray ) {
+
+						// Update the active layer
+						this.renderer._activeCubeFace = i;
+						this._currentContext.activeCubeFace = i;
+
+						this._setFramebuffer( this._currentContext );
+
+					}
+
 					const vp = subCamera.viewport;
 
-					const x = vp.x * pixelRatio;
-					const y = vp.y * pixelRatio;
-					const width = vp.width * pixelRatio;
-					const height = vp.height * pixelRatio;
+					if ( vp !== undefined ) {
 
-					state.viewport(
-						Math.floor( x ),
-						Math.floor( renderObject.context.height - height - y ),
-						Math.floor( width ),
-						Math.floor( height )
-					);
+						const x = vp.x * pixelRatio;
+						const y = vp.y * pixelRatio;
+						const width = vp.width * pixelRatio;
+						const height = vp.height * pixelRatio;
+
+						state.viewport(
+							Math.floor( x ),
+							Math.floor( renderObject.context.height - height - y ),
+							Math.floor( width ),
+							Math.floor( height )
+						);
+
+					}
 
 					state.bindBufferBase( gl.UNIFORM_BUFFER, cameraIndexData.index, cameraData.indexesGPU[ i ] );
 
@@ -1185,6 +1245,9 @@ class WebGLBackend extends Backend {
 
 				}
 
+				this._currentContext.activeCubeFace = prevActiveCubeFace;
+				this.renderer._activeCubeFace = prevActiveCubeFace;
+
 			}
 
 		} else {
@@ -2037,7 +2100,18 @@ class WebGLBackend extends Backend {
 
 						} else {
 
-							gl.framebufferTexture2D( gl.FRAMEBUFFER, depthStyle, gl.TEXTURE_2D, textureData.textureGPU, 0 );
+							if ( descriptor.depthTexture.isDepthArrayTexture ) {
+
+								const layer = this.renderer._activeCubeFace;
+
+								gl.framebufferTextureLayer( gl.FRAMEBUFFER, depthStyle, textureData.textureGPU, 0, layer );
+
+
+							} else {
+
+								gl.framebufferTexture2D( gl.FRAMEBUFFER, depthStyle, gl.TEXTURE_2D, textureData.textureGPU, 0 );
+
+							}
 
 						}
 
@@ -2047,6 +2121,26 @@ class WebGLBackend extends Backend {
 
 			} else {
 
+				const isRenderCameraDepthArray = this._isRenderCameraDepthArray( descriptor );
+
+				if ( isRenderCameraDepthArray ) {
+
+					state.bindFramebuffer( gl.FRAMEBUFFER, fb );
+
+					const layer = this.renderer._activeCubeFace;
+
+					const depthData = this.get( descriptor.depthTexture );
+					const depthStyle = stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
+					gl.framebufferTextureLayer(
+						gl.FRAMEBUFFER,
+						depthStyle,
+						depthData.textureGPU,
+						0,
+						layer
+					);
+
+				}
+
 				// rebind external XR textures
 
 				if ( isXRRenderTarget && hasExternalTextures ) {

+ 19 - 1
src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js

@@ -43,6 +43,8 @@ precision highp isamplerCube;
 precision highp isampler2DArray;
 
 precision lowp sampler2DShadow;
+precision lowp sampler2DArrayShadow;
+precision lowp samplerCubeShadow;
 `;
 
 /**
@@ -389,6 +391,8 @@ ${ flowData.code }
 
 		if ( texture.isDepthTexture ) {
 
+			if ( depthSnippet ) uvSnippet = `vec4( ${ uvSnippet }, ${ depthSnippet } )`;
+
 			return `texture( ${ textureProperty }, ${ uvSnippet } ).x`;
 
 		} else {
@@ -462,6 +466,12 @@ ${ flowData.code }
 
 		if ( shaderStage === 'fragment' ) {
 
+			if ( depthSnippet ) {
+
+				return `texture( ${ textureProperty }, vec4( ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet } ) )`;
+
+			}
+
 			return `texture( ${ textureProperty }, vec3( ${ uvSnippet }, ${ compareSnippet } ) )`;
 
 		} else {
@@ -542,7 +552,15 @@ ${ flowData.code }
 
 				} else if ( texture.compareFunction ) {
 
-					snippet = `sampler2DShadow ${ uniform.name };`;
+					if ( texture.isDepthArrayTexture === true ) {
+
+						snippet = `sampler2DArrayShadow ${ uniform.name };`;
+
+					} else {
+
+						snippet = `sampler2DShadow ${ uniform.name };`;
+
+					}
 
 				} else if ( texture.isDataArrayTexture === true || texture.isCompressedArrayTexture === true ) {
 

+ 11 - 6
src/renderers/webgl-fallback/utils/WebGLTextureUtils.js

@@ -112,7 +112,7 @@ class WebGLTextureUtils {
 
 			glTextureType = gl.TEXTURE_CUBE_MAP;
 
-		} else if ( texture.isDataArrayTexture === true || texture.isCompressedArrayTexture === true ) {
+		} else if ( texture.isDepthArrayTexture === true || texture.isDataArrayTexture === true || texture.isCompressedArrayTexture === true ) {
 
 			glTextureType = gl.TEXTURE_2D_ARRAY;
 
@@ -306,7 +306,12 @@ class WebGLTextureUtils {
 
 		if ( textureType === gl.TEXTURE_3D || textureType === gl.TEXTURE_2D_ARRAY ) {
 
-			gl.texParameteri( textureType, gl.TEXTURE_WRAP_R, wrappingToGL[ texture.wrapR ] );
+			// WebGL 2 does not support wrapping for depth 2D array textures
+			if ( ! texture.isDepthArrayTexture ) {
+
+				gl.texParameteri( textureType, gl.TEXTURE_WRAP_R, wrappingToGL[ texture.wrapR ] );
+
+			}
 
 		}
 
@@ -404,7 +409,7 @@ class WebGLTextureUtils {
 
 		this.setTextureParameters( glTextureType, texture );
 
-		if ( texture.isDataArrayTexture || texture.isCompressedArrayTexture ) {
+		if ( texture.isDepthArrayTexture || texture.isDataArrayTexture || texture.isCompressedArrayTexture ) {
 
 			gl.texStorage3D( gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, width, height, depth );
 
@@ -566,7 +571,7 @@ class WebGLTextureUtils {
 
 			}
 
-		} else if ( texture.isDataArrayTexture ) {
+		} else if ( texture.isDataArrayTexture || texture.isDepthArrayTexture ) {
 
 			const image = options.image;
 
@@ -731,7 +736,7 @@ class WebGLTextureUtils {
 			width = Math.floor( image.width * levelScale );
 			height = Math.floor( image.height * levelScale );
 
-			if ( srcTexture.isDataArrayTexture ) {
+			if ( srcTexture.isDataArrayTexture || srcTexture.isDepthArrayTexture ) {
 
 				depth = image.depth;
 
@@ -784,7 +789,7 @@ class WebGLTextureUtils {
 		gl.pixelStorei( gl.UNPACK_SKIP_IMAGES, minZ );
 
 		// set up the src texture
-		const isDst3D = dstTexture.isDataArrayTexture || dstTexture.isData3DTexture;
+		const isDst3D = dstTexture.isDataArrayTexture || dstTexture.isData3DTexture || dstTexture.isDepthArrayTexture;
 		if ( srcTexture.isRenderTargetTexture || srcTexture.isDepthTexture ) {
 
 			const srcTextureData = backend.get( srcTexture );

+ 421 - 121
src/renderers/webgpu/WebGPUBackend.js

@@ -342,6 +342,20 @@ class WebGPUBackend extends Backend {
 
 	}
 
+	/**
+	 * Internal to determine if the current render target is a render target array with depth 2D array texture.
+	 *
+	 * @param {RenderContext} renderContext - The render context.
+	 * @return {boolean} Whether the render target is a render target array with depth 2D array texture.
+	 *
+	 * @private
+	 */
+	_isRenderCameraDepthArray( renderContext ) {
+
+		return renderContext.depthTexture && renderContext.depthTexture.isDepthArrayTexture && renderContext.camera.isArrayCamera;
+
+	}
+
 	/**
 	 * Returns the render pass descriptor for the given render context.
 	 *
@@ -397,6 +411,8 @@ class WebGPUBackend extends Backend {
 
 			let sliceIndex;
 
+			const isRenderCameraDepthArray = this._isRenderCameraDepthArray( renderContext );
+
 			for ( let i = 0; i < textures.length; i ++ ) {
 
 				const textureData = this.get( textures[ i ] );
@@ -420,32 +436,60 @@ class WebGPUBackend extends Backend {
 
 				} else if ( renderTarget.isRenderTargetArray ) {
 
-					viewDescriptor.dimension = GPUTextureViewDimension.TwoDArray;
-					viewDescriptor.depthOrArrayLayers = textures[ i ].image.depth;
+					if ( isRenderCameraDepthArray === true ) {
+
+						const cameras = renderContext.camera.cameras;
+						for ( let layer = 0; layer < cameras.length; layer ++ ) {
+
+							const layerViewDescriptor = {
+								...viewDescriptor,
+								baseArrayLayer: layer,
+								arrayLayerCount: 1,
+								dimension: GPUTextureViewDimension.TwoD
+							};
+							const textureView = textureData.texture.createView( layerViewDescriptor );
+							textureViews.push( {
+								view: textureView,
+								resolveTarget: undefined,
+								depthSlice: undefined
+							} );
+
+						}
+
+					} else {
+
+						viewDescriptor.dimension = GPUTextureViewDimension.TwoDArray;
+						viewDescriptor.depthOrArrayLayers = textures[ i ].image.depth;
+
+					}
 
 				}
 
-				const textureView = textureData.texture.createView( viewDescriptor );
+				if ( isRenderCameraDepthArray !== true ) {
 
-				let view, resolveTarget;
+					const textureView = textureData.texture.createView( viewDescriptor );
 
-				if ( textureData.msaaTexture !== undefined ) {
+					let view, resolveTarget;
 
-					view = textureData.msaaTexture.createView();
-					resolveTarget = textureView;
+					if ( textureData.msaaTexture !== undefined ) {
 
-				} else {
+						view = textureData.msaaTexture.createView();
+						resolveTarget = textureView;
 
-					view = textureView;
-					resolveTarget = undefined;
+					} else {
 
-				}
+						view = textureView;
+						resolveTarget = undefined;
 
-				textureViews.push( {
-					view,
-					resolveTarget,
-					depthSlice: sliceIndex
-				} );
+					}
+
+					textureViews.push( {
+						view,
+						resolveTarget,
+						depthSlice: sliceIndex
+					} );
+
+				}
 
 			}
 
@@ -454,7 +498,16 @@ class WebGPUBackend extends Backend {
 			if ( renderContext.depth ) {
 
 				const depthTextureData = this.get( renderContext.depthTexture );
-				descriptorBase.depthStencilView = depthTextureData.texture.createView();
+				const options = {};
+				if ( renderContext.depthTexture.isDepthArrayTexture ) {
+
+					options.dimension = GPUTextureViewDimension.TwoD;
+					options.arrayLayerCount = 1;
+					options.baseArrayLayer = renderContext.activeCubeFace;
+
+				}
+
+				descriptorBase.depthStencilView = depthTextureData.texture.createView( options );
 
 			}
 
@@ -576,15 +629,15 @@ class WebGPUBackend extends Backend {
 
 					colorAttachment.clearValue = i === 0 ? renderContext.clearColorValue : { r: 0, g: 0, b: 0, a: 1 };
 					colorAttachment.loadOp = GPULoadOp.Clear;
-					colorAttachment.storeOp = GPUStoreOp.Store;
 
 				} else {
 
 					colorAttachment.loadOp = GPULoadOp.Load;
-					colorAttachment.storeOp = GPUStoreOp.Store;
 
 				}
 
+				colorAttachment.storeOp = GPUStoreOp.Store;
+
 			}
 
 		} else {
@@ -595,15 +648,15 @@ class WebGPUBackend extends Backend {
 
 				colorAttachment.clearValue = renderContext.clearColorValue;
 				colorAttachment.loadOp = GPULoadOp.Clear;
-				colorAttachment.storeOp = GPUStoreOp.Store;
 
 			} else {
 
 				colorAttachment.loadOp = GPULoadOp.Load;
-				colorAttachment.storeOp = GPUStoreOp.Store;
 
 			}
 
+		  	colorAttachment.storeOp = GPUStoreOp.Store;
+
 		}
 
 		//
@@ -614,60 +667,235 @@ class WebGPUBackend extends Backend {
 
 				depthStencilAttachment.depthClearValue = renderContext.clearDepthValue;
 				depthStencilAttachment.depthLoadOp = GPULoadOp.Clear;
-				depthStencilAttachment.depthStoreOp = GPUStoreOp.Store;
 
 			} else {
 
 				depthStencilAttachment.depthLoadOp = GPULoadOp.Load;
-				depthStencilAttachment.depthStoreOp = GPUStoreOp.Store;
 
 			}
 
+		  depthStencilAttachment.depthStoreOp = GPUStoreOp.Store;
+
 		}
 
 		if ( renderContext.stencil ) {
 
-			if ( renderContext.clearStencil ) {
+		  if ( renderContext.clearStencil ) {
 
 				depthStencilAttachment.stencilClearValue = renderContext.clearStencilValue;
 				depthStencilAttachment.stencilLoadOp = GPULoadOp.Clear;
-				depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store;
 
 			} else {
 
 				depthStencilAttachment.stencilLoadOp = GPULoadOp.Load;
-				depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store;
 
 			}
 
+		  depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store;
+
 		}
 
 		//
 
 		const encoder = device.createCommandEncoder( { label: 'renderContext_' + renderContext.id } );
-		const currentPass = encoder.beginRenderPass( descriptor );
+
+		// shadow arrays - prepare bundle encoders for each camera in an array camera
+
+		if ( this._isRenderCameraDepthArray( renderContext ) === true ) {
+
+			const cameras = renderContext.camera.cameras;
+
+			if ( ! renderContextData.layerDescriptors || renderContextData.layerDescriptors.length !== cameras.length ) {
+
+				this._createDepthLayerDescriptors( renderContext, renderContextData, descriptor, cameras );
+
+			} else {
+
+				this._updateDepthLayerDescriptors( renderContext, renderContextData, cameras );
+
+			}
+
+			// Create bundle encoders for each layer
+			renderContextData.bundleEncoders = [];
+			renderContextData.bundleSets = [];
+
+			// Create separate bundle encoders for each camera in the array
+			for ( let i = 0; i < cameras.length; i ++ ) {
+
+				const bundleEncoder = this.pipelineUtils.createBundleEncoder(
+					renderContext,
+					'renderBundleArrayCamera_' + i
+				);
+
+				// Initialize state tracking for this bundle
+				const bundleSets = {
+					attributes: {},
+					bindingGroups: [],
+					pipeline: null,
+					index: null
+				};
+
+				renderContextData.bundleEncoders.push( bundleEncoder );
+				renderContextData.bundleSets.push( bundleSets );
+
+			}
+
+			// We'll complete the bundles in finishRender
+			renderContextData.currentPass = null;
+
+		} else {
+
+			const currentPass = encoder.beginRenderPass( descriptor );
+			renderContextData.currentPass = currentPass;
+
+			if ( renderContext.viewport ) {
+
+				this.updateViewport( renderContext );
+
+			}
+
+			if ( renderContext.scissor ) {
+
+				const { x, y, width, height } = renderContext.scissorValue;
+				currentPass.setScissorRect( x, y, width, height );
+
+			}
+
+		}
 
 		//
 
 		renderContextData.descriptor = descriptor;
 		renderContextData.encoder = encoder;
-		renderContextData.currentPass = currentPass;
 		renderContextData.currentSets = { attributes: {}, bindingGroups: [], pipeline: null, index: null };
 		renderContextData.renderBundles = [];
 
-		//
+	}
+
+	/**
+	 * This method creates layer descriptors for each camera in an array camera
+	 * to prepare for rendering to a depth array texture.
+	 *
+	 * @param {RenderContext} renderContext - The render context.
+	 * @param {Object} renderContextData - The render context data.
+	 * @param {Object} descriptor  - The render pass descriptor.
+	 * @param {ArrayCamera} cameras - The array camera.
+	 *
+	 * @private
+	 */
+	_createDepthLayerDescriptors( renderContext, renderContextData, descriptor, cameras ) {
+
+		const depthStencilAttachment = descriptor.depthStencilAttachment;
+		renderContextData.layerDescriptors = [];
+
+		const depthTextureData = this.get( renderContext.depthTexture );
+		if ( ! depthTextureData.viewCache ) {
+
+			depthTextureData.viewCache = [];
+
+		}
+
+		for ( let i = 0; i < cameras.length; i ++ ) {
+
+			const layerDescriptor = {
+				...descriptor,
+				colorAttachments: [ {
+					...descriptor.colorAttachments[ 0 ],
+					view: descriptor.colorAttachments[ i ].view
+				} ]
+			};
+
+			if ( descriptor.depthStencilAttachment ) {
+
+				const layerIndex = i;
+
+				if ( ! depthTextureData.viewCache[ layerIndex ] ) {
 
-		if ( renderContext.viewport ) {
+					depthTextureData.viewCache[ layerIndex ] = depthTextureData.texture.createView( {
+						dimension: GPUTextureViewDimension.TwoD,
+						baseArrayLayer: i,
+						arrayLayerCount: 1
+					} );
 
-			this.updateViewport( renderContext );
+				}
+
+				layerDescriptor.depthStencilAttachment = {
+					view: depthTextureData.viewCache[ layerIndex ],
+					depthLoadOp: depthStencilAttachment.depthLoadOp || GPULoadOp.Clear,
+					depthStoreOp: depthStencilAttachment.depthStoreOp || GPUStoreOp.Store,
+					depthClearValue: depthStencilAttachment.depthClearValue || 1.0
+				};
+
+				if ( renderContext.stencil ) {
+
+					layerDescriptor.depthStencilAttachment.stencilLoadOp = depthStencilAttachment.stencilLoadOp;
+					layerDescriptor.depthStencilAttachment.stencilStoreOp = depthStencilAttachment.stencilStoreOp;
+					layerDescriptor.depthStencilAttachment.stencilClearValue = depthStencilAttachment.stencilClearValue;
+
+				}
+
+			} else {
+
+				layerDescriptor.depthStencilAttachment = { ...depthStencilAttachment };
+
+			}
+
+			renderContextData.layerDescriptors.push( layerDescriptor );
 
 		}
 
-		if ( renderContext.scissor ) {
+	}
+
+	/**
+	 * This method updates the layer descriptors for each camera in an array camera
+	 * to prepare for rendering to a depth array texture.
+	 *
+	 * @param {RenderContext} renderContext - The render context.
+	 * @param {Object} renderContextData - The render context data.
+	 * @param {ArrayCamera} cameras - The array camera.
+	 *
+	 */
+	_updateDepthLayerDescriptors( renderContext, renderContextData, cameras ) {
+
+		for ( let i = 0; i < cameras.length; i ++ ) {
+
+			const layerDescriptor = renderContextData.layerDescriptors[ i ];
+
+			if ( layerDescriptor.depthStencilAttachment ) {
 
-			const { x, y, width, height } = renderContext.scissorValue;
+				const depthAttachment = layerDescriptor.depthStencilAttachment;
 
-			currentPass.setScissorRect( x, y, width, height );
+				if ( renderContext.depth ) {
+
+					if ( renderContext.clearDepth ) {
+
+						depthAttachment.depthClearValue = renderContext.clearDepthValue;
+						depthAttachment.depthLoadOp = GPULoadOp.Clear;
+
+					} else {
+
+						depthAttachment.depthLoadOp = GPULoadOp.Load;
+
+					}
+
+				}
+
+				if ( renderContext.stencil ) {
+
+					if ( renderContext.clearStencil ) {
+
+						depthAttachment.stencilClearValue = renderContext.clearStencilValue;
+						depthAttachment.stencilLoadOp = GPULoadOp.Clear;
+
+					} else {
+
+						depthAttachment.stencilLoadOp = GPULoadOp.Load;
+
+					}
+
+				}
+
+			}
 
 		}
 
@@ -696,7 +924,55 @@ class WebGPUBackend extends Backend {
 
 		}
 
-		renderContextData.currentPass.end();
+		// shadow arrays - Execute bundles for each layer
+
+		const encoder = renderContextData.encoder;
+
+		if ( this._isRenderCameraDepthArray( renderContext ) === true ) {
+
+		  const bundles = [];
+
+		  for ( let i = 0; i < renderContextData.bundleEncoders.length; i ++ ) {
+
+				const bundleEncoder = renderContextData.bundleEncoders[ i ];
+				bundles.push( bundleEncoder.finish() );
+
+			}
+
+		  for ( let i = 0; i < renderContextData.layerDescriptors.length; i ++ ) {
+
+				if ( i < bundles.length ) {
+
+					const layerDescriptor = renderContextData.layerDescriptors[ i ];
+					const renderPass = encoder.beginRenderPass( layerDescriptor );
+
+					if ( renderContext.viewport ) {
+
+						const { x, y, width, height, minDepth, maxDepth } = renderContext.viewportValue;
+						renderPass.setViewport( x, y, width, height, minDepth, maxDepth );
+
+					}
+
+					if ( renderContext.scissor ) {
+
+						const { x, y, width, height } = renderContext.scissorValue;
+						renderPass.setScissorRect( x, y, width, height );
+
+					}
+
+					renderPass.executeBundles( [ bundles[ i ] ] );
+
+					renderPass.end();
+
+				}
+
+			}
+
+		} else if ( renderContextData.currentPass ) {
+
+		  renderContextData.currentPass.end();
+
+		}
 
 		if ( occlusionQueryCount > 0 ) {
 
@@ -1123,123 +1399,89 @@ class WebGPUBackend extends Backend {
 		const bindings = renderObject.getBindings();
 		const renderContextData = this.get( context );
 		const pipelineGPU = this.get( pipeline ).pipeline;
-		const currentSets = renderContextData.currentSets;
-		const passEncoderGPU = renderContextData.currentPass;
 
-		const drawParams = renderObject.getDrawParameters();
+		const index = renderObject.getIndex();
+		const hasIndex = ( index !== null );
+
 
+		const drawParams = renderObject.getDrawParameters();
 		if ( drawParams === null ) return;
 
 		// pipeline
 
-		if ( currentSets.pipeline !== pipelineGPU ) {
+		const setPipelineAndBindings = ( passEncoderGPU, currentSets ) => {
 
+			// pipeline
 			passEncoderGPU.setPipeline( pipelineGPU );
-
 			currentSets.pipeline = pipelineGPU;
 
-		}
-
-		// bind groups
-
-		const currentBindingGroups = currentSets.bindingGroups;
-
-		for ( let i = 0, l = bindings.length; i < l; i ++ ) {
-
-			const bindGroup = bindings[ i ];
-			const bindingsData = this.get( bindGroup );
-
-			if ( currentBindingGroups[ bindGroup.index ] !== bindGroup.id ) {
-
-				passEncoderGPU.setBindGroup( bindGroup.index, bindingsData.group );
-				currentBindingGroups[ bindGroup.index ] = bindGroup.id;
-
-			}
-
-		}
-
-		// attributes
-
-		const index = renderObject.getIndex();
-
-		const hasIndex = ( index !== null );
-
-		// index
-
-		if ( hasIndex === true ) {
-
-			if ( currentSets.index !== index ) {
+			// bind groups
+			const currentBindingGroups = currentSets.bindingGroups;
+			for ( let i = 0, l = bindings.length; i < l; i ++ ) {
 
-				const buffer = this.get( index ).buffer;
-				const indexFormat = ( index.array instanceof Uint16Array ) ? GPUIndexFormat.Uint16 : GPUIndexFormat.Uint32;
+				const bindGroup = bindings[ i ];
+				const bindingsData = this.get( bindGroup );
+				if ( currentBindingGroups[ bindGroup.index ] !== bindGroup.id ) {
 
-				passEncoderGPU.setIndexBuffer( buffer, indexFormat );
+					passEncoderGPU.setBindGroup( bindGroup.index, bindingsData.group );
+					currentBindingGroups[ bindGroup.index ] = bindGroup.id;
 
-				currentSets.index = index;
+				}
 
 			}
 
-		}
+			// attributes
 
-		// vertex buffers
+			// index
 
-		const vertexBuffers = renderObject.getVertexBuffers();
+			if ( hasIndex === true ) {
 
-		for ( let i = 0, l = vertexBuffers.length; i < l; i ++ ) {
+				if ( currentSets.index !== index ) {
 
-			const vertexBuffer = vertexBuffers[ i ];
+					const buffer = this.get( index ).buffer;
+					const indexFormat = ( index.array instanceof Uint16Array ) ? GPUIndexFormat.Uint16 : GPUIndexFormat.Uint32;
 
-			if ( currentSets.attributes[ i ] !== vertexBuffer ) {
+					passEncoderGPU.setIndexBuffer( buffer, indexFormat );
 
-				const buffer = this.get( vertexBuffer ).buffer;
-				passEncoderGPU.setVertexBuffer( i, buffer );
+					currentSets.index = index;
 
-				currentSets.attributes[ i ] = vertexBuffer;
+				}
 
 			}
+			// vertex buffers
 
-		}
-
-		// occlusion queries - handle multiple consecutive draw calls for an object
+			const vertexBuffers = renderObject.getVertexBuffers();
 
-		if ( renderContextData.occlusionQuerySet !== undefined ) {
+			for ( let i = 0, l = vertexBuffers.length; i < l; i ++ ) {
 
-			const lastObject = renderContextData.lastOcclusionObject;
+				const vertexBuffer = vertexBuffers[ i ];
 
-			if ( lastObject !== object ) {
+				if ( currentSets.attributes[ i ] !== vertexBuffer ) {
 
-				if ( lastObject !== null && lastObject.occlusionTest === true ) {
+					const buffer = this.get( vertexBuffer ).buffer;
+					passEncoderGPU.setVertexBuffer( i, buffer );
 
-					passEncoderGPU.endOcclusionQuery();
-					renderContextData.occlusionQueryIndex ++;
+					currentSets.attributes[ i ] = vertexBuffer;
 
 				}
 
-				if ( object.occlusionTest === true ) {
-
-					passEncoderGPU.beginOcclusionQuery( renderContextData.occlusionQueryIndex );
-					renderContextData.occlusionQueryObjects[ renderContextData.occlusionQueryIndex ] = object;
-
-				}
-
-				renderContextData.lastOcclusionObject = object;
-
 			}
+			// stencil
 
-		}
+			if ( context.stencil === true && material.stencilWrite === true && renderContextData.currentStencilRef !== material.stencilRef ) {
 
-		// stencil
+				passEncoderGPU.setStencilReference( material.stencilRef );
+				renderContextData.currentStencilRef = material.stencilRef;
 
-		if ( context.stencil === true && material.stencilWrite === true && renderContextData.currentStencilRef !== material.stencilRef ) {
+			}
 
-			passEncoderGPU.setStencilReference( material.stencilRef );
-			renderContextData.currentStencilRef = material.stencilRef;
 
-		}
+		};
 
-		// draw
+		// Define draw function
+		const draw = ( passEncoderGPU, currentSets ) => {
 
-		const draw = () => {
+			setPipelineAndBindings( passEncoderGPU, currentSets );
 
 			if ( object.isBatchedMesh === true ) {
 
@@ -1355,18 +1597,45 @@ class WebGPUBackend extends Backend {
 
 					const vp = subCamera.viewport;
 
-					passEncoderGPU.setViewport(
-						Math.floor( vp.x * pixelRatio ),
-						Math.floor( vp.y * pixelRatio ),
-						Math.floor( vp.width * pixelRatio ),
-						Math.floor( vp.height * pixelRatio ),
-						context.viewportValue.minDepth,
-						context.viewportValue.maxDepth
-					);
 
-					passEncoderGPU.setBindGroup( cameraIndex.index, cameraData.indexesGPU[ i ] );
 
-					draw();
+					let pass = renderContextData.currentPass;
+					let sets = renderContextData.currentSets;
+					if ( renderContextData.bundleEncoders ) {
+
+						const bundleEncoder = renderContextData.bundleEncoders[ i ];
+						const bundleSets = renderContextData.bundleSets[ i ];
+						pass = bundleEncoder;
+						sets = bundleSets;
+
+					}
+
+
+
+					if ( vp ) {
+
+						pass.setViewport(
+							Math.floor( vp.x * pixelRatio ),
+							Math.floor( vp.y * pixelRatio ),
+							Math.floor( vp.width * pixelRatio ),
+							Math.floor( vp.height * pixelRatio ),
+							context.viewportValue.minDepth,
+							context.viewportValue.maxDepth
+						);
+
+					}
+
+
+					// Set camera index binding for this layer
+					if ( cameraIndex && cameraData.indexesGPU ) {
+
+						pass.setBindGroup( cameraIndex.index, cameraData.indexesGPU[ i ] );
+						sets.bindingGroups[ cameraIndex.index ] = cameraIndex.id;
+
+					}
+
+					draw( pass, sets );
+
 
 				}
 
@@ -1374,7 +1643,38 @@ class WebGPUBackend extends Backend {
 
 		} else {
 
-			draw();
+		  // Regular single camera rendering
+		  if ( renderContextData.currentPass ) {
+
+				// Handle occlusion queries
+				if ( renderContextData.occlusionQuerySet !== undefined ) {
+
+					const lastObject = renderContextData.lastOcclusionObject;
+					if ( lastObject !== object ) {
+
+						if ( lastObject !== null && lastObject.occlusionTest === true ) {
+
+							renderContextData.currentPass.endOcclusionQuery();
+							renderContextData.occlusionQueryIndex ++;
+
+						}
+
+						if ( object.occlusionTest === true ) {
+
+							renderContextData.currentPass.beginOcclusionQuery( renderContextData.occlusionQueryIndex );
+							renderContextData.occlusionQueryObjects[ renderContextData.occlusionQueryIndex ] = object;
+
+						}
+
+						renderContextData.lastOcclusionObject = object;
+
+					}
+
+				}
+
+				draw( renderContextData.currentPass, renderContextData.currentSets );
+
+			}
 
 		}
 

+ 8 - 2
src/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -453,7 +453,7 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 			textureData.dimensionsSnippet[ levelSnippet ] = textureDimensionNode;
 
-			if ( texture.isDataArrayTexture || texture.isData3DTexture ) {
+			if ( texture.isDataArrayTexture || texture.isDepthArrayTexture || texture.isData3DTexture ) {
 
 				textureData.arrayLayerCount = new VarNode(
 					new ExpressionNode(
@@ -666,6 +666,12 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 		if ( shaderStage === 'fragment' ) {
 
+			if ( texture.isDepthArrayTexture ) {
+
+				return `textureSampleCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet } )`;
+
+			}
+
 			return `textureSampleCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ compareSnippet } )`;
 
 		} else {
@@ -1655,7 +1661,7 @@ ${ flowData.code }
 
 				} else if ( texture.isDepthTexture === true ) {
 
-					textureType = `texture_depth${multisampled}_2d`;
+					textureType = `texture_depth${ multisampled }_2d${ texture.isDepthArrayTexture === true ? '_array' : '' }`;
 
 				} else if ( texture.isVideoTexture === true ) {
 

+ 3 - 0
src/renderers/webgpu/nodes/WGSLNodeFunction.js

@@ -58,7 +58,10 @@ const wgslTypeLib = {
 	'texture_multisampled_2d': 'cubeTexture',
 
 	'texture_depth_2d': 'depthTexture',
+	'texture_depth_2d_array': 'depthTexture',
 	'texture_depth_multisampled_2d': 'depthTexture',
+	'texture_depth_cube': 'depthTexture',
+	'texture_depth_cube_array': 'depthTexture',
 
 	'texture_3d': 'texture3D',
 

+ 2 - 2
src/renderers/webgpu/utils/WebGPUBindingUtils.js

@@ -189,7 +189,7 @@ class WebGPUBindingUtils {
 
 					texture.viewDimension = GPUTextureViewDimension.Cube;
 
-				} else if ( binding.texture.isDataArrayTexture || binding.texture.isCompressedArrayTexture ) {
+				} else if ( binding.texture.isDataArrayTexture || binding.texture.isDepthArrayTexture || binding.texture.isCompressedArrayTexture ) {
 
 					texture.viewDimension = GPUTextureViewDimension.TwoDArray;
 
@@ -419,7 +419,7 @@ class WebGPUBindingUtils {
 
 							dimensionViewGPU = GPUTextureViewDimension.ThreeD;
 
-						} else if ( binding.texture.isDataArrayTexture || binding.texture.isCompressedArrayTexture ) {
+						} else if ( binding.texture.isDataArrayTexture || binding.texture.isDepthArrayTexture || binding.texture.isCompressedArrayTexture ) {
 
 							dimensionViewGPU = GPUTextureViewDimension.TwoDArray;
 

+ 3 - 2
src/renderers/webgpu/utils/WebGPUPipelineUtils.js

@@ -228,9 +228,10 @@ class WebGPUPipelineUtils {
 	 * Creates GPU render bundle encoder for the given render context.
 	 *
 	 * @param {RenderContext} renderContext - The render context.
+	 * @param {?string} [label='renderBundleEncoder'] - The label.
 	 * @return {GPURenderBundleEncoder} The GPU render bundle encoder.
 	 */
-	createBundleEncoder( renderContext ) {
+	createBundleEncoder( renderContext, label = 'renderBundleEncoder' ) {
 
 		const backend = this.backend;
 		const { utils, device } = backend;
@@ -240,7 +241,7 @@ class WebGPUPipelineUtils {
 		const sampleCount = this._getSampleCount( renderContext );
 
 		const descriptor = {
-			label: 'renderBundleEncoder',
+			label: label,
 			colorFormats: [ colorFormat ],
 			depthStencilFormat,
 			sampleCount

+ 1 - 1
src/renderers/webgpu/utils/WebGPUTextureUtils.js

@@ -458,7 +458,7 @@ class WebGPUTextureUtils {
 
 			this._copyBufferToTexture( options.image, textureData.texture, textureDescriptorGPU, 0, texture.flipY );
 
-		} else if ( texture.isDataArrayTexture || texture.isData3DTexture ) {
+		} else if ( texture.isDataArrayTexture || texture.isDepthArrayTexture || texture.isData3DTexture ) {
 
 			for ( let i = 0; i < options.image.depth; i ++ ) {
 

+ 101 - 0
src/textures/DepthArrayTexture.js

@@ -0,0 +1,101 @@
+import { DepthTexture } from './DepthTexture.js';
+
+/**
+ * Creates an array of depth textures.
+ *
+ * @augments DepthTexture
+ */
+class DepthArrayTexture extends DepthTexture {
+
+	/**
+	 * Constructs a new depth array texture.
+	 *
+	 * @param {number} [width=1] - The width of the texture.
+	 * @param {number} [height=1] - The height of the texture.
+	 * @param {number} [depth=1] - The depth of the texture.
+	 */
+	constructor( width = 1, height = 1, depth = 1 ) {
+
+		super( width, height );
+
+		/**
+		 * This flag can be used for type testing.
+		 *
+		 * @type {boolean}
+		 * @readonly
+		 * @default true
+		 */
+		this.isDepthArrayTexture = true;
+
+		/**
+		 * The image definition of a depth texture.
+		 *
+		 * @type {{width:number,height:number,depth:number}}
+		 */
+		this.image = { width: width, height: height, depth: depth };
+
+		/**
+		 * If set to `true`, the texture is flipped along the vertical axis when
+		 * uploaded to the GPU.
+		 *
+		 * Overwritten and set to `false` by default.
+		 *
+		 * @type {boolean}
+		 * @default false
+		 */
+		this.flipY = false;
+
+		/**
+		 * Whether to generate mipmaps (if possible) for a texture.
+		 *
+		 * Overwritten and set to `false` by default.
+		 *
+		 * @type {boolean}
+		 * @default false
+		 */
+		this.generateMipmaps = false;
+
+		/**
+		 * The depth compare function.
+		 *
+		 * @type {?(NeverCompare|LessCompare|EqualCompare|LessEqualCompare|GreaterCompare|NotEqualCompare|GreaterEqualCompare|AlwaysCompare)}
+		 * @default null
+		 */
+		this.compareFunction = null;
+
+		/**
+		 * A set of all layers which need to be updated in the texture.
+		 *
+		 * @type {Set<number>}
+		 */
+		this.layerUpdates = new Set();
+
+	}
+
+	/**
+	 * Describes that a specific layer of the texture needs to be updated.
+	 * Normally when {@link Texture#needsUpdate} is set to `true`, the
+	 * entire slice is sent to the GPU. Marking specific
+	 * layers will only transmit subsets of all mipmaps associated with a
+	 * specific depth in the array which is often much more performant.
+	 *
+	 * @param {number} layerIndex - The layer index that should be updated.
+	 */
+	addLayerUpdate( layerIndex ) {
+
+		this.layerUpdates.add( layerIndex );
+
+	}
+
+	/**
+	 * Resets the layer updates registry.
+	 */
+	clearLayerUpdates() {
+
+		this.layerUpdates.clear();
+
+	}
+
+}
+
+export { DepthArrayTexture };

+ 1 - 0
test/e2e/puppeteer.js

@@ -145,6 +145,7 @@ const exceptionList = [
 	'webgpu_particles',
 	'webgpu_shadertoy',
 	'webgpu_shadowmap',
+	'webgpu_shadowmap_array',
 	'webgpu_tsl_editor',
 	'webgpu_tsl_transpiler',
 	'webgpu_tsl_interoperability',

粤ICP备19079148号