TileShadowNode.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import {
  2. Vector3,
  3. Object3D,
  4. ShadowBaseNode,
  5. Plane,
  6. Line3,
  7. DepthArrayTexture,
  8. LessCompare,
  9. Vector2,
  10. RedFormat,
  11. ArrayCamera,
  12. VSMShadowMap,
  13. RendererUtils,
  14. Quaternion
  15. } from 'three/webgpu';
  16. import { min, Fn, shadow, NodeUpdateType, getShadowMaterial, getShadowRenderObjectFunction } from 'three/tsl';
  17. const { resetRendererAndSceneState, restoreRendererAndSceneState } = RendererUtils;
  18. let _rendererState;
  19. const _cameraLayers = [];
  20. const _vec3Temp1 = /*@__PURE__*/ new Vector3();
  21. const _vec3Temp2 = /*@__PURE__*/ new Vector3();
  22. const _vec3Temp3 = /*@__PURE__*/ new Vector3();
  23. const _quatTemp1 = /*@__PURE__*/ new Quaternion();
  24. class LwLight extends Object3D {
  25. constructor() {
  26. super();
  27. this.target = new Object3D();
  28. }
  29. }
  30. /**
  31. * A class that extends `ShadowBaseNode` to implement tiled shadow mapping.
  32. * This allows splitting a shadow map into multiple tiles, each with its own light and camera,
  33. * to improve shadow quality and performance for large scenes.
  34. *
  35. * **Note:** This class does not support `VSMShadowMap` at the moment.
  36. *
  37. * @class
  38. * @augments ShadowBaseNode
  39. * @three_import import { TileShadowNode } from 'three/addons/tsl/shadows/TileShadowNode.js';
  40. */
  41. class TileShadowNode extends ShadowBaseNode {
  42. /**
  43. * Creates an instance of `TileShadowNode`.
  44. *
  45. * @param {Light} light - The original light source used for shadow mapping.
  46. * @param {Object} [options={}] - Configuration options for the tiled shadow node.
  47. * @param {number} [options.tilesX=2] - The number of tiles along the X-axis.
  48. * @param {number} [options.tilesY=2] - The number of tiles along the Y-axis.
  49. * @param {Object} [options.resolution] - The resolution of the shadow map.
  50. * @param {boolean} [options.debug=false] - Whether to enable debug mode.
  51. */
  52. constructor( light, options = {} ) {
  53. super( light );
  54. // Default configuration with sensible defaults
  55. this.config = {
  56. tilesX: options.tilesX || 2,
  57. tilesY: options.tilesY || 2,
  58. resolution: options.resolution || light.shadow.mapSize,
  59. debug: options.debug !== undefined ? options.debug : false
  60. };
  61. this.debug = this.config.debug;
  62. this.originalLight = light;
  63. this.lightPlane = new Plane( new Vector3( 0, 1, 0 ), 0 );
  64. this.line = new Line3();
  65. this.initialLightDirection = new Vector3();
  66. this.updateLightDirection();
  67. this._cameraFrameId = new WeakMap();
  68. this.shadowSize = {
  69. top: light.shadow.camera.top,
  70. bottom: light.shadow.camera.bottom,
  71. left: light.shadow.camera.left,
  72. right: light.shadow.camera.right,
  73. };
  74. this.lights = [];
  75. this._shadowNodes = [];
  76. this.tiles = this.generateTiles( this.config.tilesX, this.config.tilesY );
  77. }
  78. /**
  79. * Generates the tiles for the shadow map based on the specified number of tiles along the X and Y axes.
  80. *
  81. * @param {number} tilesX - The number of tiles along the X-axis.
  82. * @param {number} tilesY - The number of tiles along the Y-axis.
  83. * @returns {Array<Object>} An array of tile objects, each containing the tile's bounds and index.
  84. */
  85. generateTiles( tilesX, tilesY ) {
  86. const tiles = [];
  87. const tileWidth = 1 / tilesX;
  88. const tileHeight = 1 / tilesY;
  89. for ( let y = 0; y < tilesY; y ++ ) {
  90. for ( let x = 0; x < tilesX; x ++ ) {
  91. tiles.push( {
  92. x: [ x * tileWidth, ( x + 1 ) * tileWidth ],
  93. y: [ ( tilesY - 1 - y ) * tileHeight, ( tilesY - y ) * tileHeight ], // Start from top row
  94. index: y * tilesX + x
  95. } );
  96. }
  97. }
  98. return tiles;
  99. }
  100. /**
  101. * Updates the initial light direction based on the light's target position.
  102. */
  103. updateLightDirection() {
  104. this.initialLightDirection.subVectors(
  105. this.originalLight.target.getWorldPosition( new Vector3() ),
  106. this.originalLight.getWorldPosition( new Vector3() )
  107. ).normalize();
  108. }
  109. /**
  110. * Initializes the tiled shadow node by creating lights, cameras, and shadow maps for each tile.
  111. *
  112. * @param {Builder} builder - The builder used to create render targets and other resources.
  113. */
  114. init( builder ) {
  115. const light = this.originalLight;
  116. const parent = light.parent;
  117. const width = this.shadowSize.right - this.shadowSize.left;
  118. const height = this.shadowSize.top - this.shadowSize.bottom;
  119. const tileCount = this.tiles.length;
  120. const shadowWidth = this.config.resolution.width;
  121. const shadowHeight = this.config.resolution.height;
  122. // Clear existing lights/nodes if re-initializing
  123. this.disposeLightsAndNodes();
  124. const depthTexture = new DepthArrayTexture( shadowWidth, shadowHeight, tileCount );
  125. depthTexture.compareFunction = LessCompare;
  126. depthTexture.name = 'ShadowDepthArrayTexture';
  127. const shadowMap = builder.createRenderTargetArray( shadowWidth, shadowHeight, tileCount, { format: RedFormat } );
  128. shadowMap.depthTexture = depthTexture;
  129. shadowMap.texture.name = 'ShadowTexture';
  130. this.shadowMap = shadowMap;
  131. const cameras = [];
  132. // Create lights, one for each tile
  133. for ( let i = 0; i < tileCount; i ++ ) {
  134. const lwLight = new LwLight();
  135. lwLight.castShadow = true;
  136. const lShadow = light.shadow.clone();
  137. lShadow.filterNode = light.shadow.filterNode;
  138. const tile = this.tiles[ i ];
  139. lShadow.camera.left = this.shadowSize.left + width * tile.x[ 0 ];
  140. lShadow.camera.right = this.shadowSize.left + width * tile.x[ 1 ];
  141. lShadow.camera.top = this.shadowSize.bottom + height * tile.y[ 1 ];
  142. lShadow.camera.bottom = this.shadowSize.bottom + height * tile.y[ 0 ];
  143. lShadow.bias = light.shadow.bias;
  144. lShadow.camera.near = light.shadow.camera.near;
  145. lShadow.camera.far = light.shadow.camera.far;
  146. lShadow.camera.userData.tileIndex = i;
  147. lwLight.shadow = lShadow;
  148. if ( parent ) {
  149. parent.add( lwLight );
  150. parent.add( lwLight.target );
  151. } else {
  152. console.warn( 'TileShadowNode: Original light has no parent during init. Tile lights not added to scene graph directly.' );
  153. }
  154. this.syncLightTransformation( lwLight, light );
  155. this.lights.push( lwLight );
  156. lShadow.camera.updateMatrixWorld();
  157. cameras.push( lShadow.camera );
  158. const shadowNode = shadow( lwLight, lShadow );
  159. shadowNode.depthLayer = i;
  160. shadowNode.updateBeforeType = NodeUpdateType.NONE;
  161. shadowNode.setupRenderTarget = () => {
  162. return { shadowMap, depthTexture };
  163. };
  164. this._shadowNodes.push( shadowNode );
  165. }
  166. const cameraArray = new ArrayCamera( cameras );
  167. this.cameraArray = cameraArray;
  168. }
  169. /**
  170. * Updates the light transformations and shadow cameras for each tile.
  171. */
  172. update() {
  173. const light = this.originalLight;
  174. const shadowCam = light.shadow.camera;
  175. const lsMin = new Vector2( shadowCam.left, shadowCam.bottom );
  176. const lsMax = new Vector2( shadowCam.right, shadowCam.top );
  177. const fullWidth = lsMax.x - lsMin.x;
  178. const fullHeight = lsMax.y - lsMin.y;
  179. for ( let i = 0; i < this.lights.length; i ++ ) {
  180. const lwLight = this.lights[ i ];
  181. const tile = this.tiles[ i ];
  182. this.syncLightTransformation( lwLight, light );
  183. const lShadow = lwLight.shadow;
  184. const tileLeft = lsMin.x + tile.x[ 0 ] * fullWidth;
  185. const tileRight = lsMin.x + tile.x[ 1 ] * fullWidth;
  186. const tileBottom = lsMin.y + tile.y[ 0 ] * fullHeight;
  187. const tileTop = lsMin.y + tile.y[ 1 ] * fullHeight;
  188. lShadow.camera.left = tileLeft;
  189. lShadow.camera.right = tileRight;
  190. lShadow.camera.bottom = tileBottom;
  191. lShadow.camera.top = tileTop;
  192. lShadow.camera.near = light.shadow.camera.near;
  193. lShadow.camera.far = light.shadow.camera.far;
  194. lShadow.camera.updateProjectionMatrix();
  195. lShadow.camera.updateWorldMatrix( true, false );
  196. lShadow.camera.updateMatrixWorld( true );
  197. this._shadowNodes[ i ].shadow.needsUpdate = true;
  198. }
  199. }
  200. /**
  201. * Updates the shadow map rendering.
  202. * @param {NodeFrame} frame - A reference to the current node frame.
  203. */
  204. updateShadow( frame ) {
  205. const { shadowMap, light } = this;
  206. const { renderer, scene, camera } = frame;
  207. const shadowType = renderer.shadowMap.type;
  208. const depthVersion = shadowMap.depthTexture.version;
  209. this._depthVersionCached = depthVersion;
  210. const currentRenderObjectFunction = renderer.getRenderObjectFunction();
  211. const currentMRT = renderer.getMRT();
  212. const useVelocity = currentMRT ? currentMRT.has( 'velocity' ) : false;
  213. _rendererState = resetRendererAndSceneState( renderer, scene, _rendererState );
  214. scene.overrideMaterial = getShadowMaterial( light );
  215. renderer.setRenderTarget( this.shadowMap );
  216. for ( let index = 0; index < this.lights.length; index ++ ) {
  217. const light = this.lights[ index ];
  218. const shadow = light.shadow;
  219. const _shadowCameraLayer = shadow.camera.layers.mask;
  220. _cameraLayers.push( _shadowCameraLayer );
  221. if ( ( shadow.camera.layers.mask & 0xFFFFFFFE ) === 0 ) {
  222. shadow.camera.layers.mask = camera.layers.mask;
  223. }
  224. shadow.updateMatrices( light );
  225. renderer.setRenderObjectFunction( getShadowRenderObjectFunction( renderer, shadow, shadowType, useVelocity ) );
  226. this.shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height, shadowMap.depth );
  227. }
  228. renderer.render( scene, this.cameraArray );
  229. renderer.setRenderObjectFunction( currentRenderObjectFunction );
  230. if ( light.isPointLight !== true && shadowType === VSMShadowMap ) {
  231. console.warn( 'THREE.TileShadowNode: VSM shadow map is not supported yet.' );
  232. // this.vsmPass( renderer );
  233. }
  234. restoreRendererAndSceneState( renderer, scene, _rendererState );
  235. for ( let index = 0; index < this.lights.length; index ++ ) {
  236. const light = this.lights[ index ];
  237. const shadow = light.shadow;
  238. shadow.camera.layers.mask = _cameraLayers[ index ];
  239. }
  240. _cameraLayers.length = 0;
  241. }
  242. /**
  243. * The implementation performs the update of the shadow map if necessary.
  244. *
  245. * @param {NodeFrame} frame - A reference to the current node frame.
  246. */
  247. updateBefore( frame ) {
  248. const shadow = this.originalLight.shadow;
  249. let needsUpdate = shadow.needsUpdate || shadow.autoUpdate;
  250. if ( needsUpdate ) {
  251. if ( this._cameraFrameId[ frame.camera ] === frame.frameId ) {
  252. needsUpdate = false;
  253. }
  254. this._cameraFrameId[ frame.camera ] = frame.frameId;
  255. }
  256. if ( needsUpdate ) {
  257. this.update();
  258. this.updateShadow( frame );
  259. if ( this.shadowMap.depthTexture.version === this._depthVersionCached ) {
  260. shadow.needsUpdate = false;
  261. }
  262. }
  263. }
  264. /**
  265. * Synchronizes the transformation of a tile light with the source light.
  266. *
  267. * @param {LwLight} lwLight - The tile light to synchronize.
  268. * @param {Light} sourceLight - The source light to copy transformations from.
  269. */
  270. syncLightTransformation( lwLight, sourceLight ) {
  271. const sourceWorldPos = sourceLight.getWorldPosition( _vec3Temp1 );
  272. const targetWorldPos = sourceLight.target.getWorldPosition( _vec3Temp2 );
  273. const forward = _vec3Temp3.subVectors( targetWorldPos, sourceWorldPos );
  274. const targetDistance = forward.length();
  275. forward.normalize();
  276. lwLight.position.copy( sourceWorldPos );
  277. lwLight.target.position.copy( sourceWorldPos ).add( forward.multiplyScalar( targetDistance ) );
  278. lwLight.quaternion.copy( sourceLight.getWorldQuaternion( _quatTemp1 ) );
  279. lwLight.scale.copy( sourceLight.scale );
  280. lwLight.updateMatrix();
  281. lwLight.updateMatrixWorld( true );
  282. lwLight.target.updateMatrix();
  283. lwLight.target.updateMatrixWorld( true );
  284. }
  285. /**
  286. * Sets up the shadow node for rendering.
  287. *
  288. * @param {Builder} builder - The builder used to set up the shadow node.
  289. * @returns {Node} A node representing the shadow value.
  290. */
  291. setup( builder ) {
  292. if ( this.lights.length === 0 ) {
  293. this.init( builder );
  294. }
  295. return Fn( ( builder ) => {
  296. this.setupShadowPosition( builder );
  297. return min( ...this._shadowNodes ).toVar( 'shadowValue' );
  298. } )();
  299. }
  300. /**
  301. * Helper method to remove lights and associated nodes/targets.
  302. * Used internally during dispose and potential re-initialization.
  303. */
  304. disposeLightsAndNodes() {
  305. for ( const light of this.lights ) {
  306. const parent = light.parent;
  307. if ( parent ) {
  308. parent.remove( light.target );
  309. parent.remove( light );
  310. }
  311. }
  312. this.lights = [];
  313. this._shadowNodes = [];
  314. if ( this.shadowMap ) {
  315. this.shadowMap.dispose(); // Disposes render target and textures
  316. this.shadowMap = null;
  317. }
  318. }
  319. dispose() {
  320. // Dispose lights, nodes, and shadow map
  321. this.disposeLightsAndNodes();
  322. super.dispose();
  323. }
  324. }
  325. export { TileShadowNode };