CSMShadowNode.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. import {
  2. Vector2,
  3. Vector3,
  4. MathUtils,
  5. Matrix4,
  6. Box3,
  7. Object3D,
  8. WebGLCoordinateSystem,
  9. ShadowBaseNode
  10. } from 'three/webgpu';
  11. import { CSMFrustum } from './CSMFrustum.js';
  12. import { viewZToOrthographicDepth, reference, uniform, float, vec4, vec2, If, Fn, min, renderGroup, positionView, shadow } from 'three/tsl';
  13. const _cameraToLightMatrix = new Matrix4();
  14. const _lightSpaceFrustum = new CSMFrustum();
  15. const _center = new Vector3();
  16. const _bbox = new Box3();
  17. const _uniformArray = [];
  18. const _logArray = [];
  19. const _lightDirection = new Vector3();
  20. const _lightOrientationMatrix = new Matrix4();
  21. const _lightOrientationMatrixInverse = new Matrix4();
  22. const _up = new Vector3( 0, 1, 0 );
  23. class LwLight extends Object3D {
  24. constructor() {
  25. super();
  26. this.target = new Object3D();
  27. }
  28. }
  29. /**
  30. * An implementation of Cascade Shadow Maps (CSM).
  31. *
  32. * This module can only be used with {@link WebGPURenderer}. When using {@link WebGLRenderer},
  33. * use {@link CSM} instead.
  34. *
  35. * @augments ShadowBaseNode
  36. * @three_import import { CSMShadowNode } from 'three/addons/csm/CSMShadowNode.js';
  37. */
  38. class CSMShadowNode extends ShadowBaseNode {
  39. /**
  40. * Constructs a new CSM shadow node.
  41. *
  42. * @param {DirectionalLight} light - The CSM light.
  43. * @param {CSMShadowNode~Data} [data={}] - The CSM data.
  44. */
  45. constructor( light, data = {} ) {
  46. super( light );
  47. /**
  48. * The scene's camera.
  49. *
  50. * @type {?Camera}
  51. * @default null
  52. */
  53. this.camera = null;
  54. /**
  55. * The number of cascades.
  56. *
  57. * @type {number}
  58. * @default 3
  59. */
  60. this.cascades = data.cascades || 3;
  61. /**
  62. * The maximum far value.
  63. *
  64. * @type {number}
  65. * @default 100000
  66. */
  67. this.maxFar = data.maxFar || 100000;
  68. /**
  69. * The frustum split mode.
  70. *
  71. * @type {('practical'|'uniform'|'logarithmic'|'custom')}
  72. * @default 'practical'
  73. */
  74. this.mode = data.mode || 'practical';
  75. /**
  76. * The light margin.
  77. *
  78. * @type {number}
  79. * @default 200
  80. */
  81. this.lightMargin = data.lightMargin || 200;
  82. /**
  83. * Custom split callback when using `mode='custom'`.
  84. *
  85. * @type {Function}
  86. */
  87. this.customSplitsCallback = data.customSplitsCallback;
  88. /**
  89. * Whether to fade between cascades or not.
  90. *
  91. * @type {boolean}
  92. * @default false
  93. */
  94. this.fade = false;
  95. /**
  96. * An array of numbers in the range `[0,1]` the defines how the
  97. * mainCSM frustum should be split up.
  98. *
  99. * @type {Array<number>}
  100. */
  101. this.breaks = [];
  102. this._cascades = [];
  103. /**
  104. * The main frustum.
  105. *
  106. * @type {?CSMFrustum}
  107. * @default null
  108. */
  109. this.mainFrustum = null;
  110. /**
  111. * An array of frustums representing the cascades.
  112. *
  113. * @type {Array<CSMFrustum>}
  114. */
  115. this.frustums = [];
  116. /**
  117. * An array of directional lights which cast the shadows for
  118. * the different cascades. There is one directional light for each
  119. * cascade.
  120. *
  121. * @type {Array<DirectionalLight>}
  122. */
  123. this.lights = [];
  124. this._shadowNodes = [];
  125. }
  126. /**
  127. * Inits the CSM shadow node.
  128. *
  129. * @private
  130. * @param {NodeBuilder} builder - The node builder.
  131. */
  132. _init( { camera, renderer } ) {
  133. this.camera = camera;
  134. const data = { webGL: renderer.coordinateSystem === WebGLCoordinateSystem };
  135. this.mainFrustum = new CSMFrustum( data );
  136. const light = this.light;
  137. const parent = light.parent;
  138. for ( let i = 0; i < this.cascades; i ++ ) {
  139. const lwLight = new LwLight();
  140. lwLight.castShadow = true;
  141. const lShadow = light.shadow.clone();
  142. lShadow.bias = lShadow.bias * ( i + 1 );
  143. this.lights.push( lwLight );
  144. parent.add( lwLight );
  145. parent.add( lwLight.target );
  146. lwLight.shadow = lShadow;
  147. this._shadowNodes.push( shadow( lwLight, lShadow ) );
  148. this._cascades.push( new Vector2() );
  149. }
  150. this.updateFrustums();
  151. }
  152. /**
  153. * Inits the cascades according to the scene's camera and breaks configuration.
  154. *
  155. * @private
  156. */
  157. _initCascades() {
  158. const camera = this.camera;
  159. camera.updateProjectionMatrix();
  160. this.mainFrustum.setFromProjectionMatrix( camera.projectionMatrix, this.maxFar );
  161. this.mainFrustum.split( this.breaks, this.frustums );
  162. }
  163. /**
  164. * Computes the breaks of this CSM instance based on the scene's camera, number of cascades
  165. * and the selected split mode.
  166. *
  167. * @private
  168. */
  169. _getBreaks() {
  170. const camera = this.camera;
  171. const far = Math.min( camera.far, this.maxFar );
  172. this.breaks.length = 0;
  173. switch ( this.mode ) {
  174. case 'uniform':
  175. uniformSplit( this.cascades, camera.near, far, this.breaks );
  176. break;
  177. case 'logarithmic':
  178. logarithmicSplit( this.cascades, camera.near, far, this.breaks );
  179. break;
  180. case 'practical':
  181. practicalSplit( this.cascades, camera.near, far, 0.5, this.breaks );
  182. break;
  183. case 'custom':
  184. if ( this.customSplitsCallback === undefined ) console.error( 'CSM: Custom split scheme callback not defined.' );
  185. this.customSplitsCallback( this.cascades, camera.near, far, this.breaks );
  186. break;
  187. }
  188. function uniformSplit( amount, near, far, target ) {
  189. for ( let i = 1; i < amount; i ++ ) {
  190. target.push( ( near + ( far - near ) * i / amount ) / far );
  191. }
  192. target.push( 1 );
  193. }
  194. function logarithmicSplit( amount, near, far, target ) {
  195. for ( let i = 1; i < amount; i ++ ) {
  196. target.push( ( near * ( far / near ) ** ( i / amount ) ) / far );
  197. }
  198. target.push( 1 );
  199. }
  200. function practicalSplit( amount, near, far, lambda, target ) {
  201. _uniformArray.length = 0;
  202. _logArray.length = 0;
  203. logarithmicSplit( amount, near, far, _logArray );
  204. uniformSplit( amount, near, far, _uniformArray );
  205. for ( let i = 1; i < amount; i ++ ) {
  206. target.push( MathUtils.lerp( _uniformArray[ i - 1 ], _logArray[ i - 1 ], lambda ) );
  207. }
  208. target.push( 1 );
  209. }
  210. }
  211. /**
  212. * Sets the light breaks.
  213. *
  214. * @private
  215. */
  216. _setLightBreaks() {
  217. for ( let i = 0, l = this.cascades; i < l; i ++ ) {
  218. const amount = this.breaks[ i ];
  219. const prev = this.breaks[ i - 1 ] || 0;
  220. this._cascades[ i ].set( prev, amount );
  221. }
  222. }
  223. /**
  224. * Updates the shadow bounds of this CSM instance.
  225. *
  226. * @private
  227. */
  228. _updateShadowBounds() {
  229. const frustums = this.frustums;
  230. for ( let i = 0; i < frustums.length; i ++ ) {
  231. const shadowCam = this.lights[ i ].shadow.camera;
  232. const frustum = this.frustums[ i ];
  233. // Get the two points that represent that furthest points on the frustum assuming
  234. // that's either the diagonal across the far plane or the diagonal across the whole
  235. // frustum itself.
  236. const nearVerts = frustum.vertices.near;
  237. const farVerts = frustum.vertices.far;
  238. const point1 = farVerts[ 0 ];
  239. let point2;
  240. if ( point1.distanceTo( farVerts[ 2 ] ) > point1.distanceTo( nearVerts[ 2 ] ) ) {
  241. point2 = farVerts[ 2 ];
  242. } else {
  243. point2 = nearVerts[ 2 ];
  244. }
  245. let squaredBBWidth = point1.distanceTo( point2 );
  246. if ( this.fade ) {
  247. // expand the shadow extents by the fade margin if fade is enabled.
  248. const camera = this.camera;
  249. const far = Math.max( camera.far, this.maxFar );
  250. const linearDepth = frustum.vertices.far[ 0 ].z / ( far - camera.near );
  251. const margin = 0.25 * Math.pow( linearDepth, 2.0 ) * ( far - camera.near );
  252. squaredBBWidth += margin;
  253. }
  254. shadowCam.left = - squaredBBWidth / 2;
  255. shadowCam.right = squaredBBWidth / 2;
  256. shadowCam.top = squaredBBWidth / 2;
  257. shadowCam.bottom = - squaredBBWidth / 2;
  258. shadowCam.updateProjectionMatrix();
  259. }
  260. }
  261. /**
  262. * Applications must call this method every time they change camera or CSM settings.
  263. */
  264. updateFrustums() {
  265. this._getBreaks();
  266. this._initCascades();
  267. this._updateShadowBounds();
  268. this._setLightBreaks();
  269. }
  270. /**
  271. * Setups the TSL when using fading.
  272. *
  273. * @private
  274. * @return {ShaderCallNodeInternal}
  275. */
  276. _setupFade() {
  277. const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup );
  278. const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).label( 'cascades' );
  279. const shadowFar = uniform( 'float' ).setGroup( renderGroup ).label( 'shadowFar' )
  280. .onRenderUpdate( () => Math.min( this.maxFar, this.camera.far ) );
  281. const linearDepth = viewZToOrthographicDepth( positionView.z, cameraNear, shadowFar ).toVar( 'linearDepth' );
  282. const lastCascade = this.cascades - 1;
  283. return Fn( ( builder ) => {
  284. this.setupShadowPosition( builder );
  285. const ret = vec4( 1, 1, 1, 1 ).toVar( 'shadowValue' );
  286. const cascade = vec2().toVar( 'cascade' );
  287. const cascadeCenter = float().toVar( 'cascadeCenter' );
  288. const margin = float().toVar( 'margin' );
  289. const csmX = float().toVar( 'csmX' );
  290. const csmY = float().toVar( 'csmY' );
  291. for ( let i = 0; i < this.cascades; i ++ ) {
  292. const isLastCascade = i === lastCascade;
  293. cascade.assign( cascades.element( i ) );
  294. cascadeCenter.assign( cascade.x.add( cascade.y ).div( 2.0 ) );
  295. const closestEdge = linearDepth.lessThan( cascadeCenter ).select( cascade.x, cascade.y );
  296. margin.assign( float( 0.25 ).mul( closestEdge.pow( 2.0 ) ) );
  297. csmX.assign( cascade.x.sub( margin.div( 2.0 ) ) );
  298. if ( isLastCascade ) {
  299. csmY.assign( cascade.y );
  300. } else {
  301. csmY.assign( cascade.y.add( margin.div( 2.0 ) ) );
  302. }
  303. const inRange = linearDepth.greaterThanEqual( csmX ).and( linearDepth.lessThanEqual( csmY ) );
  304. If( inRange, () => {
  305. const dist = min( linearDepth.sub( csmX ), csmY.sub( linearDepth ) ).toVar();
  306. let ratio = dist.div( margin ).clamp( 0.0, 1.0 );
  307. if ( i === 0 ) {
  308. // don't fade at nearest edge
  309. ratio = linearDepth.greaterThan( cascadeCenter ).select( ratio, 1 );
  310. }
  311. ret.subAssign( this._shadowNodes[ i ].oneMinus().mul( ratio ) );
  312. } );
  313. }
  314. return ret;
  315. } )();
  316. }
  317. /**
  318. * Setups the TSL when no fading (default).
  319. *
  320. * @private
  321. * @return {ShaderCallNodeInternal}
  322. */
  323. _setupStandard() {
  324. const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup );
  325. const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).label( 'cascades' );
  326. const shadowFar = uniform( 'float' ).setGroup( renderGroup ).label( 'shadowFar' )
  327. .onRenderUpdate( () => Math.min( this.maxFar, this.camera.far ) );
  328. const linearDepth = viewZToOrthographicDepth( positionView.z, cameraNear, shadowFar ).toVar( 'linearDepth' );
  329. return Fn( ( builder ) => {
  330. this.setupShadowPosition( builder );
  331. const ret = vec4( 1, 1, 1, 1 ).toVar( 'shadowValue' );
  332. const cascade = vec2().toVar( 'cascade' );
  333. for ( let i = 0; i < this.cascades; i ++ ) {
  334. cascade.assign( cascades.element( i ) );
  335. If( linearDepth.greaterThanEqual( cascade.x ).and( linearDepth.lessThanEqual( cascade.y ) ), () => {
  336. ret.assign( this._shadowNodes[ i ] );
  337. } );
  338. }
  339. return ret;
  340. } )();
  341. }
  342. setup( builder ) {
  343. if ( this.camera === null ) this._init( builder );
  344. return this.fade === true ? this._setupFade() : this._setupStandard();
  345. }
  346. updateBefore( /*builder*/ ) {
  347. const light = this.light;
  348. const camera = this.camera;
  349. const frustums = this.frustums;
  350. _lightDirection.subVectors( light.target.position, light.position ).normalize();
  351. // for each frustum we need to find its min-max box aligned with the light orientation
  352. // the position in _lightOrientationMatrix does not matter, as we transform there and back
  353. _lightOrientationMatrix.lookAt( light.position, light.target.position, _up );
  354. _lightOrientationMatrixInverse.copy( _lightOrientationMatrix ).invert();
  355. for ( let i = 0; i < frustums.length; i ++ ) {
  356. const lwLight = this.lights[ i ];
  357. const shadow = lwLight.shadow;
  358. const shadowCam = shadow.camera;
  359. const texelWidth = ( shadowCam.right - shadowCam.left ) / shadow.mapSize.width;
  360. const texelHeight = ( shadowCam.top - shadowCam.bottom ) / shadow.mapSize.height;
  361. _cameraToLightMatrix.multiplyMatrices( _lightOrientationMatrixInverse, camera.matrixWorld );
  362. frustums[ i ].toSpace( _cameraToLightMatrix, _lightSpaceFrustum );
  363. const nearVerts = _lightSpaceFrustum.vertices.near;
  364. const farVerts = _lightSpaceFrustum.vertices.far;
  365. _bbox.makeEmpty();
  366. for ( let j = 0; j < 4; j ++ ) {
  367. _bbox.expandByPoint( nearVerts[ j ] );
  368. _bbox.expandByPoint( farVerts[ j ] );
  369. }
  370. _bbox.getCenter( _center );
  371. _center.z = _bbox.max.z + this.lightMargin;
  372. _center.x = Math.floor( _center.x / texelWidth ) * texelWidth;
  373. _center.y = Math.floor( _center.y / texelHeight ) * texelHeight;
  374. _center.applyMatrix4( _lightOrientationMatrix );
  375. lwLight.position.copy( _center );
  376. lwLight.target.position.copy( _center );
  377. lwLight.target.position.add( _lightDirection );
  378. }
  379. }
  380. /**
  381. * Frees the GPU-related resources allocated by this instance. Call this
  382. * method whenever this instance is no longer used in your app.
  383. */
  384. dispose() {
  385. for ( let i = 0; i < this.lights.length; i ++ ) {
  386. const light = this.lights[ i ];
  387. const parent = light.parent;
  388. parent.remove( light.target );
  389. parent.remove( light );
  390. }
  391. super.dispose();
  392. }
  393. }
  394. /**
  395. * Constructor data of `CSMShadowNode`.
  396. *
  397. * @typedef {Object} CSMShadowNode~Data
  398. * @property {number} [cascades=3] - The number of cascades.
  399. * @property {number} [maxFar=100000] - The maximum far value.
  400. * @property {('practical'|'uniform'|'logarithmic'|'custom')} [mode='practical'] - The frustum split mode.
  401. * @property {Function} [customSplitsCallback] - Custom split callback when using `mode='custom'`.
  402. * @property {number} [lightMargin=200] - The light margin.
  403. **/
  404. export { CSMShadowNode };