LightProbeGrid.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. import {
  2. Box3,
  3. CubeCamera,
  4. CubeRenderTarget,
  5. FloatType,
  6. HalfFloatType,
  7. Light,
  8. LinearFilter,
  9. NearestFilter,
  10. NodeMaterial,
  11. QuadMesh,
  12. RenderTarget,
  13. RenderTarget3D,
  14. RGBAFormat,
  15. Vector3
  16. } from 'three/webgpu';
  17. import {
  18. array,
  19. cubeTexture,
  20. float,
  21. Fn,
  22. int,
  23. ivec2,
  24. Loop,
  25. screenCoordinate,
  26. texture,
  27. uniform,
  28. vec3,
  29. vec4
  30. } from 'three/tsl';
  31. import { LightProbeGridNode, ATLAS_PADDING } from '../tsl/lighting/LightProbeGridNode.js';
  32. // Shared fullscreen-quad for the bake passes.
  33. const _quad = /*@__PURE__*/ new QuadMesh();
  34. // Reusable temp objects.
  35. const _position = /*@__PURE__*/ new Vector3();
  36. const _size = /*@__PURE__*/ new Vector3();
  37. // Bake materials, shared across grids so the shaders compile once, not per bake.
  38. let _shMaterial = null;
  39. let _shSampleCount = - 1;
  40. let _cubeNode = null;
  41. let _batchNode = null;
  42. let _resolutionUniform = null;
  43. let _sliceZUniform = null;
  44. let _repackMaterials = null;
  45. // Bake render targets, pooled by size so rebakes don't churn allocations.
  46. let _cubeRenderTarget = null;
  47. let _cubeCamera = null;
  48. let _cubeKey = '';
  49. let _batchTarget = null;
  50. let _batchProbes = - 1;
  51. // Golden-angle increment for the equal-area Fibonacci sphere.
  52. const GOLDEN_ANGLE = Math.PI * ( 3.0 - Math.sqrt( 5.0 ) );
  53. /**
  54. * Returns the output node for the spherical-harmonic projection pass. Each
  55. * fragment of the 9-wide batch row computes a single SH coefficient by
  56. * integrating the captured cubemap over an equal-area Fibonacci sphere,
  57. * selecting the basis function for its column. Sampling the cubemap by world
  58. * direction keeps the projection independent of the cube face layout.
  59. *
  60. * @private
  61. * @param {Node} cube - The captured environment cubemap texture node.
  62. * @param {number} sampleCount - Number of directions to integrate.
  63. * @return {Node<vec4>} The projected coefficient.
  64. */
  65. function projectSHNode( cube, sampleCount ) {
  66. return Fn( () => {
  67. const coefIndex = int( screenCoordinate.x ).toVar();
  68. const accum = vec3( 0.0 ).toVar();
  69. Loop( sampleCount, ( { i } ) => {
  70. const fi = float( i );
  71. // Equal-area Fibonacci sphere direction.
  72. const z = float( 1.0 ).sub( fi.mul( 2.0 ).add( 1.0 ).div( sampleCount ) );
  73. const r = z.mul( z ).oneMinus().max( 0.0 ).sqrt();
  74. const phi = fi.mul( GOLDEN_ANGLE );
  75. const dir = vec3( r.mul( phi.cos() ), z, r.mul( phi.sin() ) ).toVar();
  76. const radiance = cube.sample( dir ).level( 0 ).rgb;
  77. // The L2 SH basis function for this fragment's coefficient.
  78. const x = dir.x, y = dir.y, zc = dir.z;
  79. const basis = array( [
  80. float( 0.282095 ),
  81. y.mul( 0.488603 ),
  82. zc.mul( 0.488603 ),
  83. x.mul( 0.488603 ),
  84. x.mul( y ).mul( 1.092548 ),
  85. y.mul( zc ).mul( 1.092548 ),
  86. zc.mul( zc ).mul( 3.0 ).sub( 1.0 ).mul( 0.315392 ),
  87. x.mul( zc ).mul( 1.092548 ),
  88. x.mul( x ).sub( y.mul( y ) ).mul( 0.546274 )
  89. ] ).element( coefIndex );
  90. accum.addAssign( radiance.mul( basis ) );
  91. } );
  92. // Equal-area quadrature: each direction covers 4*PI / sampleCount.
  93. const norm = float( 4.0 * Math.PI / sampleCount );
  94. return vec4( accum.mul( norm ), 1.0 );
  95. } )();
  96. }
  97. /**
  98. * Returns the repack output node for one of the seven SH textures. It reads the
  99. * 9 projected coefficients from the batch texture for the probe at the current
  100. * texel and packs the four floats stored by this texture index.
  101. *
  102. * @private
  103. * @param {Node} batch - The batch texture node holding projected coefficients.
  104. * @param {number} textureIndex - The output texture index (0–6).
  105. * @param {Node<vec3>} resolution - The probe grid resolution uniform.
  106. * @param {Node<int>} sliceZ - The current Z slice being written.
  107. * @return {Node<vec4>} The packed texel.
  108. */
  109. function repackNode( batch, textureIndex, resolution, sliceZ ) {
  110. return Fn( () => {
  111. const ix = int( screenCoordinate.x );
  112. const iy = int( screenCoordinate.y );
  113. const nx = int( resolution.x );
  114. const ny = int( resolution.y );
  115. const probeIndex = ix.add( iy.mul( nx ) ).add( sliceZ.mul( nx ).mul( ny ) );
  116. const c0 = batch.load( ivec2( 0, probeIndex ) );
  117. const c1 = batch.load( ivec2( 1, probeIndex ) );
  118. const c2 = batch.load( ivec2( 2, probeIndex ) );
  119. const c3 = batch.load( ivec2( 3, probeIndex ) );
  120. const c4 = batch.load( ivec2( 4, probeIndex ) );
  121. const c5 = batch.load( ivec2( 5, probeIndex ) );
  122. const c6 = batch.load( ivec2( 6, probeIndex ) );
  123. const c7 = batch.load( ivec2( 7, probeIndex ) );
  124. const c8 = batch.load( ivec2( 8, probeIndex ) );
  125. let packed;
  126. switch ( textureIndex ) {
  127. case 0: packed = vec4( c0.xyz, c1.x ); break;
  128. case 1: packed = vec4( c1.yz, c2.xy ); break;
  129. case 2: packed = vec4( c2.z, c3.xyz ); break;
  130. case 3: packed = vec4( c4.xyz, c5.x ); break;
  131. case 4: packed = vec4( c5.yz, c6.xy ); break;
  132. case 5: packed = vec4( c6.z, c7.xyz ); break;
  133. default: packed = vec4( c8.xyz, 0.0 ); break;
  134. }
  135. return packed;
  136. } )();
  137. }
  138. /**
  139. * Lazily pools the shared cube and batch render targets, recreating them only
  140. * when their dimensions change.
  141. *
  142. * @private
  143. * @param {number} cubemapSize - Resolution of each cubemap face.
  144. * @param {number} near - Cube camera near plane.
  145. * @param {number} far - Cube camera far plane.
  146. * @param {number} totalProbes - Number of probes (batch target height).
  147. */
  148. function ensureBakeTargets( cubemapSize, near, far, totalProbes ) {
  149. const cubeKey = `${ cubemapSize },${ near },${ far }`;
  150. if ( _cubeRenderTarget === null || _cubeKey !== cubeKey ) {
  151. if ( _cubeRenderTarget !== null ) _cubeRenderTarget.dispose();
  152. _cubeRenderTarget = new CubeRenderTarget( cubemapSize, { type: HalfFloatType, generateMipmaps: false } );
  153. _cubeCamera = new CubeCamera( near, far, _cubeRenderTarget );
  154. _cubeKey = cubeKey;
  155. }
  156. if ( _batchTarget === null || _batchProbes !== totalProbes ) {
  157. if ( _batchTarget !== null ) _batchTarget.dispose();
  158. _batchTarget = new RenderTarget( 9, totalProbes, {
  159. type: FloatType,
  160. format: RGBAFormat,
  161. minFilter: NearestFilter,
  162. magFilter: NearestFilter,
  163. depthBuffer: false
  164. } );
  165. _batchProbes = totalProbes;
  166. }
  167. }
  168. /**
  169. * Lazily builds the shared bake materials and rebinds them to the current
  170. * cube/batch textures. The SH projection material is rebuilt only when the
  171. * sample count changes; the repack materials are static.
  172. *
  173. * @private
  174. * @param {number} sampleCount - Number of directions integrated by the projection.
  175. * @param {CubeTexture} cubeMap - The current cube render target texture.
  176. * @param {Texture} batchMap - The current batch render target texture.
  177. */
  178. function ensureBakeMaterials( sampleCount, cubeMap, batchMap ) {
  179. if ( _repackMaterials === null ) {
  180. _cubeNode = cubeTexture( cubeMap );
  181. _batchNode = texture( batchMap );
  182. _resolutionUniform = uniform( new Vector3() );
  183. _sliceZUniform = uniform( 0, 'int' );
  184. _repackMaterials = [];
  185. for ( let t = 0; t < 7; t ++ ) {
  186. const material = new NodeMaterial();
  187. material.outputNode = repackNode( _batchNode, t, _resolutionUniform, _sliceZUniform );
  188. material.depthTest = false;
  189. material.depthWrite = false;
  190. _repackMaterials.push( material );
  191. }
  192. } else {
  193. _cubeNode.value = cubeMap;
  194. _batchNode.value = batchMap;
  195. }
  196. if ( _shMaterial === null || _shSampleCount !== sampleCount ) {
  197. if ( _shMaterial !== null ) _shMaterial.dispose();
  198. _shMaterial = new NodeMaterial();
  199. _shMaterial.outputNode = projectSHNode( _cubeNode, sampleCount );
  200. _shMaterial.depthTest = false;
  201. _shMaterial.depthWrite = false;
  202. _shSampleCount = sampleCount;
  203. }
  204. }
  205. /**
  206. * A 3D grid of L2 Spherical Harmonic irradiance probes that provides
  207. * position-dependent diffuse global illumination.
  208. *
  209. * This is the {@link WebGPURenderer} version of `LightProbeGrid`. The grid is a
  210. * {@link Light}, so adding it to the scene applies its baked irradiance to every
  211. * lit node material automatically. When using {@link WebGLRenderer}, import the
  212. * grid from `LightProbeGridWebGL.js` instead.
  213. *
  214. * The baked data is stored in a single RGBA `RenderTarget3D` atlas that packs
  215. * the nine L2 SH coefficients into seven sub-volumes stacked along Z. Baking is
  216. * fully GPU-resident: cubemap rendering, SH projection, and texture packing all
  217. * happen on the GPU with zero CPU readback.
  218. *
  219. * @augments Light
  220. * @three_import import { LightProbeGrid } from 'three/addons/lighting/LightProbeGrid.js';
  221. */
  222. class LightProbeGrid extends Light {
  223. /**
  224. * Constructs a new irradiance probe grid.
  225. *
  226. * The volume is centered at the object's position.
  227. *
  228. * @param {number} [width=1] - Full width of the volume along X.
  229. * @param {number} [height=1] - Full height of the volume along Y.
  230. * @param {number} [depth=1] - Full depth of the volume along Z.
  231. * @param {number} [widthProbes] - Number of probes along X. Defaults to `Math.max( 2, Math.round( width ) + 1 )`.
  232. * @param {number} [heightProbes] - Number of probes along Y. Defaults to `Math.max( 2, Math.round( height ) + 1 )`.
  233. * @param {number} [depthProbes] - Number of probes along Z. Defaults to `Math.max( 2, Math.round( depth ) + 1 )`.
  234. */
  235. constructor( width = 1, height = 1, depth = 1, widthProbes, heightProbes, depthProbes ) {
  236. super( 0xffffff, 1 );
  237. /**
  238. * This flag can be used for type testing.
  239. *
  240. * @type {boolean}
  241. * @readonly
  242. * @default true
  243. */
  244. this.isLightProbeGrid = true;
  245. this.type = 'LightProbeGrid';
  246. /**
  247. * The full width of the volume along X.
  248. *
  249. * @type {number}
  250. */
  251. this.width = width;
  252. /**
  253. * The full height of the volume along Y.
  254. *
  255. * @type {number}
  256. */
  257. this.height = height;
  258. /**
  259. * The full depth of the volume along Z.
  260. *
  261. * @type {number}
  262. */
  263. this.depth = depth;
  264. /**
  265. * The number of probes along each axis.
  266. *
  267. * @type {Vector3}
  268. */
  269. this.resolution = new Vector3(
  270. widthProbes !== undefined ? widthProbes : Math.max( 2, Math.round( width ) + 1 ),
  271. heightProbes !== undefined ? heightProbes : Math.max( 2, Math.round( height ) + 1 ),
  272. depthProbes !== undefined ? depthProbes : Math.max( 2, Math.round( depth ) + 1 )
  273. );
  274. /**
  275. * The world-space bounding box for the grid. Updated automatically
  276. * by {@link LightProbeGrid#bake}.
  277. *
  278. * @type {Box3}
  279. */
  280. this.boundingBox = new Box3();
  281. /**
  282. * Distance in world units over which the grid contribution fades out
  283. * past the volume boundary. `0` applies the contribution everywhere
  284. * (clamped), which matches a single-volume setup. Use a small positive
  285. * value to blend multiple overlapping grids.
  286. *
  287. * @type {number}
  288. * @default 0
  289. */
  290. this.falloff = 0;
  291. /**
  292. * The single RGBA atlas 3D texture storing all seven packed SH
  293. * sub-volumes stacked along Z.
  294. *
  295. * @type {?Data3DTexture}
  296. * @default null
  297. */
  298. this.texture = null;
  299. /**
  300. * Internal render target for GPU-resident baking.
  301. *
  302. * @private
  303. * @type {?RenderTarget3D}
  304. * @default null
  305. */
  306. this._renderTarget = null;
  307. this.updateBoundingBox();
  308. }
  309. /**
  310. * Returns the world-space position of the probe at grid indices (ix, iy, iz).
  311. *
  312. * @param {number} ix - X index.
  313. * @param {number} iy - Y index.
  314. * @param {number} iz - Z index.
  315. * @param {Vector3} target - The target vector.
  316. * @return {Vector3} The world-space position.
  317. */
  318. getProbePosition( ix, iy, iz, target ) {
  319. const pos = this.position;
  320. const res = this.resolution;
  321. const w = this.width, h = this.height, d = this.depth;
  322. target.set(
  323. res.x > 1 ? pos.x - w / 2 + ix * w / ( res.x - 1 ) : pos.x,
  324. res.y > 1 ? pos.y - h / 2 + iy * h / ( res.y - 1 ) : pos.y,
  325. res.z > 1 ? pos.z - d / 2 + iz * d / ( res.z - 1 ) : pos.z
  326. );
  327. return target;
  328. }
  329. /**
  330. * Updates the world-space bounding box from the current position and size.
  331. */
  332. updateBoundingBox() {
  333. _size.set( this.width, this.height, this.depth );
  334. this.boundingBox.setFromCenterAndSize( this.position, _size );
  335. }
  336. /**
  337. * Bakes all probes by rendering cubemaps at each probe position and
  338. * projecting to L2 SH. Optionally iterates additional passes to capture
  339. * indirect bounces: each extra pass samples the previous pass's data as
  340. * indirect light, so a grid added to the scene before baking accumulates
  341. * one bounce per extra pass.
  342. *
  343. * @param {WebGPURenderer} renderer - The renderer.
  344. * @param {Scene} scene - The scene to render.
  345. * @param {Object} [options] - Bake options.
  346. * @param {number} [options.cubemapSize=8] - Resolution of each cubemap face.
  347. * @param {number} [options.near=0.1] - Near plane for the cube camera.
  348. * @param {number} [options.far=100] - Far plane for the cube camera.
  349. * @param {number} [options.bounces=0] - Additional bounce passes after the initial direct pass.
  350. * @param {number} [options.sampleCount=512] - Directions integrated when projecting each cubemap to SH.
  351. */
  352. bake( renderer, scene, options = {} ) {
  353. // The bake is node based, so it needs a WebGPURenderer.
  354. if ( renderer.isWebGPURenderer !== true ) {
  355. throw new Error( 'THREE.LightProbeGrid: .bake() requires a WebGPURenderer. For WebGLRenderer, use LightProbeGridWebGL.' );
  356. }
  357. // The bake issues GPU work immediately, so the renderer must be ready.
  358. if ( renderer.initialized === false ) {
  359. throw new Error( 'THREE.LightProbeGrid: .bake() called before the renderer is initialized. Use "await renderer.init();" first.' );
  360. }
  361. // Register the light node with this renderer (idempotent).
  362. if ( renderer.library.getLightNodeClass( LightProbeGrid ) === null ) {
  363. renderer.library.addLight( LightProbeGridNode, LightProbeGrid );
  364. }
  365. const { cubemapSize = 8, near = 0.1, far = 100, bounces = 0, sampleCount = 512 } = options;
  366. this._ensureTextures();
  367. this.updateBoundingBox();
  368. const res = this.resolution;
  369. const nz = res.z;
  370. const paddedSlices = nz + 2 * ATLAS_PADDING;
  371. const totalProbes = res.x * res.y * res.z;
  372. // Bind the pooled bake resources to the current textures.
  373. ensureBakeTargets( cubemapSize, near, far, totalProbes );
  374. ensureBakeMaterials( sampleCount, _cubeRenderTarget.texture, _batchTarget.texture );
  375. _resolutionUniform.value.copy( res );
  376. const cubeCamera = _cubeCamera;
  377. const batchTarget = _batchTarget;
  378. const shMaterial = _shMaterial;
  379. const repackMaterials = _repackMaterials;
  380. const sliceZ = _sliceZUniform;
  381. // Save renderer / scene state to restore after the bake.
  382. const currentRenderTarget = renderer.getRenderTarget();
  383. const currentAutoClear = renderer.autoClear;
  384. const currentMatrixWorldAutoUpdate = scene.matrixWorldAutoUpdate;
  385. const shadowLights = [];
  386. try {
  387. // Scene is static during the bake: update once, disable auto-update.
  388. if ( currentMatrixWorldAutoUpdate === true ) {
  389. scene.updateMatrixWorld( true );
  390. scene.matrixWorldAutoUpdate = false;
  391. }
  392. // Render each shadow map once, not once per cube face.
  393. scene.traverse( ( object ) => {
  394. if ( object.isLight && object.castShadow && object.shadow ) {
  395. shadowLights.push( { light: object, autoUpdate: object.shadow.autoUpdate } );
  396. object.shadow.autoUpdate = false;
  397. object.shadow.needsUpdate = true;
  398. }
  399. } );
  400. for ( let pass = 0; pass <= bounces; pass ++ ) {
  401. // Pass 0 is direct light (grid hidden); each later pass reads the
  402. // previous pass as indirect, adding one bounce.
  403. this.visible = pass > 0;
  404. // Clear once, then write each probe's row with autoClear off. The
  405. // viewport goes on the render target, not the renderer (which ignores
  406. // the canvas viewport when one is bound).
  407. batchTarget.viewport.set( 0, 0, 9, totalProbes );
  408. renderer.setRenderTarget( batchTarget );
  409. renderer.clear();
  410. // Phase 1: render cubemaps and project to SH into the batch target.
  411. _quad.material = shMaterial;
  412. for ( let iz = 0; iz < res.z; iz ++ ) {
  413. for ( let iy = 0; iy < res.y; iy ++ ) {
  414. for ( let ix = 0; ix < res.x; ix ++ ) {
  415. const probeIndex = ix + iy * res.x + iz * res.x * res.y;
  416. this.getProbePosition( ix, iy, iz, _position );
  417. cubeCamera.position.copy( _position );
  418. // The cube faces must be cleared per face.
  419. renderer.autoClear = true;
  420. cubeCamera.update( renderer, scene );
  421. // Write only this probe's row, preserving the others.
  422. renderer.autoClear = false;
  423. batchTarget.viewport.set( 0, probeIndex, 9, 1 );
  424. renderer.setRenderTarget( batchTarget );
  425. _quad.render( renderer );
  426. }
  427. }
  428. }
  429. // Phase 2: repack the batch into the atlas, padding each sub-volume
  430. // with a copy of its first and last data slice.
  431. renderer.autoClear = true;
  432. const renderTarget = this._renderTarget;
  433. for ( let t = 0; t < 7; t ++ ) {
  434. _quad.material = repackMaterials[ t ];
  435. const base = t * paddedSlices;
  436. // Data slices.
  437. for ( let iz = 0; iz < nz; iz ++ ) {
  438. sliceZ.value = iz;
  439. renderer.setRenderTarget( renderTarget, base + ATLAS_PADDING + iz );
  440. _quad.render( renderer );
  441. }
  442. // Leading padding: copy of data slice 0.
  443. sliceZ.value = 0;
  444. renderer.setRenderTarget( renderTarget, base );
  445. _quad.render( renderer );
  446. // Trailing padding: copy of data slice nz-1.
  447. sliceZ.value = nz - 1;
  448. renderer.setRenderTarget( renderTarget, base + ATLAS_PADDING + nz );
  449. _quad.render( renderer );
  450. }
  451. }
  452. } finally {
  453. // Restore renderer / scene state (pooled targets and materials kept).
  454. renderer.setRenderTarget( currentRenderTarget );
  455. renderer.autoClear = currentAutoClear;
  456. scene.matrixWorldAutoUpdate = currentMatrixWorldAutoUpdate;
  457. for ( const { light, autoUpdate } of shadowLights ) light.shadow.autoUpdate = autoUpdate;
  458. this.visible = true;
  459. }
  460. }
  461. /**
  462. * Ensures the atlas 3D texture exists with the correct dimensions.
  463. *
  464. * @private
  465. */
  466. _ensureTextures() {
  467. if ( this._renderTarget !== null ) return;
  468. const res = this.resolution;
  469. const nx = res.x, ny = res.y, nz = res.z;
  470. // Atlas depth: 7 sub-volumes, each with ATLAS_PADDING slices at both ends.
  471. const atlasDepth = 7 * ( nz + 2 * ATLAS_PADDING );
  472. this._renderTarget = new RenderTarget3D( nx, ny, atlasDepth, {
  473. type: HalfFloatType,
  474. format: RGBAFormat,
  475. minFilter: LinearFilter,
  476. magFilter: LinearFilter,
  477. generateMipmaps: false,
  478. depthBuffer: false
  479. } );
  480. this.texture = this._renderTarget.texture;
  481. }
  482. /**
  483. * Frees GPU resources.
  484. */
  485. dispose() {
  486. if ( this._renderTarget !== null ) {
  487. this._renderTarget.dispose();
  488. this._renderTarget = null;
  489. this.texture = null;
  490. }
  491. super.dispose();
  492. }
  493. }
  494. export { LightProbeGrid };
粤ICP备19079148号