CSMShadowNode.js 13 KB

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