LightProbeGrid.js 18 KB

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