CSMShadowNode.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  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. for ( let i = 0; i < this.cascades; i ++ ) {
  138. const lwLight = new LwLight();
  139. lwLight.castShadow = true;
  140. const lShadow = light.shadow.clone();
  141. lShadow.bias = lShadow.bias * ( i + 1 );
  142. this.lights.push( lwLight );
  143. lwLight.shadow = lShadow;
  144. this._shadowNodes.push( shadow( lwLight, lShadow ) );
  145. this._cascades.push( new Vector2() );
  146. }
  147. this.updateFrustums();
  148. }
  149. /**
  150. * Inits the cascades according to the scene's camera and breaks configuration.
  151. *
  152. * @private
  153. */
  154. _initCascades() {
  155. const camera = this.camera;
  156. camera.updateProjectionMatrix();
  157. this.mainFrustum.setFromProjectionMatrix( camera.projectionMatrix, this.maxFar );
  158. this.mainFrustum.split( this.breaks, this.frustums );
  159. }
  160. /**
  161. * Computes the breaks of this CSM instance based on the scene's camera, number of cascades
  162. * and the selected split mode.
  163. *
  164. * @private
  165. */
  166. _getBreaks() {
  167. const camera = this.camera;
  168. const far = Math.min( camera.far, this.maxFar );
  169. this.breaks.length = 0;
  170. switch ( this.mode ) {
  171. case 'uniform':
  172. uniformSplit( this.cascades, camera.near, far, this.breaks );
  173. break;
  174. case 'logarithmic':
  175. logarithmicSplit( this.cascades, camera.near, far, this.breaks );
  176. break;
  177. case 'practical':
  178. practicalSplit( this.cascades, camera.near, far, 0.5, this.breaks );
  179. break;
  180. case 'custom':
  181. if ( this.customSplitsCallback === undefined ) console.error( 'CSM: Custom split scheme callback not defined.' );
  182. this.customSplitsCallback( this.cascades, camera.near, far, this.breaks );
  183. break;
  184. }
  185. function uniformSplit( amount, near, far, target ) {
  186. for ( let i = 1; i < amount; i ++ ) {
  187. target.push( ( near + ( far - near ) * i / amount ) / far );
  188. }
  189. target.push( 1 );
  190. }
  191. function logarithmicSplit( amount, near, far, target ) {
  192. for ( let i = 1; i < amount; i ++ ) {
  193. target.push( ( near * ( far / near ) ** ( i / amount ) ) / far );
  194. }
  195. target.push( 1 );
  196. }
  197. function practicalSplit( amount, near, far, lambda, target ) {
  198. _uniformArray.length = 0;
  199. _logArray.length = 0;
  200. logarithmicSplit( amount, near, far, _logArray );
  201. uniformSplit( amount, near, far, _uniformArray );
  202. for ( let i = 1; i < amount; i ++ ) {
  203. target.push( MathUtils.lerp( _uniformArray[ i - 1 ], _logArray[ i - 1 ], lambda ) );
  204. }
  205. target.push( 1 );
  206. }
  207. }
  208. /**
  209. * Sets the light breaks.
  210. *
  211. * @private
  212. */
  213. _setLightBreaks() {
  214. for ( let i = 0, l = this.cascades; i < l; i ++ ) {
  215. const amount = this.breaks[ i ];
  216. const prev = this.breaks[ i - 1 ] || 0;
  217. this._cascades[ i ].set( prev, amount );
  218. }
  219. }
  220. /**
  221. * Updates the shadow bounds of this CSM instance.
  222. *
  223. * @private
  224. */
  225. _updateShadowBounds() {
  226. const frustums = this.frustums;
  227. for ( let i = 0; i < frustums.length; i ++ ) {
  228. const shadowCam = this.lights[ i ].shadow.camera;
  229. const frustum = this.frustums[ i ];
  230. // Get the two points that represent that furthest points on the frustum assuming
  231. // that's either the diagonal across the far plane or the diagonal across the whole
  232. // frustum itself.
  233. const nearVerts = frustum.vertices.near;
  234. const farVerts = frustum.vertices.far;
  235. const point1 = farVerts[ 0 ];
  236. let point2;
  237. if ( point1.distanceTo( farVerts[ 2 ] ) > point1.distanceTo( nearVerts[ 2 ] ) ) {
  238. point2 = farVerts[ 2 ];
  239. } else {
  240. point2 = nearVerts[ 2 ];
  241. }
  242. let squaredBBWidth = point1.distanceTo( point2 );
  243. if ( this.fade ) {
  244. // expand the shadow extents by the fade margin if fade is enabled.
  245. const camera = this.camera;
  246. const far = Math.max( camera.far, this.maxFar );
  247. const linearDepth = frustum.vertices.far[ 0 ].z / ( far - camera.near );
  248. const margin = 0.25 * Math.pow( linearDepth, 2.0 ) * ( far - camera.near );
  249. squaredBBWidth += margin;
  250. }
  251. shadowCam.left = - squaredBBWidth / 2;
  252. shadowCam.right = squaredBBWidth / 2;
  253. shadowCam.top = squaredBBWidth / 2;
  254. shadowCam.bottom = - squaredBBWidth / 2;
  255. shadowCam.updateProjectionMatrix();
  256. }
  257. }
  258. /**
  259. * Applications must call this method every time they change camera or CSM settings.
  260. */
  261. updateFrustums() {
  262. this._getBreaks();
  263. this._initCascades();
  264. this._updateShadowBounds();
  265. this._setLightBreaks();
  266. }
  267. /**
  268. * Setups the TSL when using fading.
  269. *
  270. * @private
  271. * @return {ShaderCallNodeInternal}
  272. */
  273. _setupFade() {
  274. const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup );
  275. const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).label( 'cascades' );
  276. const shadowFar = uniform( 'float' ).setGroup( renderGroup ).label( 'shadowFar' )
  277. .onRenderUpdate( () => Math.min( this.maxFar, this.camera.far ) );
  278. const linearDepth = viewZToOrthographicDepth( positionView.z, cameraNear, shadowFar ).toVar( 'linearDepth' );
  279. const lastCascade = this.cascades - 1;
  280. return Fn( ( builder ) => {
  281. this.setupShadowPosition( builder );
  282. const ret = vec4( 1, 1, 1, 1 ).toVar( 'shadowValue' );
  283. const cascade = vec2().toVar( 'cascade' );
  284. const cascadeCenter = float().toVar( 'cascadeCenter' );
  285. const margin = float().toVar( 'margin' );
  286. const csmX = float().toVar( 'csmX' );
  287. const csmY = float().toVar( 'csmY' );
  288. for ( let i = 0; i < this.cascades; i ++ ) {
  289. const isLastCascade = i === lastCascade;
  290. cascade.assign( cascades.element( i ) );
  291. cascadeCenter.assign( cascade.x.add( cascade.y ).div( 2.0 ) );
  292. const closestEdge = linearDepth.lessThan( cascadeCenter ).select( cascade.x, cascade.y );
  293. margin.assign( float( 0.25 ).mul( closestEdge.pow( 2.0 ) ) );
  294. csmX.assign( cascade.x.sub( margin.div( 2.0 ) ) );
  295. if ( isLastCascade ) {
  296. csmY.assign( cascade.y );
  297. } else {
  298. csmY.assign( cascade.y.add( margin.div( 2.0 ) ) );
  299. }
  300. const inRange = linearDepth.greaterThanEqual( csmX ).and( linearDepth.lessThanEqual( csmY ) );
  301. If( inRange, () => {
  302. const dist = min( linearDepth.sub( csmX ), csmY.sub( linearDepth ) ).toVar();
  303. let ratio = dist.div( margin ).clamp( 0.0, 1.0 );
  304. if ( i === 0 ) {
  305. // don't fade at nearest edge
  306. ratio = linearDepth.greaterThan( cascadeCenter ).select( ratio, 1 );
  307. }
  308. ret.subAssign( this._shadowNodes[ i ].oneMinus().mul( ratio ) );
  309. } );
  310. }
  311. return ret;
  312. } )();
  313. }
  314. /**
  315. * Setups the TSL when no fading (default).
  316. *
  317. * @private
  318. * @return {ShaderCallNodeInternal}
  319. */
  320. _setupStandard() {
  321. const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup );
  322. const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).label( 'cascades' );
  323. const shadowFar = uniform( 'float' ).setGroup( renderGroup ).label( 'shadowFar' )
  324. .onRenderUpdate( () => Math.min( this.maxFar, this.camera.far ) );
  325. const linearDepth = viewZToOrthographicDepth( positionView.z, cameraNear, shadowFar ).toVar( 'linearDepth' );
  326. return Fn( ( builder ) => {
  327. this.setupShadowPosition( builder );
  328. const ret = vec4( 1, 1, 1, 1 ).toVar( 'shadowValue' );
  329. const cascade = vec2().toVar( 'cascade' );
  330. for ( let i = 0; i < this.cascades; i ++ ) {
  331. cascade.assign( cascades.element( i ) );
  332. If( linearDepth.greaterThanEqual( cascade.x ).and( linearDepth.lessThanEqual( cascade.y ) ), () => {
  333. ret.assign( this._shadowNodes[ i ] );
  334. } );
  335. }
  336. return ret;
  337. } )();
  338. }
  339. setup( builder ) {
  340. if ( this.camera === null ) this._init( builder );
  341. return this.fade === true ? this._setupFade() : this._setupStandard();
  342. }
  343. updateBefore( /*builder*/ ) {
  344. const light = this.light;
  345. const parent = light.parent;
  346. const camera = this.camera;
  347. const frustums = this.frustums;
  348. // make sure the placeholder light objects which represent the
  349. // multiple cascade shadow casters are part of the scene graph
  350. for ( let i = 0; i < this.lights.length; i ++ ) {
  351. const lwLight = this.lights[ i ];
  352. if ( lwLight.parent === null ) {
  353. parent.add( lwLight.target );
  354. parent.add( lwLight );
  355. }
  356. }
  357. _lightDirection.subVectors( light.target.position, light.position ).normalize();
  358. // for each frustum we need to find its min-max box aligned with the light orientation
  359. // the position in _lightOrientationMatrix does not matter, as we transform there and back
  360. _lightOrientationMatrix.lookAt( light.position, light.target.position, _up );
  361. _lightOrientationMatrixInverse.copy( _lightOrientationMatrix ).invert();
  362. for ( let i = 0; i < frustums.length; i ++ ) {
  363. const lwLight = this.lights[ i ];
  364. const shadow = lwLight.shadow;
  365. const shadowCam = shadow.camera;
  366. const texelWidth = ( shadowCam.right - shadowCam.left ) / shadow.mapSize.width;
  367. const texelHeight = ( shadowCam.top - shadowCam.bottom ) / shadow.mapSize.height;
  368. _cameraToLightMatrix.multiplyMatrices( _lightOrientationMatrixInverse, camera.matrixWorld );
  369. frustums[ i ].toSpace( _cameraToLightMatrix, _lightSpaceFrustum );
  370. const nearVerts = _lightSpaceFrustum.vertices.near;
  371. const farVerts = _lightSpaceFrustum.vertices.far;
  372. _bbox.makeEmpty();
  373. for ( let j = 0; j < 4; j ++ ) {
  374. _bbox.expandByPoint( nearVerts[ j ] );
  375. _bbox.expandByPoint( farVerts[ j ] );
  376. }
  377. _bbox.getCenter( _center );
  378. _center.z = _bbox.max.z + this.lightMargin;
  379. _center.x = Math.floor( _center.x / texelWidth ) * texelWidth;
  380. _center.y = Math.floor( _center.y / texelHeight ) * texelHeight;
  381. _center.applyMatrix4( _lightOrientationMatrix );
  382. lwLight.position.copy( _center );
  383. lwLight.target.position.copy( _center );
  384. lwLight.target.position.add( _lightDirection );
  385. }
  386. }
  387. /**
  388. * Frees the GPU-related resources allocated by this instance. Call this
  389. * method whenever this instance is no longer used in your app.
  390. */
  391. dispose() {
  392. for ( let i = 0; i < this.lights.length; i ++ ) {
  393. const light = this.lights[ i ];
  394. const parent = light.parent;
  395. parent.remove( light.target );
  396. parent.remove( light );
  397. }
  398. super.dispose();
  399. }
  400. }
  401. /**
  402. * Constructor data of `CSMShadowNode`.
  403. *
  404. * @typedef {Object} CSMShadowNode~Data
  405. * @property {number} [cascades=3] - The number of cascades.
  406. * @property {number} [maxFar=100000] - The maximum far value.
  407. * @property {('practical'|'uniform'|'logarithmic'|'custom')} [mode='practical'] - The frustum split mode.
  408. * @property {Function} [customSplitsCallback] - Custom split callback when using `mode='custom'`.
  409. * @property {number} [lightMargin=200] - The light margin.
  410. **/
  411. export { CSMShadowNode };
粤ICP备19079148号