LightProbeGrid.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. import {
  2. Box3,
  3. CubeCamera,
  4. FloatType,
  5. HalfFloatType,
  6. LinearFilter,
  7. Mesh,
  8. NearestFilter,
  9. Object3D,
  10. OrthographicCamera,
  11. PlaneGeometry,
  12. RGBAFormat,
  13. Scene,
  14. ShaderMaterial,
  15. Vector3,
  16. Vector4,
  17. WebGL3DRenderTarget,
  18. WebGLCubeRenderTarget,
  19. WebGLRenderTarget
  20. } from 'three';
  21. // Shared fullscreen-quad scene / camera
  22. let _scene = null;
  23. let _camera = null;
  24. let _mesh = null;
  25. // SH projection material (depends on cubemapSize)
  26. let _shMaterial = null;
  27. let _lastCubemapSize = 0;
  28. // Repack materials (one per output sub-volume / texture index)
  29. let _repackMaterials = null;
  30. // Cached bake resources
  31. let _cubeRenderTarget = null;
  32. let _cubeCamera = null;
  33. let _cachedCubemapSize = 0;
  34. let _cachedNear = 0;
  35. let _cachedFar = 0;
  36. // Cached batch render target
  37. let _batchTarget = null;
  38. let _batchTargetProbes = 0;
  39. // Reusable temp objects
  40. const _position = /*@__PURE__*/ new Vector3();
  41. const _size = /*@__PURE__*/ new Vector3();
  42. const _savedViewport = /*@__PURE__*/ new Vector4();
  43. const _savedScissor = /*@__PURE__*/ new Vector4();
  44. // Number of padding texels added at each boundary of every sub-volume in the atlas.
  45. const ATLAS_PADDING = 1;
  46. /**
  47. * A 3D grid of L2 Spherical Harmonic irradiance probes that provides
  48. * position-dependent diffuse global illumination.
  49. *
  50. * All seven packed SH sub-volumes are stored in a **single** RGBA
  51. * `WebGL3DRenderTarget` using a texture-atlas layout along the Z axis.
  52. * Each sub-volume occupies `( nz + 2 )` atlas slices: one padding slice at
  53. * each end (a copy of the nearest edge data slice) to prevent color bleeding
  54. * when the hardware trilinear filter reads across a sub-volume boundary.
  55. *
  56. * Atlas layout (nz = resolution.z, PADDING = 1):
  57. * ```
  58. * slice 0 : padding (copy of sub-volume 0, data slice 0)
  59. * slices 1 … nz : sub-volume 0 data
  60. * slice nz + 1 : padding (copy of sub-volume 0, data slice nz-1)
  61. * slice nz + 2 : padding (copy of sub-volume 1, data slice 0)
  62. * slices nz+3 … 2*nz+2 : sub-volume 1 data
  63. * …
  64. * ```
  65. * Total atlas depth = `7 * ( nz + 2 )`.
  66. *
  67. * Baking is fully GPU-resident: cubemap rendering, SH projection, and
  68. * texture packing all happen on the GPU with zero CPU readback.
  69. *
  70. * @three_import import { LightProbeGrid } from 'three/addons/lighting/LightProbeGrid.js';
  71. */
  72. class LightProbeGrid extends Object3D {
  73. /**
  74. * Constructs a new irradiance probe grid.
  75. *
  76. * The volume is centered at the object's position.
  77. *
  78. * @param {number} [width=1] - Full width of the volume along X.
  79. * @param {number} [height=1] - Full height of the volume along Y.
  80. * @param {number} [depth=1] - Full depth of the volume along Z.
  81. * @param {number} [widthProbes] - Number of probes along X. Defaults to `Math.max( 2, Math.round( width ) + 1 )`.
  82. * @param {number} [heightProbes] - Number of probes along Y. Defaults to `Math.max( 2, Math.round( height ) + 1 )`.
  83. * @param {number} [depthProbes] - Number of probes along Z. Defaults to `Math.max( 2, Math.round( depth ) + 1 )`.
  84. */
  85. constructor( width = 1, height = 1, depth = 1, widthProbes, heightProbes, depthProbes ) {
  86. super();
  87. /**
  88. * This flag can be used for type testing.
  89. *
  90. * @type {boolean}
  91. * @readonly
  92. * @default true
  93. */
  94. this.isLightProbeGrid = true;
  95. /**
  96. * The full width of the volume along X.
  97. *
  98. * @type {number}
  99. */
  100. this.width = width;
  101. /**
  102. * The full height of the volume along Y.
  103. *
  104. * @type {number}
  105. */
  106. this.height = height;
  107. /**
  108. * The full depth of the volume along Z.
  109. *
  110. * @type {number}
  111. */
  112. this.depth = depth;
  113. /**
  114. * The number of probes along each axis.
  115. *
  116. * @type {Vector3}
  117. */
  118. this.resolution = new Vector3(
  119. widthProbes !== undefined ? widthProbes : Math.max( 2, Math.round( width ) + 1 ),
  120. heightProbes !== undefined ? heightProbes : Math.max( 2, Math.round( height ) + 1 ),
  121. depthProbes !== undefined ? depthProbes : Math.max( 2, Math.round( depth ) + 1 )
  122. );
  123. /**
  124. * The world-space bounding box for the grid. Updated automatically
  125. * by {@link LightProbeGrid#bake}.
  126. *
  127. * @type {Box3}
  128. */
  129. this.boundingBox = new Box3();
  130. /**
  131. * The single RGBA atlas 3D texture storing all seven packed SH sub-volumes.
  132. *
  133. * @type {?Data3DTexture}
  134. * @default null
  135. */
  136. this.texture = null;
  137. /**
  138. * Internal render target for GPU-resident baking.
  139. *
  140. * @private
  141. * @type {?WebGL3DRenderTarget}
  142. * @default null
  143. */
  144. this._renderTarget = null;
  145. this.updateBoundingBox();
  146. }
  147. /**
  148. * Returns the world-space position of the probe at grid indices (ix, iy, iz).
  149. *
  150. * @param {number} ix - X index.
  151. * @param {number} iy - Y index.
  152. * @param {number} iz - Z index.
  153. * @param {Vector3} target - The target vector.
  154. * @return {Vector3} The world-space position.
  155. */
  156. getProbePosition( ix, iy, iz, target ) {
  157. const pos = this.position;
  158. const res = this.resolution;
  159. const w = this.width, h = this.height, d = this.depth;
  160. target.set(
  161. res.x > 1 ? pos.x - w / 2 + ix * w / ( res.x - 1 ) : pos.x,
  162. res.y > 1 ? pos.y - h / 2 + iy * h / ( res.y - 1 ) : pos.y,
  163. res.z > 1 ? pos.z - d / 2 + iz * d / ( res.z - 1 ) : pos.z
  164. );
  165. return target;
  166. }
  167. /**
  168. * Updates the world-space bounding box from the current position and size.
  169. */
  170. updateBoundingBox() {
  171. _size.set( this.width, this.height, this.depth );
  172. this.boundingBox.setFromCenterAndSize( this.position, _size );
  173. }
  174. /**
  175. * Bakes all probes by rendering cubemaps at each probe position
  176. * and projecting to L2 SH. Fully GPU-resident with zero CPU readback.
  177. *
  178. * @param {WebGLRenderer} renderer - The renderer.
  179. * @param {Scene} scene - The scene to render.
  180. * @param {Object} [options] - Bake options.
  181. * @param {number} [options.cubemapSize=8] - Resolution of each cubemap face.
  182. * @param {number} [options.near=0.1] - Near plane for the cube camera.
  183. * @param {number} [options.far=100] - Far plane for the cube camera.
  184. */
  185. bake( renderer, scene, options = {} ) {
  186. const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( options );
  187. this._ensureTextures();
  188. this.updateBoundingBox();
  189. // Prevent feedback: temporarily hide the volume during baking
  190. this.visible = false;
  191. const res = this.resolution;
  192. const totalProbes = res.x * res.y * res.z;
  193. // Batch render target for SH coefficients: 9 pixels wide, one row per probe
  194. const batchTarget = _ensureBatchTarget( totalProbes );
  195. // Save renderer state
  196. const savedRenderTarget = renderer.getRenderTarget();
  197. renderer.getViewport( _savedViewport );
  198. renderer.getScissor( _savedScissor );
  199. const savedScissorTest = renderer.getScissorTest();
  200. // Clear pooled batch target so skipped probes read as zero
  201. batchTarget.scissorTest = false;
  202. batchTarget.viewport.set( 0, 0, 9, totalProbes );
  203. renderer.setRenderTarget( batchTarget );
  204. renderer.clear();
  205. // const t0 = performance.now();
  206. // Phase 1: Render cubemaps and project to SH into batch target
  207. // Note: set viewport/scissor on the render target directly to avoid pixel ratio scaling
  208. batchTarget.scissorTest = true;
  209. // Disable shadow map auto-update during bake — lights don't move between probes.
  210. // Force one shadow update on the first render so maps are initialized.
  211. const savedShadowAutoUpdate = renderer.shadowMap.autoUpdate;
  212. renderer.shadowMap.autoUpdate = false;
  213. renderer.shadowMap.needsUpdate = true;
  214. for ( let iz = 0; iz < res.z; iz ++ ) {
  215. for ( let iy = 0; iy < res.y; iy ++ ) {
  216. for ( let ix = 0; ix < res.x; ix ++ ) {
  217. const probeIndex = ix + iy * res.x + iz * res.x * res.y;
  218. this.getProbePosition( ix, iy, iz, _position );
  219. cubeCamera.position.copy( _position );
  220. cubeCamera.update( renderer, scene );
  221. // SH projection
  222. _shMaterial.uniforms.envMap.value = cubeRenderTarget.texture;
  223. _mesh.material = _shMaterial;
  224. batchTarget.viewport.set( 0, probeIndex, 9, 1 );
  225. batchTarget.scissor.set( 0, probeIndex, 9, 1 );
  226. renderer.setRenderTarget( batchTarget );
  227. renderer.render( _scene, _camera );
  228. }
  229. }
  230. }
  231. renderer.shadowMap.autoUpdate = savedShadowAutoUpdate;
  232. // Phase 2: Repack SH data from batch target into the atlas 3D texture (GPU-to-GPU).
  233. //
  234. // For each of the 7 packed sub-volumes (texture index t) we write:
  235. // - A leading padding slice (copy of data slice iz = 0)
  236. // - All nz data slices (iz = 0 … nz-1)
  237. // - A trailing padding slice (copy of data slice iz = nz-1)
  238. //
  239. // In the atlas the slices for sub-volume t occupy the range:
  240. // [ t * paddedSlices, t * paddedSlices + paddedSlices - 1 ]
  241. // where paddedSlices = nz + 2 * ATLAS_PADDING.
  242. _ensureRepackResources();
  243. const paddedSlices = res.z + 2 * ATLAS_PADDING;
  244. const rt = this._renderTarget;
  245. rt.scissorTest = false;
  246. rt.viewport.set( 0, 0, res.x, res.y );
  247. for ( let t = 0; t < 7; t ++ ) {
  248. _repackMaterials[ t ].uniforms.batchTexture.value = batchTarget.texture;
  249. _repackMaterials[ t ].uniforms.resolution.value.copy( res );
  250. // Write data slices
  251. for ( let iz = 0; iz < res.z; iz ++ ) {
  252. _repackMaterials[ t ].uniforms.sliceZ.value = iz;
  253. _mesh.material = _repackMaterials[ t ];
  254. renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + iz );
  255. renderer.render( _scene, _camera );
  256. }
  257. // Leading padding: copy of data slice iz = 0
  258. _repackMaterials[ t ].uniforms.sliceZ.value = 0;
  259. _mesh.material = _repackMaterials[ t ];
  260. renderer.setRenderTarget( rt, t * paddedSlices );
  261. renderer.render( _scene, _camera );
  262. // Trailing padding: copy of data slice iz = nz - 1
  263. _repackMaterials[ t ].uniforms.sliceZ.value = res.z - 1;
  264. _mesh.material = _repackMaterials[ t ];
  265. renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + res.z );
  266. renderer.render( _scene, _camera );
  267. }
  268. // Restore renderer state
  269. renderer.setRenderTarget( savedRenderTarget );
  270. renderer.setViewport( _savedViewport );
  271. renderer.setScissor( _savedScissor );
  272. renderer.setScissorTest( savedScissorTest );
  273. // console.log( `LightProbeGrid: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` );
  274. this.visible = true;
  275. }
  276. /**
  277. * Ensures the atlas 3D render target exists with the correct dimensions.
  278. *
  279. * @private
  280. */
  281. _ensureTextures() {
  282. if ( this._renderTarget !== null ) return;
  283. const res = this.resolution;
  284. const nx = res.x, ny = res.y, nz = res.z;
  285. // Atlas depth: 7 sub-volumes, each with ATLAS_PADDING slices at both ends
  286. const atlasDepth = 7 * ( nz + 2 * ATLAS_PADDING );
  287. const rt = new WebGL3DRenderTarget( nx, ny, atlasDepth, {
  288. format: RGBAFormat,
  289. type: FloatType,
  290. minFilter: LinearFilter,
  291. magFilter: LinearFilter,
  292. generateMipmaps: false,
  293. depthBuffer: false
  294. } );
  295. this._renderTarget = rt;
  296. this.texture = rt.texture;
  297. }
  298. /**
  299. * Frees GPU resources.
  300. */
  301. dispose() {
  302. if ( this._renderTarget !== null ) {
  303. this._renderTarget.dispose();
  304. this._renderTarget = null;
  305. this.texture = null;
  306. }
  307. }
  308. }
  309. // Internal: Ensure the shared fullscreen-quad scene exists
  310. function _ensureScene() {
  311. if ( _scene === null ) {
  312. _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
  313. _mesh = new Mesh( new PlaneGeometry( 2, 2 ) );
  314. _scene = new Scene();
  315. _scene.add( _mesh );
  316. }
  317. }
  318. // Internal: Ensure GPU resources for SH projection are created
  319. function _ensureGPUResources( cubemapSize ) {
  320. _ensureScene();
  321. // Recreate material when cubemap size changes
  322. if ( cubemapSize !== _lastCubemapSize ) {
  323. if ( _shMaterial !== null ) _shMaterial.dispose();
  324. _shMaterial = new ShaderMaterial( {
  325. precision: 'highp',
  326. defines: {
  327. CUBEMAP_SIZE: cubemapSize
  328. },
  329. uniforms: {
  330. envMap: { value: null }
  331. },
  332. vertexShader: /* glsl */`
  333. void main() {
  334. gl_Position = vec4( position.xy, 0.0, 1.0 );
  335. }
  336. `,
  337. fragmentShader: /* glsl */`
  338. #include <common>
  339. uniform samplerCube envMap;
  340. void main() {
  341. int coefIndex = int( gl_FragCoord.x );
  342. vec3 accum0 = vec3( 0.0 );
  343. vec3 accum1 = vec3( 0.0 );
  344. vec3 accum2 = vec3( 0.0 );
  345. vec3 accum3 = vec3( 0.0 );
  346. vec3 accum4 = vec3( 0.0 );
  347. vec3 accum5 = vec3( 0.0 );
  348. vec3 accum6 = vec3( 0.0 );
  349. vec3 accum7 = vec3( 0.0 );
  350. vec3 accum8 = vec3( 0.0 );
  351. float totalWeight = 0.0;
  352. float pixelSize = 2.0 / float( CUBEMAP_SIZE );
  353. for ( int face = 0; face < 6; face ++ ) {
  354. for ( int iy = 0; iy < CUBEMAP_SIZE; iy ++ ) {
  355. for ( int ix = 0; ix < CUBEMAP_SIZE; ix ++ ) {
  356. // WebGL cubemaps have a left-handed orientation (flip = -1)
  357. float col = ( float( ix ) + 0.5 ) * pixelSize - 1.0;
  358. float row = 1.0 - ( float( iy ) + 0.5 ) * pixelSize;
  359. vec3 coord;
  360. if ( face == 0 ) coord = vec3( 1.0, row, -col );
  361. else if ( face == 1 ) coord = vec3( -1.0, row, col );
  362. else if ( face == 2 ) coord = vec3( col, 1.0, -row );
  363. else if ( face == 3 ) coord = vec3( col, -1.0, row );
  364. else if ( face == 4 ) coord = vec3( col, row, 1.0 );
  365. else coord = vec3( -col, row, -1.0 );
  366. float lengthSq = dot( coord, coord );
  367. float weight = 4.0 / ( sqrt( lengthSq ) * lengthSq );
  368. totalWeight += weight;
  369. vec3 dir = normalize( coord );
  370. vec3 cw = textureCube( envMap, coord ).rgb * weight;
  371. // band 0
  372. accum0 += cw * 0.282095;
  373. // band 1
  374. accum1 += cw * ( 0.488603 * dir.y );
  375. accum2 += cw * ( 0.488603 * dir.z );
  376. accum3 += cw * ( 0.488603 * dir.x );
  377. // band 2
  378. accum4 += cw * ( 1.092548 * ( dir.x * dir.y ) );
  379. accum5 += cw * ( 1.092548 * ( dir.y * dir.z ) );
  380. accum6 += cw * ( 0.315392 * ( 3.0 * dir.z * dir.z - 1.0 ) );
  381. accum7 += cw * ( 1.092548 * ( dir.x * dir.z ) );
  382. accum8 += cw * ( 0.546274 * ( dir.x * dir.x - dir.y * dir.y ) );
  383. }
  384. }
  385. }
  386. float norm = 4.0 * PI / totalWeight;
  387. vec3 accum;
  388. if ( coefIndex == 0 ) accum = accum0;
  389. else if ( coefIndex == 1 ) accum = accum1;
  390. else if ( coefIndex == 2 ) accum = accum2;
  391. else if ( coefIndex == 3 ) accum = accum3;
  392. else if ( coefIndex == 4 ) accum = accum4;
  393. else if ( coefIndex == 5 ) accum = accum5;
  394. else if ( coefIndex == 6 ) accum = accum6;
  395. else if ( coefIndex == 7 ) accum = accum7;
  396. else accum = accum8;
  397. gl_FragColor = vec4( accum * norm, 1.0 );
  398. }
  399. `
  400. } );
  401. _lastCubemapSize = cubemapSize;
  402. }
  403. }
  404. // Internal: Ensure GPU resources for repacking SH into the atlas 3D texture
  405. function _ensureRepackResources() {
  406. if ( _repackMaterials !== null ) return;
  407. _ensureScene();
  408. // Create 7 materials, one per output texture packing
  409. // Texture 0: (c0.r, c0.g, c0.b, c1.r)
  410. // Texture 1: (c1.g, c1.b, c2.r, c2.g)
  411. // Texture 2: (c2.b, c3.r, c3.g, c3.b)
  412. // Texture 3: (c4.r, c4.g, c4.b, c5.r)
  413. // Texture 4: (c5.g, c5.b, c6.r, c6.g)
  414. // Texture 5: (c6.b, c7.r, c7.g, c7.b)
  415. // Texture 6: (c8.r, c8.g, c8.b, 0.0)
  416. const repackVertexShader = /* glsl */`
  417. void main() {
  418. gl_Position = vec4( position.xy, 0.0, 1.0 );
  419. }
  420. `;
  421. _repackMaterials = [];
  422. for ( let t = 0; t < 7; t ++ ) {
  423. _repackMaterials[ t ] = new ShaderMaterial( {
  424. precision: 'highp',
  425. defines: {
  426. TEXTURE_INDEX: t
  427. },
  428. uniforms: {
  429. batchTexture: { value: null },
  430. resolution: { value: new Vector3() },
  431. sliceZ: { value: 0 }
  432. },
  433. vertexShader: repackVertexShader,
  434. fragmentShader: /* glsl */`
  435. uniform sampler2D batchTexture;
  436. uniform vec3 resolution;
  437. uniform int sliceZ;
  438. void main() {
  439. int ix = int( gl_FragCoord.x );
  440. int iy = int( gl_FragCoord.y );
  441. int iz = sliceZ;
  442. int probeIndex = ix + iy * int( resolution.x ) + iz * int( resolution.x ) * int( resolution.y );
  443. // Read 9 SH coefficients from the batch texture row
  444. vec4 c0 = texelFetch( batchTexture, ivec2( 0, probeIndex ), 0 );
  445. vec4 c1 = texelFetch( batchTexture, ivec2( 1, probeIndex ), 0 );
  446. vec4 c2 = texelFetch( batchTexture, ivec2( 2, probeIndex ), 0 );
  447. vec4 c3 = texelFetch( batchTexture, ivec2( 3, probeIndex ), 0 );
  448. vec4 c4 = texelFetch( batchTexture, ivec2( 4, probeIndex ), 0 );
  449. vec4 c5 = texelFetch( batchTexture, ivec2( 5, probeIndex ), 0 );
  450. vec4 c6 = texelFetch( batchTexture, ivec2( 6, probeIndex ), 0 );
  451. vec4 c7 = texelFetch( batchTexture, ivec2( 7, probeIndex ), 0 );
  452. vec4 c8 = texelFetch( batchTexture, ivec2( 8, probeIndex ), 0 );
  453. // Pack into the output format for this texture index
  454. #if TEXTURE_INDEX == 0
  455. gl_FragColor = vec4( c0.rgb, c1.r );
  456. #elif TEXTURE_INDEX == 1
  457. gl_FragColor = vec4( c1.gb, c2.rg );
  458. #elif TEXTURE_INDEX == 2
  459. gl_FragColor = vec4( c2.b, c3.rgb );
  460. #elif TEXTURE_INDEX == 3
  461. gl_FragColor = vec4( c4.rgb, c5.r );
  462. #elif TEXTURE_INDEX == 4
  463. gl_FragColor = vec4( c5.gb, c6.rg );
  464. #elif TEXTURE_INDEX == 5
  465. gl_FragColor = vec4( c6.b, c7.rgb );
  466. #else
  467. gl_FragColor = vec4( c8.rgb, 0.0 );
  468. #endif
  469. }
  470. `
  471. } );
  472. }
  473. }
  474. // Internal: Ensure cube render target and camera exist with the right parameters
  475. function _ensureBakeResources( options ) {
  476. const {
  477. cubemapSize = 8,
  478. near = 0.1,
  479. far = 100
  480. } = options;
  481. if ( _cubeRenderTarget === null || cubemapSize !== _cachedCubemapSize || near !== _cachedNear || far !== _cachedFar ) {
  482. if ( _cubeRenderTarget !== null ) _cubeRenderTarget.dispose();
  483. _cubeRenderTarget = new WebGLCubeRenderTarget( cubemapSize, { type: HalfFloatType } );
  484. _cubeCamera = new CubeCamera( near, far, _cubeRenderTarget );
  485. _cachedCubemapSize = cubemapSize;
  486. _cachedNear = near;
  487. _cachedFar = far;
  488. }
  489. _ensureGPUResources( cubemapSize );
  490. return { cubeRenderTarget: _cubeRenderTarget, cubeCamera: _cubeCamera };
  491. }
  492. function _ensureBatchTarget( totalProbes ) {
  493. if ( _batchTarget === null || _batchTargetProbes !== totalProbes ) {
  494. if ( _batchTarget !== null ) _batchTarget.dispose();
  495. _batchTarget = new WebGLRenderTarget( 9, totalProbes, {
  496. type: FloatType,
  497. minFilter: NearestFilter,
  498. magFilter: NearestFilter,
  499. depthBuffer: false
  500. } );
  501. _batchTargetProbes = totalProbes;
  502. }
  503. return _batchTarget;
  504. }
  505. export { LightProbeGrid };
粤ICP备19079148号