LightProbeGrid.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895
  1. import {
  2. Box3,
  3. CubeCamera,
  4. Data3DTexture,
  5. DataUtils,
  6. FloatType,
  7. HalfFloatType,
  8. LinearFilter,
  9. Mesh,
  10. NearestFilter,
  11. Object3D,
  12. OrthographicCamera,
  13. PlaneGeometry,
  14. RGBAFormat,
  15. Scene,
  16. ShaderMaterial,
  17. Vector3,
  18. Vector4,
  19. WebGL3DRenderTarget,
  20. WebGLCubeRenderTarget,
  21. WebGLRenderTarget
  22. } from 'three';
  23. // Number of SH coefficients stored per probe: 9 L2 bands x 3 colour channels.
  24. const COEFFICIENTS_PER_PROBE = 27;
  25. // Fixed-point scale used by toJSON(): coefficients are stored as
  26. // Math.round( value * SERIALIZE_SCALE ) integers. 4096 keeps the quantization
  27. // error around 1e-4 (negligible for irradiance) while gzipping very well.
  28. const SERIALIZE_SCALE = 4096;
  29. // Shared fullscreen-quad scene / camera
  30. let _scene = null;
  31. let _camera = null;
  32. let _mesh = null;
  33. // SH projection material (depends on cubemapSize)
  34. let _shMaterial = null;
  35. let _lastCubemapSize = 0;
  36. // Repack materials (one per output sub-volume / texture index)
  37. let _repackMaterials = null;
  38. // Cached bake resources
  39. let _cubeRenderTarget = null;
  40. let _cubeCamera = null;
  41. let _cachedCubemapSize = 0;
  42. let _cachedNear = 0;
  43. let _cachedFar = 0;
  44. // Cached batch render target
  45. let _batchTarget = null;
  46. let _batchTargetProbes = 0;
  47. // Reusable temp objects
  48. const _position = /*@__PURE__*/ new Vector3();
  49. const _size = /*@__PURE__*/ new Vector3();
  50. const _currentViewport = /*@__PURE__*/ new Vector4();
  51. const _currentScissor = /*@__PURE__*/ new Vector4();
  52. // Number of padding texels added at each boundary of every sub-volume in the atlas.
  53. const ATLAS_PADDING = 1;
  54. /**
  55. * A 3D grid of L2 Spherical Harmonic irradiance probes that provides
  56. * position-dependent diffuse global illumination.
  57. *
  58. * Note that this class can only be used with {@link WebGLRenderer}.
  59. * A version for {@link WebGPURenderer} will be added at a later point.
  60. *
  61. * All seven packed SH sub-volumes are stored in a **single** RGBA
  62. * `WebGL3DRenderTarget` using a texture-atlas layout along the Z axis.
  63. * Each sub-volume occupies `( nz + 2 )` atlas slices: one padding slice at
  64. * each end (a copy of the nearest edge data slice) to prevent color bleeding
  65. * when the hardware trilinear filter reads across a sub-volume boundary.
  66. *
  67. * Atlas layout (nz = resolution.z, PADDING = 1):
  68. * ```
  69. * slice 0 : padding (copy of sub-volume 0, data slice 0)
  70. * slices 1 … nz : sub-volume 0 data
  71. * slice nz + 1 : padding (copy of sub-volume 0, data slice nz-1)
  72. * slice nz + 2 : padding (copy of sub-volume 1, data slice 0)
  73. * slices nz+3 … 2*nz+2 : sub-volume 1 data
  74. * …
  75. * ```
  76. * Total atlas depth = `7 * ( nz + 2 )`.
  77. *
  78. * Baking happens almost entirely on the GPU — cubemap rendering, SH
  79. * projection, and texture packing all run as GPU passes; only a small
  80. * `9 x totalProbes` SH coefficient buffer is read back to the CPU at the
  81. * end of baking so the result can be serialized via {@link LightProbeGrid#toJSON}.
  82. *
  83. * @three_import import { LightProbeGrid } from 'three/addons/lighting/LightProbeGrid.js';
  84. */
  85. class LightProbeGrid extends Object3D {
  86. /**
  87. * Constructs a new irradiance probe grid.
  88. *
  89. * The volume is centered at the object's position.
  90. *
  91. * @param {number} [width=1] - Full width of the volume along X.
  92. * @param {number} [height=1] - Full height of the volume along Y.
  93. * @param {number} [depth=1] - Full depth of the volume along Z.
  94. * @param {number} [widthProbes] - Number of probes along X. Defaults to `Math.max( 2, Math.round( width ) + 1 )`.
  95. * @param {number} [heightProbes] - Number of probes along Y. Defaults to `Math.max( 2, Math.round( height ) + 1 )`.
  96. * @param {number} [depthProbes] - Number of probes along Z. Defaults to `Math.max( 2, Math.round( depth ) + 1 )`.
  97. */
  98. constructor( width = 1, height = 1, depth = 1, widthProbes, heightProbes, depthProbes ) {
  99. super();
  100. /**
  101. * This flag can be used for type testing.
  102. *
  103. * @type {boolean}
  104. * @readonly
  105. * @default true
  106. */
  107. this.isLightProbeGrid = true;
  108. /**
  109. * The full width of the volume along X.
  110. *
  111. * @type {number}
  112. */
  113. this.width = width;
  114. /**
  115. * The full height of the volume along Y.
  116. *
  117. * @type {number}
  118. */
  119. this.height = height;
  120. /**
  121. * The full depth of the volume along Z.
  122. *
  123. * @type {number}
  124. */
  125. this.depth = depth;
  126. /**
  127. * The number of probes along each axis.
  128. *
  129. * @type {Vector3}
  130. */
  131. this.resolution = new Vector3(
  132. widthProbes !== undefined ? widthProbes : Math.max( 2, Math.round( width ) + 1 ),
  133. heightProbes !== undefined ? heightProbes : Math.max( 2, Math.round( height ) + 1 ),
  134. depthProbes !== undefined ? depthProbes : Math.max( 2, Math.round( depth ) + 1 )
  135. );
  136. /**
  137. * The world-space bounding box for the grid. Updated automatically
  138. * by {@link LightProbeGrid#bake}.
  139. *
  140. * @type {Box3}
  141. */
  142. this.boundingBox = new Box3();
  143. /**
  144. * The single RGBA atlas 3D texture storing all seven packed SH sub-volumes.
  145. *
  146. * @type {?Data3DTexture}
  147. * @default null
  148. */
  149. this.texture = null;
  150. /**
  151. * The raw baked SH coefficients, laid out as `27` floats per probe
  152. * (`9` L2 bands times `3` colour channels), in
  153. * `ix + iy * nx + iz * nx * ny` probe order.
  154. *
  155. * Populated by {@link LightProbeGrid#bake} (read back from the GPU) and
  156. * by {@link LightProbeGrid.fromJSON}. This is the unpadded, minimal
  157. * representation used by {@link LightProbeGrid#toJSON}.
  158. *
  159. * @type {?Float32Array}
  160. * @default null
  161. */
  162. this.coefficients = null;
  163. /**
  164. * Internal render target for GPU-resident baking.
  165. *
  166. * @private
  167. * @type {?WebGL3DRenderTarget}
  168. * @default null
  169. */
  170. this._renderTarget = null;
  171. this.updateBoundingBox();
  172. }
  173. /**
  174. * Returns the world-space position of the probe at grid indices (ix, iy, iz).
  175. *
  176. * @param {number} ix - X index.
  177. * @param {number} iy - Y index.
  178. * @param {number} iz - Z index.
  179. * @param {Vector3} target - The target vector.
  180. * @return {Vector3} The world-space position.
  181. */
  182. getProbePosition( ix, iy, iz, target ) {
  183. const pos = this.position;
  184. const res = this.resolution;
  185. const w = this.width, h = this.height, d = this.depth;
  186. target.set(
  187. res.x > 1 ? pos.x - w / 2 + ix * w / ( res.x - 1 ) : pos.x,
  188. res.y > 1 ? pos.y - h / 2 + iy * h / ( res.y - 1 ) : pos.y,
  189. res.z > 1 ? pos.z - d / 2 + iz * d / ( res.z - 1 ) : pos.z
  190. );
  191. return target;
  192. }
  193. /**
  194. * Updates the world-space bounding box from the current position and size.
  195. */
  196. updateBoundingBox() {
  197. _size.set( this.width, this.height, this.depth );
  198. this.boundingBox.setFromCenterAndSize( this.position, _size );
  199. }
  200. /**
  201. * Bakes all probes by rendering cubemaps at each probe position
  202. * and projecting to L2 SH. Optionally iterates additional passes to
  203. * capture indirect bounces — each extra pass samples the previous pass's
  204. * atlas as indirect light, so a grid added to the scene before baking
  205. * accumulates one bounce per extra pass.
  206. *
  207. * @param {WebGLRenderer} renderer - The renderer.
  208. * @param {Scene} scene - The scene to render.
  209. * @param {Object} [options] - Bake options.
  210. * @param {number} [options.cubemapSize=8] - Resolution of each cubemap face.
  211. * @param {number} [options.near=0.1] - Near plane for the cube camera.
  212. * @param {number} [options.far=100] - Far plane for the cube camera.
  213. * @param {number} [options.bounces=0] - Additional bounce passes after the initial direct pass.
  214. */
  215. bake( renderer, scene, options = {} ) {
  216. const { bounces = 0 } = options;
  217. const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( options );
  218. this._ensureTextures();
  219. this.updateBoundingBox();
  220. const res = this.resolution;
  221. const totalProbes = res.x * res.y * res.z;
  222. // Batch render target for SH coefficients: 9 pixels wide, one row per probe
  223. const batchTarget = _ensureBatchTarget( totalProbes );
  224. // Save renderer state
  225. const currentRenderTarget = renderer.getRenderTarget();
  226. renderer.getViewport( _currentViewport );
  227. renderer.getScissor( _currentScissor );
  228. const currentScissorTest = renderer.getScissorTest();
  229. // Scene is static across the bake — update once and disable per-render auto updates.
  230. const currentMatrixWorldAutoUpdate = scene.matrixWorldAutoUpdate;
  231. if ( currentMatrixWorldAutoUpdate === true ) {
  232. scene.updateMatrixWorld( true );
  233. scene.matrixWorldAutoUpdate = false;
  234. }
  235. // Disable shadow map auto-update across all passes — lights don't move.
  236. // Force a single shadow update on the first render so maps are initialized.
  237. const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
  238. renderer.shadowMap.autoUpdate = false;
  239. renderer.shadowMap.needsUpdate = true;
  240. _ensureRepackResources();
  241. const paddedSlices = res.z + 2 * ATLAS_PADDING;
  242. const rt = this._renderTarget;
  243. // const t0 = performance.now();
  244. // Pass 0 captures direct light only (grid hidden, so probesSH is not sampled
  245. // — the atlas at this point may be uninitialized or hold a prior bake).
  246. // Each subsequent pass keeps the grid visible so the cube cameras read the
  247. // previous pass's atlas as indirect light, accumulating one bounce per pass.
  248. // Phase 1 writes to the batch target and Phase 2 only swaps it into the atlas
  249. // at the very end of each pass, which gives an implicit ping-pong for free.
  250. for ( let pass = 0; pass <= bounces; pass ++ ) {
  251. this.visible = pass > 0;
  252. // Clear pooled batch target so skipped probes read as zero
  253. batchTarget.scissorTest = false;
  254. batchTarget.viewport.set( 0, 0, 9, totalProbes );
  255. renderer.setRenderTarget( batchTarget );
  256. renderer.clear();
  257. // Phase 1: Render cubemaps and project to SH into batch target
  258. // Note: set viewport/scissor on the render target directly to avoid pixel ratio scaling
  259. batchTarget.scissorTest = true;
  260. for ( let iz = 0; iz < res.z; iz ++ ) {
  261. for ( let iy = 0; iy < res.y; iy ++ ) {
  262. for ( let ix = 0; ix < res.x; ix ++ ) {
  263. const probeIndex = ix + iy * res.x + iz * res.x * res.y;
  264. this.getProbePosition( ix, iy, iz, _position );
  265. cubeCamera.position.copy( _position );
  266. cubeCamera.update( renderer, scene );
  267. // SH projection
  268. _shMaterial.uniforms.envMap.value = cubeRenderTarget.texture;
  269. _mesh.material = _shMaterial;
  270. batchTarget.viewport.set( 0, probeIndex, 9, 1 );
  271. batchTarget.scissor.set( 0, probeIndex, 9, 1 );
  272. renderer.setRenderTarget( batchTarget );
  273. renderer.render( _scene, _camera );
  274. }
  275. }
  276. }
  277. // Phase 2: Repack SH data from batch target into the atlas 3D texture (GPU-to-GPU).
  278. //
  279. // For each of the 7 packed sub-volumes (texture index t) we write:
  280. // - A leading padding slice (copy of data slice iz = 0)
  281. // - All nz data slices (iz = 0 … nz-1)
  282. // - A trailing padding slice (copy of data slice iz = nz-1)
  283. //
  284. // In the atlas the slices for sub-volume t occupy the range:
  285. // [ t * paddedSlices, t * paddedSlices + paddedSlices - 1 ]
  286. // where paddedSlices = nz + 2 * ATLAS_PADDING.
  287. rt.scissorTest = false;
  288. rt.viewport.set( 0, 0, res.x, res.y );
  289. for ( let t = 0; t < 7; t ++ ) {
  290. _repackMaterials[ t ].uniforms.batchTexture.value = batchTarget.texture;
  291. _repackMaterials[ t ].uniforms.resolution.value.copy( res );
  292. // Write data slices
  293. for ( let iz = 0; iz < res.z; iz ++ ) {
  294. _repackMaterials[ t ].uniforms.sliceZ.value = iz;
  295. _mesh.material = _repackMaterials[ t ];
  296. renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + iz );
  297. renderer.render( _scene, _camera );
  298. }
  299. // Leading padding: copy of data slice iz = 0
  300. _repackMaterials[ t ].uniforms.sliceZ.value = 0;
  301. _mesh.material = _repackMaterials[ t ];
  302. renderer.setRenderTarget( rt, t * paddedSlices );
  303. renderer.render( _scene, _camera );
  304. // Trailing padding: copy of data slice iz = nz - 1
  305. _repackMaterials[ t ].uniforms.sliceZ.value = res.z - 1;
  306. _mesh.material = _repackMaterials[ t ];
  307. renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + res.z );
  308. renderer.render( _scene, _camera );
  309. }
  310. }
  311. renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
  312. // Read the final pass's SH coefficients back to the CPU so the bake can
  313. // be serialized (see toJSON). The batch target is a compact 9 x totalProbes
  314. // RGBA buffer; only the RGB of each of the 9 texels per row carries data.
  315. {
  316. const batchPixels = new Float32Array( 9 * totalProbes * 4 );
  317. renderer.readRenderTargetPixels( batchTarget, 0, 0, 9, totalProbes, batchPixels );
  318. const coefficients = new Float32Array( totalProbes * COEFFICIENTS_PER_PROBE );
  319. for ( let p = 0; p < totalProbes; p ++ ) {
  320. for ( let k = 0; k < 9; k ++ ) {
  321. const src = ( p * 9 + k ) * 4;
  322. const dst = p * COEFFICIENTS_PER_PROBE + k * 3;
  323. coefficients[ dst ] = batchPixels[ src ];
  324. coefficients[ dst + 1 ] = batchPixels[ src + 1 ];
  325. coefficients[ dst + 2 ] = batchPixels[ src + 2 ];
  326. }
  327. }
  328. this.coefficients = coefficients;
  329. }
  330. // Restore renderer state
  331. renderer.setRenderTarget( currentRenderTarget );
  332. renderer.setViewport( _currentViewport );
  333. renderer.setScissor( _currentScissor );
  334. renderer.setScissorTest( currentScissorTest );
  335. scene.matrixWorldAutoUpdate = currentMatrixWorldAutoUpdate;
  336. // console.log( `LightProbeGrid: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` );
  337. this.visible = true;
  338. }
  339. /**
  340. * Ensures the atlas 3D render target exists with the correct dimensions.
  341. *
  342. * @private
  343. */
  344. _ensureTextures() {
  345. if ( this._renderTarget !== null ) return;
  346. // Re-baking a deserialized grid: drop the standalone atlas texture.
  347. if ( this.texture !== null ) this.texture.dispose();
  348. const res = this.resolution;
  349. const nx = res.x, ny = res.y, nz = res.z;
  350. // Atlas depth: 7 sub-volumes, each with ATLAS_PADDING slices at both ends
  351. const atlasDepth = 7 * ( nz + 2 * ATLAS_PADDING );
  352. const rt = new WebGL3DRenderTarget( nx, ny, atlasDepth, {
  353. format: RGBAFormat,
  354. type: FloatType,
  355. minFilter: LinearFilter,
  356. magFilter: LinearFilter,
  357. generateMipmaps: false,
  358. depthBuffer: false
  359. } );
  360. this._renderTarget = rt;
  361. this.texture = rt.texture;
  362. }
  363. /**
  364. * Frees GPU resources.
  365. */
  366. dispose() {
  367. if ( this._renderTarget !== null ) {
  368. // Baked grid: the texture is owned by the render target.
  369. this._renderTarget.dispose();
  370. this._renderTarget = null;
  371. } else if ( this.texture !== null ) {
  372. // Deserialized grid: the texture is a standalone Data3DTexture.
  373. this.texture.dispose();
  374. }
  375. this.texture = null;
  376. }
  377. /**
  378. * Serializes the baked grid to a JSON-compatible object.
  379. *
  380. * Only the `27` SH coefficients per probe are stored — the padded atlas
  381. * layout used on the GPU is fully reconstructed by {@link LightProbeGrid.fromJSON}.
  382. * Coefficients are stored as fixed-point integers (`value * scale`,
  383. * rounded). This is simple, human-readable, and — because the many
  384. * near-zero higher-order coefficients collapse to short tokens — gzips
  385. * better than a binary layout: a `7 x 7 x 3` grid is a ~16 KB JSON file
  386. * that gzips to ~6.7 KB.
  387. *
  388. * Requires the grid to have been baked (see {@link LightProbeGrid#bake}).
  389. *
  390. * @return {Object} A JSON-compatible representation of the grid.
  391. */
  392. toJSON() {
  393. if ( this.coefficients === null ) {
  394. throw new Error( 'THREE.LightProbeGrid: toJSON() requires a baked grid. Call bake() first.' );
  395. }
  396. const source = this.coefficients;
  397. const data = new Array( source.length );
  398. for ( let i = 0; i < source.length; i ++ ) {
  399. data[ i ] = Math.round( source[ i ] * SERIALIZE_SCALE );
  400. }
  401. return {
  402. metadata: {
  403. version: 1,
  404. type: 'LightProbeGrid',
  405. generator: 'LightProbeGrid.toJSON'
  406. },
  407. width: this.width,
  408. height: this.height,
  409. depth: this.depth,
  410. resolution: [ this.resolution.x, this.resolution.y, this.resolution.z ],
  411. position: [ this.position.x, this.position.y, this.position.z ],
  412. coefficients: {
  413. scale: SERIALIZE_SCALE,
  414. data: data
  415. }
  416. };
  417. }
  418. /**
  419. * Reconstructs a baked grid from data produced by {@link LightProbeGrid#toJSON}.
  420. *
  421. * The returned grid carries a ready-to-use {@link Data3DTexture} and does
  422. * not need a renderer — baking can be skipped entirely.
  423. *
  424. * @param {Object} json - The serialized grid.
  425. * @return {LightProbeGrid} The reconstructed grid.
  426. */
  427. static fromJSON( json ) {
  428. const [ nx, ny, nz ] = json.resolution;
  429. const grid = new LightProbeGrid( json.width, json.height, json.depth, nx, ny, nz );
  430. grid.position.fromArray( json.position );
  431. grid.updateBoundingBox();
  432. const { scale, data } = json.coefficients;
  433. const coefficients = new Float32Array( data.length );
  434. for ( let i = 0; i < data.length; i ++ ) {
  435. coefficients[ i ] = data[ i ] / scale;
  436. }
  437. grid.coefficients = coefficients;
  438. grid.texture = grid._createDataTexture( coefficients );
  439. return grid;
  440. }
  441. /**
  442. * Builds the padded atlas {@link Data3DTexture} from the raw SH
  443. * coefficients (`27` per probe). The atlas layout matches the one produced
  444. * by {@link LightProbeGrid#bake} so the same shader code can sample either.
  445. *
  446. * @private
  447. * @param {Float32Array} coefficients - Raw coefficients (`27` per probe).
  448. * @return {Data3DTexture} The atlas texture.
  449. */
  450. _createDataTexture( coefficients ) {
  451. const res = this.resolution;
  452. const nx = res.x, ny = res.y, nz = res.z;
  453. const sliceTexels = nx * ny;
  454. const paddedSlices = nz + 2 * ATLAS_PADDING;
  455. const atlasDepth = 7 * paddedSlices;
  456. const atlas = new Uint16Array( sliceTexels * atlasDepth * 4 );
  457. for ( let t = 0; t < 7; t ++ ) {
  458. for ( let s = 0; s < paddedSlices; s ++ ) {
  459. // Padding slices clamp to the first / last data slice.
  460. const iz = Math.min( Math.max( s - ATLAS_PADDING, 0 ), nz - 1 );
  461. const layer = t * paddedSlices + s;
  462. for ( let iy = 0; iy < ny; iy ++ ) {
  463. for ( let ix = 0; ix < nx; ix ++ ) {
  464. const probeIndex = ix + iy * nx + iz * sliceTexels;
  465. const src = probeIndex * COEFFICIENTS_PER_PROBE;
  466. const dst = ( layer * sliceTexels + iy * nx + ix ) * 4;
  467. for ( let ch = 0; ch < 4; ch ++ ) {
  468. // Channels map sequentially onto the 27 coefficients;
  469. // the very last channel (sub-volume 6, alpha) is unused.
  470. const ci = t * 4 + ch;
  471. atlas[ dst + ch ] = ci < COEFFICIENTS_PER_PROBE ? DataUtils.toHalfFloat( coefficients[ src + ci ] ) : 0;
  472. }
  473. }
  474. }
  475. }
  476. }
  477. const texture = new Data3DTexture( atlas, nx, ny, atlasDepth );
  478. texture.format = RGBAFormat;
  479. texture.type = HalfFloatType;
  480. texture.minFilter = LinearFilter;
  481. texture.magFilter = LinearFilter;
  482. texture.generateMipmaps = false;
  483. texture.needsUpdate = true;
  484. return texture;
  485. }
  486. }
  487. // Internal: Ensure the shared fullscreen-quad scene exists
  488. function _ensureScene() {
  489. if ( _scene === null ) {
  490. _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
  491. _mesh = new Mesh( new PlaneGeometry( 2, 2 ) );
  492. _scene = new Scene();
  493. _scene.add( _mesh );
  494. }
  495. }
  496. // Internal: Ensure GPU resources for SH projection are created
  497. function _ensureGPUResources( cubemapSize ) {
  498. _ensureScene();
  499. // Recreate material when cubemap size changes
  500. if ( cubemapSize !== _lastCubemapSize ) {
  501. if ( _shMaterial !== null ) _shMaterial.dispose();
  502. _shMaterial = new ShaderMaterial( {
  503. precision: 'highp',
  504. defines: {
  505. CUBEMAP_SIZE: cubemapSize
  506. },
  507. uniforms: {
  508. envMap: { value: null }
  509. },
  510. vertexShader: /* glsl */`
  511. void main() {
  512. gl_Position = vec4( position.xy, 0.0, 1.0 );
  513. }
  514. `,
  515. fragmentShader: /* glsl */`
  516. #include <common>
  517. uniform samplerCube envMap;
  518. void main() {
  519. int coefIndex = int( gl_FragCoord.x );
  520. vec3 accum0 = vec3( 0.0 );
  521. vec3 accum1 = vec3( 0.0 );
  522. vec3 accum2 = vec3( 0.0 );
  523. vec3 accum3 = vec3( 0.0 );
  524. vec3 accum4 = vec3( 0.0 );
  525. vec3 accum5 = vec3( 0.0 );
  526. vec3 accum6 = vec3( 0.0 );
  527. vec3 accum7 = vec3( 0.0 );
  528. vec3 accum8 = vec3( 0.0 );
  529. float totalWeight = 0.0;
  530. float pixelSize = 2.0 / float( CUBEMAP_SIZE );
  531. for ( int face = 0; face < 6; face ++ ) {
  532. for ( int iy = 0; iy < CUBEMAP_SIZE; iy ++ ) {
  533. for ( int ix = 0; ix < CUBEMAP_SIZE; ix ++ ) {
  534. // WebGL cubemaps have a left-handed orientation (flip = -1)
  535. float col = ( float( ix ) + 0.5 ) * pixelSize - 1.0;
  536. float row = 1.0 - ( float( iy ) + 0.5 ) * pixelSize;
  537. vec3 coord;
  538. if ( face == 0 ) coord = vec3( 1.0, row, -col );
  539. else if ( face == 1 ) coord = vec3( -1.0, row, col );
  540. else if ( face == 2 ) coord = vec3( col, 1.0, -row );
  541. else if ( face == 3 ) coord = vec3( col, -1.0, row );
  542. else if ( face == 4 ) coord = vec3( col, row, 1.0 );
  543. else coord = vec3( -col, row, -1.0 );
  544. float lengthSq = dot( coord, coord );
  545. float weight = 4.0 / ( sqrt( lengthSq ) * lengthSq );
  546. totalWeight += weight;
  547. vec3 dir = normalize( coord );
  548. vec3 cw = textureCube( envMap, coord ).rgb * weight;
  549. // band 0
  550. accum0 += cw * 0.282095;
  551. // band 1
  552. accum1 += cw * ( 0.488603 * dir.y );
  553. accum2 += cw * ( 0.488603 * dir.z );
  554. accum3 += cw * ( 0.488603 * dir.x );
  555. // band 2
  556. accum4 += cw * ( 1.092548 * ( dir.x * dir.y ) );
  557. accum5 += cw * ( 1.092548 * ( dir.y * dir.z ) );
  558. accum6 += cw * ( 0.315392 * ( 3.0 * dir.z * dir.z - 1.0 ) );
  559. accum7 += cw * ( 1.092548 * ( dir.x * dir.z ) );
  560. accum8 += cw * ( 0.546274 * ( dir.x * dir.x - dir.y * dir.y ) );
  561. }
  562. }
  563. }
  564. float norm = 4.0 * PI / totalWeight;
  565. vec3 accum;
  566. if ( coefIndex == 0 ) accum = accum0;
  567. else if ( coefIndex == 1 ) accum = accum1;
  568. else if ( coefIndex == 2 ) accum = accum2;
  569. else if ( coefIndex == 3 ) accum = accum3;
  570. else if ( coefIndex == 4 ) accum = accum4;
  571. else if ( coefIndex == 5 ) accum = accum5;
  572. else if ( coefIndex == 6 ) accum = accum6;
  573. else if ( coefIndex == 7 ) accum = accum7;
  574. else accum = accum8;
  575. gl_FragColor = vec4( accum * norm, 1.0 );
  576. }
  577. `
  578. } );
  579. _lastCubemapSize = cubemapSize;
  580. }
  581. }
  582. // Internal: Ensure GPU resources for repacking SH into the atlas 3D texture
  583. function _ensureRepackResources() {
  584. if ( _repackMaterials !== null ) return;
  585. _ensureScene();
  586. // Create 7 materials, one per output texture packing
  587. // Texture 0: (c0.r, c0.g, c0.b, c1.r)
  588. // Texture 1: (c1.g, c1.b, c2.r, c2.g)
  589. // Texture 2: (c2.b, c3.r, c3.g, c3.b)
  590. // Texture 3: (c4.r, c4.g, c4.b, c5.r)
  591. // Texture 4: (c5.g, c5.b, c6.r, c6.g)
  592. // Texture 5: (c6.b, c7.r, c7.g, c7.b)
  593. // Texture 6: (c8.r, c8.g, c8.b, 0.0)
  594. const repackVertexShader = /* glsl */`
  595. void main() {
  596. gl_Position = vec4( position.xy, 0.0, 1.0 );
  597. }
  598. `;
  599. _repackMaterials = [];
  600. for ( let t = 0; t < 7; t ++ ) {
  601. _repackMaterials[ t ] = new ShaderMaterial( {
  602. precision: 'highp',
  603. defines: {
  604. TEXTURE_INDEX: t
  605. },
  606. uniforms: {
  607. batchTexture: { value: null },
  608. resolution: { value: new Vector3() },
  609. sliceZ: { value: 0 }
  610. },
  611. vertexShader: repackVertexShader,
  612. fragmentShader: /* glsl */`
  613. uniform sampler2D batchTexture;
  614. uniform vec3 resolution;
  615. uniform int sliceZ;
  616. void main() {
  617. int ix = int( gl_FragCoord.x );
  618. int iy = int( gl_FragCoord.y );
  619. int iz = sliceZ;
  620. int probeIndex = ix + iy * int( resolution.x ) + iz * int( resolution.x ) * int( resolution.y );
  621. // Read 9 SH coefficients from the batch texture row
  622. vec4 c0 = texelFetch( batchTexture, ivec2( 0, probeIndex ), 0 );
  623. vec4 c1 = texelFetch( batchTexture, ivec2( 1, probeIndex ), 0 );
  624. vec4 c2 = texelFetch( batchTexture, ivec2( 2, probeIndex ), 0 );
  625. vec4 c3 = texelFetch( batchTexture, ivec2( 3, probeIndex ), 0 );
  626. vec4 c4 = texelFetch( batchTexture, ivec2( 4, probeIndex ), 0 );
  627. vec4 c5 = texelFetch( batchTexture, ivec2( 5, probeIndex ), 0 );
  628. vec4 c6 = texelFetch( batchTexture, ivec2( 6, probeIndex ), 0 );
  629. vec4 c7 = texelFetch( batchTexture, ivec2( 7, probeIndex ), 0 );
  630. vec4 c8 = texelFetch( batchTexture, ivec2( 8, probeIndex ), 0 );
  631. // Pack into the output format for this texture index
  632. #if TEXTURE_INDEX == 0
  633. gl_FragColor = vec4( c0.rgb, c1.r );
  634. #elif TEXTURE_INDEX == 1
  635. gl_FragColor = vec4( c1.gb, c2.rg );
  636. #elif TEXTURE_INDEX == 2
  637. gl_FragColor = vec4( c2.b, c3.rgb );
  638. #elif TEXTURE_INDEX == 3
  639. gl_FragColor = vec4( c4.rgb, c5.r );
  640. #elif TEXTURE_INDEX == 4
  641. gl_FragColor = vec4( c5.gb, c6.rg );
  642. #elif TEXTURE_INDEX == 5
  643. gl_FragColor = vec4( c6.b, c7.rgb );
  644. #else
  645. gl_FragColor = vec4( c8.rgb, 0.0 );
  646. #endif
  647. }
  648. `
  649. } );
  650. }
  651. }
  652. // Internal: Ensure cube render target and camera exist with the right parameters
  653. function _ensureBakeResources( options ) {
  654. const {
  655. cubemapSize = 8,
  656. near = 0.1,
  657. far = 100
  658. } = options;
  659. if ( _cubeRenderTarget === null || cubemapSize !== _cachedCubemapSize || near !== _cachedNear || far !== _cachedFar ) {
  660. if ( _cubeRenderTarget !== null ) _cubeRenderTarget.dispose();
  661. _cubeRenderTarget = new WebGLCubeRenderTarget( cubemapSize, { type: HalfFloatType } );
  662. _cubeCamera = new CubeCamera( near, far, _cubeRenderTarget );
  663. _cachedCubemapSize = cubemapSize;
  664. _cachedNear = near;
  665. _cachedFar = far;
  666. }
  667. _ensureGPUResources( cubemapSize );
  668. return { cubeRenderTarget: _cubeRenderTarget, cubeCamera: _cubeCamera };
  669. }
  670. function _ensureBatchTarget( totalProbes ) {
  671. if ( _batchTarget === null || _batchTargetProbes !== totalProbes ) {
  672. if ( _batchTarget !== null ) _batchTarget.dispose();
  673. _batchTarget = new WebGLRenderTarget( 9, totalProbes, {
  674. type: FloatType,
  675. minFilter: NearestFilter,
  676. magFilter: NearestFilter,
  677. depthBuffer: false
  678. } );
  679. _batchTargetProbes = totalProbes;
  680. }
  681. return _batchTarget;
  682. }
  683. export { LightProbeGrid };
粤ICP备19079148号