From 626d40aeeb013100a27dc95531b1fba39b9d4ea8 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Thu, 5 Mar 2026 17:37:24 +0900 Subject: [PATCH 01/22] IrradianceProbeGrid: Add position-dependent diffuse GI GPU-resident L1 SH probe grid with hardware trilinear interpolation. Cubemap rendering, SH projection and texture packing run entirely on the GPU with zero CPU readback. Co-Authored-By: Claude Opus 4.6 --- examples/jsm/lighting/IrradianceProbeGrid.js | 514 ++++++++++++++++++ src/renderers/WebGLRenderer.js | 28 + src/renderers/shaders/ShaderChunk.js | 2 + ...rradiance_probe_grid_pars_fragment.glsl.js | 46 ++ .../ShaderChunk/lights_fragment_begin.glsl.js | 8 + .../ShaderChunk/lights_pars_begin.glsl.js | 2 + src/renderers/shaders/UniformsLib.js | 9 +- src/renderers/webgl/WebGLProgram.js | 2 + src/renderers/webgl/WebGLPrograms.js | 4 + src/scenes/Scene.js | 11 + 10 files changed, 625 insertions(+), 1 deletion(-) create mode 100644 examples/jsm/lighting/IrradianceProbeGrid.js create mode 100644 src/renderers/shaders/ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js diff --git a/examples/jsm/lighting/IrradianceProbeGrid.js b/examples/jsm/lighting/IrradianceProbeGrid.js new file mode 100644 index 00000000000000..713d5607391f50 --- /dev/null +++ b/examples/jsm/lighting/IrradianceProbeGrid.js @@ -0,0 +1,514 @@ +import { + CubeCamera, + FloatType, + HalfFloatType, + LinearFilter, + Mesh, + NearestFilter, + OrthographicCamera, + PlaneGeometry, + RGBAFormat, + Scene, + ShaderMaterial, + Vector3, + Vector4, + WebGL3DRenderTarget, + WebGLCoordinateSystem, + WebGLCubeRenderTarget, + WebGLRenderTarget +} from 'three'; + +// Shared fullscreen-quad scene / camera +let _scene = null; +let _camera = null; +let _mesh = null; + +// SH projection material (depends on cubemapSize + flip) +let _shMaterial = null; +let _lastCubemapSize = 0; +let _lastFlip = 0; + +// Repack materials (one per output texture) +let _repackMaterials = null; + +// Cached bake resources +let _cubeRenderTarget = null; +let _cubeCamera = null; +let _cachedCubemapSize = 0; +let _cachedNear = 0; +let _cachedFar = 0; + +// Cached batch render target +let _batchTarget = null; +let _batchTargetProbes = 0; + +// Reusable temp objects +const _position = /*@__PURE__*/ new Vector3(); +const _savedViewport = /*@__PURE__*/ new Vector4(); +const _savedScissor = /*@__PURE__*/ new Vector4(); + +/** + * A 3D grid of L1 Spherical Harmonic irradiance probes that provides + * position-dependent diffuse global illumination. The probe data is stored + * in three RGBA `WebGL3DRenderTarget` instances with `LinearFilter` for + * hardware trilinear interpolation. + * + * Baking is fully GPU-resident: cubemap rendering, SH projection, and + * texture packing all happen on the GPU with zero CPU readback. + * + * @three_import import { IrradianceProbeGrid } from 'three/addons/lighting/IrradianceProbeGrid.js'; + */ +class IrradianceProbeGrid { + + /** + * Constructs a new irradiance probe grid. + * + * @param {Box3} boundingBox - The world-space bounding box for the grid. + * @param {Vector3} resolution - The number of probes along each axis (x, y, z). + */ + constructor( boundingBox, resolution ) { + + /** + * The world-space bounding box for the grid. + * @type {Box3} + */ + this.boundingBox = boundingBox.clone(); + + /** + * The number of probes along each axis. + * @type {Vector3} + */ + this.resolution = resolution.clone(); + + /** + * The three RGBA 3D textures storing packed SH coefficients. + * @type {Data3DTexture[]} + */ + this.textures = [ null, null, null ]; + + /** + * Internal render targets for GPU-resident baking. + * @private + */ + this._renderTargets = [ null, null, null ]; + + } + + /** + * Returns the world-space position of the probe at grid indices (ix, iy, iz). + * + * @param {number} ix - X index. + * @param {number} iy - Y index. + * @param {number} iz - Z index. + * @param {Vector3} target - The target vector. + * @return {Vector3} The world-space position. + */ + getProbePosition( ix, iy, iz, target ) { + + const min = this.boundingBox.min; + const max = this.boundingBox.max; + const res = this.resolution; + + target.set( + res.x > 1 ? min.x + ix * ( max.x - min.x ) / ( res.x - 1 ) : ( min.x + max.x ) * 0.5, + res.y > 1 ? min.y + iy * ( max.y - min.y ) / ( res.y - 1 ) : ( min.y + max.y ) * 0.5, + res.z > 1 ? min.z + iz * ( max.z - min.z ) / ( res.z - 1 ) : ( min.z + max.z ) * 0.5 + ); + + return target; + + } + + /** + * Bakes all probes by rendering cubemaps at each probe position + * and projecting to L1 SH. Fully GPU-resident with zero CPU readback. + * + * @param {WebGLRenderer} renderer - The renderer. + * @param {Scene} scene - The scene to render. + * @param {Object} [options] - Bake options. + * @param {number} [options.cubemapSize=8] - Resolution of each cubemap face. + * @param {number} [options.near=0.1] - Near plane for the cube camera. + * @param {number} [options.far=100] - Far plane for the cube camera. + */ + bake( renderer, scene, options = {} ) { + + const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( renderer, options ); + + this._ensureTextures(); + + // Prevent feedback: temporarily remove the probe grid from the scene + const savedGrid = scene.irradianceProbeGrid; + scene.irradianceProbeGrid = null; + + const res = this.resolution; + const totalProbes = res.x * res.y * res.z; + + // Batch render target for SH coefficients: 4 pixels wide, one row per probe + const batchTarget = _ensureBatchTarget( totalProbes ); + + // Save renderer state + const savedRenderTarget = renderer.getRenderTarget(); + renderer.getViewport( _savedViewport ); + renderer.getScissor( _savedScissor ); + const savedScissorTest = renderer.getScissorTest(); + + // Clear pooled batch target so skipped probes read as zero + batchTarget.scissorTest = false; + batchTarget.viewport.set( 0, 0, 4, totalProbes ); + renderer.setRenderTarget( batchTarget ); + renderer.clear(); + + const t0 = performance.now(); + + // Phase 1: Render cubemaps and project to SH into batch target + // Note: set viewport/scissor on the render target directly to avoid pixel ratio scaling + batchTarget.scissorTest = true; + + // Disable shadow map auto-update during bake — lights don't move between probes. + // Force one shadow update on the first render so maps are initialized. + const savedShadowAutoUpdate = renderer.shadowMap.autoUpdate; + renderer.shadowMap.autoUpdate = false; + renderer.shadowMap.needsUpdate = true; + + for ( let iz = 0; iz < res.z; iz ++ ) { + + for ( let iy = 0; iy < res.y; iy ++ ) { + + for ( let ix = 0; ix < res.x; ix ++ ) { + + const probeIndex = ix + iy * res.x + iz * res.x * res.y; + + this.getProbePosition( ix, iy, iz, _position ); + cubeCamera.position.copy( _position ); + cubeCamera.update( renderer, scene ); + + // SH projection + _shMaterial.uniforms.envMap.value = cubeRenderTarget.texture; + _mesh.material = _shMaterial; + batchTarget.viewport.set( 0, probeIndex, 4, 1 ); + batchTarget.scissor.set( 0, probeIndex, 4, 1 ); + renderer.setRenderTarget( batchTarget ); + renderer.render( _scene, _camera ); + + } + + } + + } + + renderer.shadowMap.autoUpdate = savedShadowAutoUpdate; + + // Phase 2: Repack SH data from batch target into 3D textures (GPU-to-GPU) + _ensureRepackResources(); + + for ( let t = 0; t < 3; t ++ ) { + + _repackMaterials[ t ].uniforms.batchTexture.value = batchTarget.texture; + _repackMaterials[ t ].uniforms.resolution.value.copy( res ); + + const rt = this._renderTargets[ t ]; + rt.scissorTest = false; + rt.viewport.set( 0, 0, res.x, res.y ); + + for ( let iz = 0; iz < res.z; iz ++ ) { + + _repackMaterials[ t ].uniforms.sliceZ.value = iz; + _mesh.material = _repackMaterials[ t ]; + renderer.setRenderTarget( rt, iz ); + renderer.render( _scene, _camera ); + + } + + } + + // Restore renderer state + renderer.setRenderTarget( savedRenderTarget ); + renderer.setViewport( _savedViewport ); + renderer.setScissor( _savedScissor ); + renderer.setScissorTest( savedScissorTest ); + + console.log( `IrradianceProbeGrid: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` ); + + scene.irradianceProbeGrid = savedGrid; + + } + + /** + * Ensures the 3D render target textures exist with the correct dimensions. + * @private + */ + _ensureTextures() { + + const res = this.resolution; + const nx = res.x, ny = res.y, nz = res.z; + + for ( let t = 0; t < 3; t ++ ) { + + if ( this._renderTargets[ t ] !== null ) continue; + + const rt = new WebGL3DRenderTarget( nx, ny, nz, { + format: RGBAFormat, + type: FloatType, + minFilter: LinearFilter, + magFilter: LinearFilter, + generateMipmaps: false, + depthBuffer: false + } ); + + this._renderTargets[ t ] = rt; + this.textures[ t ] = rt.texture; + + } + + } + + /** + * Frees GPU resources. + */ + dispose() { + + for ( let t = 0; t < 3; t ++ ) { + + if ( this._renderTargets[ t ] !== null ) { + + this._renderTargets[ t ].dispose(); + this._renderTargets[ t ] = null; + this.textures[ t ] = null; + + } + + } + + } + +} + +// Internal: Ensure the shared fullscreen-quad scene exists +function _ensureScene() { + + if ( _scene === null ) { + + _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); + _mesh = new Mesh( new PlaneGeometry( 2, 2 ) ); + _scene = new Scene(); + _scene.add( _mesh ); + + } + +} + +// Internal: Ensure GPU resources for SH projection are created +function _ensureGPUResources( cubemapSize, flip ) { + + _ensureScene(); + + // Recreate material when cubemap size or flip changes + if ( cubemapSize !== _lastCubemapSize || flip !== _lastFlip ) { + + if ( _shMaterial !== null ) _shMaterial.dispose(); + + _shMaterial = new ShaderMaterial( { + defines: { + CUBEMAP_SIZE: cubemapSize, + FLIP: flip.toFixed( 1 ) + }, + uniforms: { + envMap: { value: null } + }, + vertexShader: /* glsl */` + void main() { + gl_Position = vec4( position.xy, 0.0, 1.0 ); + } + `, + fragmentShader: /* glsl */` + precision highp float; + uniform samplerCube envMap; + + void main() { + + int coefIndex = int( gl_FragCoord.x ); + + vec3 accum0 = vec3( 0.0 ); + vec3 accum1 = vec3( 0.0 ); + vec3 accum2 = vec3( 0.0 ); + vec3 accum3 = vec3( 0.0 ); + float totalWeight = 0.0; + float pixelSize = 2.0 / float( CUBEMAP_SIZE ); + + for ( int face = 0; face < 6; face ++ ) { + + for ( int iy = 0; iy < CUBEMAP_SIZE; iy ++ ) { + + for ( int ix = 0; ix < CUBEMAP_SIZE; ix ++ ) { + + float col = ( 1.0 - ( float( ix ) + 0.5 ) * pixelSize ) * FLIP; + float row = 1.0 - ( float( iy ) + 0.5 ) * pixelSize; + + vec3 coord; + + if ( face == 0 ) coord = vec3( -1.0 * FLIP, row, col * FLIP ); + else if ( face == 1 ) coord = vec3( 1.0 * FLIP, row, -col * FLIP ); + else if ( face == 2 ) coord = vec3( col, 1.0, -row ); + else if ( face == 3 ) coord = vec3( col, -1.0, row ); + else if ( face == 4 ) coord = vec3( col, row, 1.0 ); + else coord = vec3( -col, row, -1.0 ); + + float lengthSq = dot( coord, coord ); + float weight = 4.0 / ( sqrt( lengthSq ) * lengthSq ); + totalWeight += weight; + + vec3 dir = normalize( coord ); + vec3 cw = textureCube( envMap, coord ).rgb * weight; + + accum0 += cw * 0.282095; + accum1 += cw * ( 0.488603 * dir.y ); + accum2 += cw * ( 0.488603 * dir.z ); + accum3 += cw * ( 0.488603 * dir.x ); + + } + + } + + } + + float norm = 4.0 * 3.14159265359 / totalWeight; + + vec3 accum; + if ( coefIndex == 0 ) accum = accum0; + else if ( coefIndex == 1 ) accum = accum1; + else if ( coefIndex == 2 ) accum = accum2; + else accum = accum3; + + gl_FragColor = vec4( accum * norm, 1.0 ); + + } + ` + } ); + + _lastCubemapSize = cubemapSize; + _lastFlip = flip; + + } + +} + +// Internal: Ensure GPU resources for repacking SH into 3D textures +function _ensureRepackResources() { + + if ( _repackMaterials !== null ) return; + + _ensureScene(); + + // Create 3 materials, one per output texture packing + // Texture 0: (c0.r, c0.g, c0.b, c1.r) + // Texture 1: (c1.g, c1.b, c2.r, c2.g) + // Texture 2: (c2.b, c3.r, c3.g, c3.b) + + const repackVertexShader = /* glsl */` + void main() { + gl_Position = vec4( position.xy, 0.0, 1.0 ); + } + `; + + _repackMaterials = []; + + for ( let t = 0; t < 3; t ++ ) { + + _repackMaterials[ t ] = new ShaderMaterial( { + defines: { + TEXTURE_INDEX: t + }, + uniforms: { + batchTexture: { value: null }, + resolution: { value: new Vector3() }, + sliceZ: { value: 0 } + }, + vertexShader: repackVertexShader, + fragmentShader: /* glsl */` + precision highp float; + uniform sampler2D batchTexture; + uniform vec3 resolution; + uniform int sliceZ; + + void main() { + + int ix = int( gl_FragCoord.x ); + int iy = int( gl_FragCoord.y ); + int iz = sliceZ; + + int probeIndex = ix + iy * int( resolution.x ) + iz * int( resolution.x ) * int( resolution.y ); + + // Read 4 SH coefficients from the batch texture row + vec4 c0 = texelFetch( batchTexture, ivec2( 0, probeIndex ), 0 ); + vec4 c1 = texelFetch( batchTexture, ivec2( 1, probeIndex ), 0 ); + vec4 c2 = texelFetch( batchTexture, ivec2( 2, probeIndex ), 0 ); + vec4 c3 = texelFetch( batchTexture, ivec2( 3, probeIndex ), 0 ); + + // Pack into the output format for this texture index + #if TEXTURE_INDEX == 0 + gl_FragColor = vec4( c0.rgb, c1.r ); + #elif TEXTURE_INDEX == 1 + gl_FragColor = vec4( c1.gb, c2.rg ); + #else + gl_FragColor = vec4( c2.b, c3.rgb ); + #endif + + } + ` + } ); + + } + +} + +// Internal: Ensure cube render target and camera exist with the right parameters +function _ensureBakeResources( renderer, options ) { + + const { + cubemapSize = 8, + near = 0.1, + far = 100 + } = options; + + if ( _cubeRenderTarget === null || cubemapSize !== _cachedCubemapSize || near !== _cachedNear || far !== _cachedFar ) { + + if ( _cubeRenderTarget !== null ) _cubeRenderTarget.dispose(); + + _cubeRenderTarget = new WebGLCubeRenderTarget( cubemapSize, { type: HalfFloatType } ); + _cubeCamera = new CubeCamera( near, far, _cubeRenderTarget ); + _cachedCubemapSize = cubemapSize; + _cachedNear = near; + _cachedFar = far; + + } + + const flip = renderer.coordinateSystem === WebGLCoordinateSystem ? - 1 : 1; + + _ensureGPUResources( cubemapSize, flip ); + + return { cubeRenderTarget: _cubeRenderTarget, cubeCamera: _cubeCamera }; + +} + +function _ensureBatchTarget( totalProbes ) { + + if ( _batchTarget === null || _batchTargetProbes !== totalProbes ) { + + if ( _batchTarget !== null ) _batchTarget.dispose(); + + _batchTarget = new WebGLRenderTarget( 4, totalProbes, { + type: FloatType, + minFilter: NearestFilter, + magFilter: NearestFilter, + depthBuffer: false + } ); + + _batchTargetProbes = totalProbes; + + } + + return _batchTarget; + +} + +export { IrradianceProbeGrid }; diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 797950ed9a7a75..0053003710e143 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -2220,6 +2220,8 @@ class WebGLRenderer { } + materialProperties.irradianceProbeGrid = scene.irradianceProbeGrid; + materialProperties.currentProgram = program; materialProperties.uniformsList = null; @@ -2419,6 +2421,10 @@ class WebGLRenderer { needsProgramChange = true; + } else if ( !! materialProperties.irradianceProbeGrid !== !! scene.irradianceProbeGrid ) { + + needsProgramChange = true; + } } else { @@ -2461,6 +2467,14 @@ class WebGLRenderer { } + if ( materialProperties.needsLights && materialProperties.__probeGrid !== ( scene.irradianceProbeGrid || null ) ) { + + materialProperties.__probeGrid = scene.irradianceProbeGrid || null; + + refreshMaterial = true; + + } + if ( refreshProgram || _currentCamera !== camera ) { // common camera uniforms @@ -2640,6 +2654,20 @@ class WebGLRenderer { materials.refreshMaterialUniforms( m_uniforms, material, _pixelRatio, _height, currentRenderState.state.transmissionRenderTarget[ camera.id ] ); + // irradiance probe grid + + if ( materialProperties.needsLights && scene.irradianceProbeGrid ) { + + const probeGrid = scene.irradianceProbeGrid; + + m_uniforms.probeGridSH0.value = probeGrid.textures[ 0 ]; + m_uniforms.probeGridSH1.value = probeGrid.textures[ 1 ]; + m_uniforms.probeGridSH2.value = probeGrid.textures[ 2 ]; + m_uniforms.probeGridMin.value.copy( probeGrid.boundingBox.min ); + m_uniforms.probeGridMax.value.copy( probeGrid.boundingBox.max ); + + } + WebGLUniforms.upload( _gl, getUniformList( materialProperties ), m_uniforms, textures ); } diff --git a/src/renderers/shaders/ShaderChunk.js b/src/renderers/shaders/ShaderChunk.js index 4d7d78f0d63f67..ccc7c059ca82bc 100644 --- a/src/renderers/shaders/ShaderChunk.js +++ b/src/renderers/shaders/ShaderChunk.js @@ -40,6 +40,7 @@ import fog_pars_vertex from './ShaderChunk/fog_pars_vertex.glsl.js'; import fog_fragment from './ShaderChunk/fog_fragment.glsl.js'; import fog_pars_fragment from './ShaderChunk/fog_pars_fragment.glsl.js'; import gradientmap_pars_fragment from './ShaderChunk/gradientmap_pars_fragment.glsl.js'; +import irradiance_probe_grid_pars_fragment from './ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js'; import lightmap_pars_fragment from './ShaderChunk/lightmap_pars_fragment.glsl.js'; import lights_lambert_fragment from './ShaderChunk/lights_lambert_fragment.glsl.js'; import lights_lambert_pars_fragment from './ShaderChunk/lights_lambert_pars_fragment.glsl.js'; @@ -168,6 +169,7 @@ export const ShaderChunk = { fog_fragment: fog_fragment, fog_pars_fragment: fog_pars_fragment, gradientmap_pars_fragment: gradientmap_pars_fragment, + irradiance_probe_grid_pars_fragment: irradiance_probe_grid_pars_fragment, lightmap_pars_fragment: lightmap_pars_fragment, lights_lambert_fragment: lights_lambert_fragment, lights_lambert_pars_fragment: lights_lambert_pars_fragment, diff --git a/src/renderers/shaders/ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js new file mode 100644 index 00000000000000..16f83d8eb2117d --- /dev/null +++ b/src/renderers/shaders/ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js @@ -0,0 +1,46 @@ +export default /* glsl */` +#ifdef USE_IRRADIANCE_PROBE_GRID + +uniform highp sampler3D probeGridSH0; +uniform highp sampler3D probeGridSH1; +uniform highp sampler3D probeGridSH2; + +uniform vec3 probeGridMin; +uniform vec3 probeGridMax; + +vec3 getProbeGridIrradiance( vec3 worldPos, vec3 worldNormal ) { + + vec3 texSize = vec3( textureSize( probeGridSH0, 0 ) ); + vec3 texSizeMinusOne = texSize - 1.0; + vec3 gridRange = probeGridMax - probeGridMin; + vec3 probeSpacing = gridRange / texSizeMinusOne; + + // Offset sample position along normal by half a probe spacing + vec3 samplePos = worldPos + worldNormal * probeSpacing * 0.5; + vec3 uvw = clamp( ( samplePos - probeGridMin ) / gridRange, 0.0, 1.0 ); + uvw = uvw * texSizeMinusOne / texSize + 0.5 / texSize; + + vec4 s0 = texture( probeGridSH0, uvw ); + vec4 s1 = texture( probeGridSH1, uvw ); + vec4 s2 = texture( probeGridSH2, uvw ); + + // Unpack 4 vec3 SH L1 coefficients + vec3 c0 = s0.xyz; + vec3 c1 = vec3( s0.w, s1.xy ); + vec3 c2 = vec3( s1.zw, s2.x ); + vec3 c3 = s2.yzw; + + // Evaluate L1 irradiance + float x = worldNormal.x, y = worldNormal.y, z = worldNormal.z; + + vec3 result = c0 * 0.886227; + result += c1 * 2.0 * 0.511664 * y; + result += c2 * 2.0 * 0.511664 * z; + result += c3 * 2.0 * 0.511664 * x; + + return max( result, vec3( 0.0 ) ); + +} + +#endif +`; diff --git a/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js index 5c0c609ce164ed..2ceb052ef91f52 100644 --- a/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js +++ b/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js @@ -194,6 +194,14 @@ IncidentLight directLight; #endif + #ifdef USE_IRRADIANCE_PROBE_GRID + + vec3 probeWorldPos = ( inverse( viewMatrix ) * vec4( geometryPosition, 1.0 ) ).xyz; + vec3 probeWorldNormal = inverseTransformDirection( geometryNormal, viewMatrix ); + irradiance += getProbeGridIrradiance( probeWorldPos, probeWorldNormal ); + + #endif + #endif #if defined( RE_IndirectSpecular ) diff --git a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js index 62961a8487c1e2..4e21381f1c8583 100644 --- a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js +++ b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js @@ -211,4 +211,6 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi } #endif + +#include `; diff --git a/src/renderers/shaders/UniformsLib.js b/src/renderers/shaders/UniformsLib.js index 4eb9fb8d761a03..ffcd156951351c 100644 --- a/src/renderers/shaders/UniformsLib.js +++ b/src/renderers/shaders/UniformsLib.js @@ -1,5 +1,6 @@ import { Color } from '../../math/Color.js'; import { Vector2 } from '../../math/Vector2.js'; +import { Vector3 } from '../../math/Vector3.js'; import { Matrix3 } from '../../math/Matrix3.js'; // Uniforms library for shared webgl shaders @@ -191,7 +192,13 @@ const UniformsLib = { } }, ltc_1: { value: null }, - ltc_2: { value: null } + ltc_2: { value: null }, + + probeGridSH0: { value: null }, + probeGridSH1: { value: null }, + probeGridSH2: { value: null }, + probeGridMin: { value: /*@__PURE__*/ new Vector3() }, + probeGridMax: { value: /*@__PURE__*/ new Vector3() } }, diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js index c5676eebf58a15..b56b72085de792 100644 --- a/src/renderers/webgl/WebGLProgram.js +++ b/src/renderers/webgl/WebGLProgram.js @@ -755,6 +755,8 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { parameters.numLightProbes > 0 ? '#define USE_LIGHT_PROBES' : '', + parameters.irradianceProbeGrid ? '#define USE_IRRADIANCE_PROBE_GRID' : '', + parameters.decodeVideoTexture ? '#define DECODE_VIDEO_TEXTURE' : '', parameters.decodeVideoTextureEmissive ? '#define DECODE_VIDEO_TEXTURE_EMISSIVE' : '', diff --git a/src/renderers/webgl/WebGLPrograms.js b/src/renderers/webgl/WebGLPrograms.js index 31aae9c3498fd6..1db04ff0485c0a 100644 --- a/src/renderers/webgl/WebGLPrograms.js +++ b/src/renderers/webgl/WebGLPrograms.js @@ -343,6 +343,8 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin numLightProbes: lights.numLightProbes, + irradianceProbeGrid: !! scene.irradianceProbeGrid, + numClippingPlanes: clipping.numPlanes, numClipIntersection: clipping.numIntersection, @@ -578,6 +580,8 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin _programLayers.enable( 20 ); if ( parameters.alphaToCoverage ) _programLayers.enable( 21 ); + if ( parameters.irradianceProbeGrid ) + _programLayers.enable( 22 ); array.push( _programLayers.mask ); diff --git a/src/scenes/Scene.js b/src/scenes/Scene.js index 151b76fe4810e7..29d346ddc64788 100644 --- a/src/scenes/Scene.js +++ b/src/scenes/Scene.js @@ -103,6 +103,15 @@ class Scene extends Object3D { */ this.environmentRotation = new Euler(); + /** + * An optional irradiance probe grid that provides position-dependent + * diffuse global illumination via a 3D grid of L1 SH probes. + * + * @type {?IrradianceProbeGrid} + * @default null + */ + this.irradianceProbeGrid = null; + /** * Forces everything in the scene to be rendered with the defined material. It is possible * to exclude materials from override by setting {@link Material#allowOverride} to `false`. @@ -135,6 +144,8 @@ class Scene extends Object3D { this.environmentIntensity = source.environmentIntensity; this.environmentRotation.copy( source.environmentRotation ); + this.irradianceProbeGrid = source.irradianceProbeGrid; + if ( source.overrideMaterial !== null ) this.overrideMaterial = source.overrideMaterial.clone(); this.matrixAutoUpdate = source.matrixAutoUpdate; From 71505181e5c121f3aa5d1783196d86a9cf8bd88e Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Thu, 5 Mar 2026 17:37:34 +0900 Subject: [PATCH 02/22] IrradianceProbeGrid: Add examples Co-Authored-By: Claude Opus 4.6 --- .../jsm/helpers/IrradianceProbeGridHelper.js | 192 +++++++++ .../webgl_lights_irradiance_probe_grid.html | 230 +++++++++++ ..._lights_irradiance_probe_grid_complex.html | 295 ++++++++++++++ ...l_lights_irradiance_probe_grid_sponza.html | 366 ++++++++++++++++++ ...ebgl_lights_irradiance_probe_grid_usd.html | 210 ++++++++++ 5 files changed, 1293 insertions(+) create mode 100644 examples/jsm/helpers/IrradianceProbeGridHelper.js create mode 100644 examples/webgl_lights_irradiance_probe_grid.html create mode 100644 examples/webgl_lights_irradiance_probe_grid_complex.html create mode 100644 examples/webgl_lights_irradiance_probe_grid_sponza.html create mode 100644 examples/webgl_lights_irradiance_probe_grid_usd.html diff --git a/examples/jsm/helpers/IrradianceProbeGridHelper.js b/examples/jsm/helpers/IrradianceProbeGridHelper.js new file mode 100644 index 00000000000000..034949a3fad777 --- /dev/null +++ b/examples/jsm/helpers/IrradianceProbeGridHelper.js @@ -0,0 +1,192 @@ +import { + InstancedBufferAttribute, + InstancedMesh, + Matrix4, + ShaderMaterial, + SphereGeometry, + Vector3 +} from 'three'; + +/** + * Visualizes an {@link IrradianceProbeGrid} by rendering a sphere at each + * probe position, shaded with the probe's L1 spherical harmonics. + * + * Uses a single `InstancedMesh` draw call for all probes. + * + * ```js + * const helper = new IrradianceProbeGridHelper( probeGrid ); + * scene.add( helper ); + * ``` + * + * @augments InstancedMesh + * @three_import import { IrradianceProbeGridHelper } from 'three/addons/helpers/IrradianceProbeGridHelper.js'; + */ +class IrradianceProbeGridHelper extends InstancedMesh { + + /** + * Constructs a new irradiance probe grid helper. + * + * @param {IrradianceProbeGrid} probeGrid - The probe grid to visualize. + * @param {number} [sphereSize=0.12] - The radius of each probe sphere. + */ + constructor( probeGrid, sphereSize = 0.12 ) { + + const geometry = new SphereGeometry( sphereSize, 16, 16 ); + + const material = new ShaderMaterial( { + + uniforms: { + + probeGridSH0: { value: null }, + probeGridSH1: { value: null }, + probeGridSH2: { value: null } + + }, + + vertexShader: /* glsl */` + + attribute vec3 instanceUVW; + + varying vec3 vWorldNormal; + varying vec3 vUVW; + + void main() { + + vUVW = instanceUVW; + vWorldNormal = normalize( mat3( modelMatrix ) * normal ); + gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4( position, 1.0 ); + + } + + `, + + fragmentShader: /* glsl */` + + precision highp sampler3D; + + uniform sampler3D probeGridSH0; + uniform sampler3D probeGridSH1; + uniform sampler3D probeGridSH2; + + varying vec3 vWorldNormal; + varying vec3 vUVW; + + void main() { + + vec4 s0 = texture( probeGridSH0, vUVW ); + vec4 s1 = texture( probeGridSH1, vUVW ); + vec4 s2 = texture( probeGridSH2, vUVW ); + + vec3 c0 = s0.rgb; + vec3 c1 = vec3( s0.a, s1.rg ); + vec3 c2 = vec3( s1.ba, s2.r ); + vec3 c3 = s2.gba; + + vec3 n = normalize( vWorldNormal ); + + vec3 result = c0 * 0.886227; + result += c1 * 2.0 * 0.511664 * n.y; + result += c2 * 2.0 * 0.511664 * n.z; + result += c3 * 2.0 * 0.511664 * n.x; + + gl_FragColor = vec4( max( result, vec3( 0.0 ) ), 1.0 ); + + } + + ` + + } ); + + const res = probeGrid.resolution; + const count = res.x * res.y * res.z; + + super( geometry, material, count ); + + /** + * The probe grid to visualize. + * + * @type {IrradianceProbeGrid} + */ + this.probeGrid = probeGrid; + + this.type = 'IrradianceProbeGridHelper'; + + this.update(); + + } + + /** + * Rebuilds instance matrices and UVW attributes from the current probe grid. + * Call this after changing `probeGrid` or after re-baking. + */ + update() { + + const probeGrid = this.probeGrid; + const res = probeGrid.resolution; + const count = res.x * res.y * res.z; + + // Resize instance matrix buffer if needed + + if ( this.instanceMatrix.count !== count ) { + + this.instanceMatrix = new InstancedBufferAttribute( new Float32Array( count * 16 ), 16 ); + + } + + this.count = count; + + const uvwArray = new Float32Array( count * 3 ); + const matrix = new Matrix4(); + const probePos = new Vector3(); + + let i = 0; + + for ( let iz = 0; iz < res.z; iz ++ ) { + + for ( let iy = 0; iy < res.y; iy ++ ) { + + for ( let ix = 0; ix < res.x; ix ++ ) { + + // Remap to texel centers (must match irradiance_probe_grid_pars_fragment.glsl.js) + uvwArray[ i * 3 ] = ( ix + 0.5 ) / res.x; + uvwArray[ i * 3 + 1 ] = ( iy + 0.5 ) / res.y; + uvwArray[ i * 3 + 2 ] = ( iz + 0.5 ) / res.z; + + probeGrid.getProbePosition( ix, iy, iz, probePos ); + matrix.makeTranslation( probePos.x, probePos.y, probePos.z ); + this.setMatrixAt( i, matrix ); + + i ++; + + } + + } + + } + + this.instanceMatrix.needsUpdate = true; + + this.geometry.setAttribute( 'instanceUVW', new InstancedBufferAttribute( uvwArray, 3 ) ); + + // Update texture uniforms + + this.material.uniforms.probeGridSH0.value = probeGrid.textures[ 0 ]; + this.material.uniforms.probeGridSH1.value = probeGrid.textures[ 1 ]; + this.material.uniforms.probeGridSH2.value = probeGrid.textures[ 2 ]; + + } + + /** + * Frees the GPU-related resources allocated by this instance. Call this + * method whenever this instance is no longer used in your app. + */ + dispose() { + + this.geometry.dispose(); + this.material.dispose(); + + } + +} + +export { IrradianceProbeGridHelper }; diff --git a/examples/webgl_lights_irradiance_probe_grid.html b/examples/webgl_lights_irradiance_probe_grid.html new file mode 100644 index 00000000000000..bb05d9f30c297b --- /dev/null +++ b/examples/webgl_lights_irradiance_probe_grid.html @@ -0,0 +1,230 @@ + + + + three.js webgl - irradiance probe grid + + + + + + +
+ three.js - irradiance probe grid
+ Position-dependent diffuse global illumination via L1 SH probe grid +
+ + + + + + diff --git a/examples/webgl_lights_irradiance_probe_grid_complex.html b/examples/webgl_lights_irradiance_probe_grid_complex.html new file mode 100644 index 00000000000000..80c832139ebc51 --- /dev/null +++ b/examples/webgl_lights_irradiance_probe_grid_complex.html @@ -0,0 +1,295 @@ + + + + three.js webgl - irradiance probe grid + + + + + + +
+ three.js - irradiance probe grid
+ Position-dependent diffuse global illumination via L1 SH probe grid +
+ + + + + + diff --git a/examples/webgl_lights_irradiance_probe_grid_sponza.html b/examples/webgl_lights_irradiance_probe_grid_sponza.html new file mode 100644 index 00000000000000..c1726b9c88e118 --- /dev/null +++ b/examples/webgl_lights_irradiance_probe_grid_sponza.html @@ -0,0 +1,366 @@ + + + + three.js webgl - irradiance probe grid (Sponza) + + + + + + +
+ three.js - irradiance probe grid (Sponza)
+ WASD to move, mouse to look +
+ + + + + + diff --git a/examples/webgl_lights_irradiance_probe_grid_usd.html b/examples/webgl_lights_irradiance_probe_grid_usd.html new file mode 100644 index 00000000000000..b6c9e4bab982c0 --- /dev/null +++ b/examples/webgl_lights_irradiance_probe_grid_usd.html @@ -0,0 +1,210 @@ + + + + three.js webgl - irradiance probe grid (USD) + + + + + + +
+ three.js - irradiance probe grid (USD)
+ WASD to move, mouse to look +
+ + + + + + From 1d3a6bae370f2ce29ac6f638d33a691647808070 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Thu, 5 Mar 2026 18:13:41 +0900 Subject: [PATCH 03/22] Rename IrradianceProbeGrid to LightProbeVolume Co-Authored-By: Claude Opus 4.6 --- ...ridHelper.js => LightProbeVolumeHelper.js} | 18 +++++++-------- ...dianceProbeGrid.js => LightProbeVolume.js} | 14 ++++++------ ...probe_grid.html => webgl_lightprobes.html} | 18 +++++++-------- ...ex.html => webgl_lightprobes_complex.html} | 18 +++++++-------- ...nza.html => webgl_lightprobes_sponza.html} | 18 +++++++-------- ...id_usd.html => webgl_lightprobes_usd.html} | 18 +++++++-------- src/renderers/WebGLRenderer.js | 22 +++++++++---------- src/renderers/shaders/ShaderChunk.js | 4 ++-- ... light_probe_volume_pars_fragment.glsl.js} | 4 ++-- .../ShaderChunk/lights_fragment_begin.glsl.js | 4 ++-- .../ShaderChunk/lights_pars_begin.glsl.js | 2 +- src/renderers/webgl/WebGLProgram.js | 2 +- src/renderers/webgl/WebGLPrograms.js | 4 ++-- src/scenes/Scene.js | 8 +++---- 14 files changed, 77 insertions(+), 77 deletions(-) rename examples/jsm/helpers/{IrradianceProbeGridHelper.js => LightProbeVolumeHelper.js} (86%) rename examples/jsm/lighting/{IrradianceProbeGrid.js => LightProbeVolume.js} (97%) rename examples/{webgl_lights_irradiance_probe_grid.html => webgl_lightprobes.html} (90%) rename examples/{webgl_lights_irradiance_probe_grid_complex.html => webgl_lightprobes_complex.html} (92%) rename examples/{webgl_lights_irradiance_probe_grid_sponza.html => webgl_lightprobes_sponza.html} (93%) rename examples/{webgl_lights_irradiance_probe_grid_usd.html => webgl_lightprobes_usd.html} (88%) rename src/renderers/shaders/ShaderChunk/{irradiance_probe_grid_pars_fragment.glsl.js => light_probe_volume_pars_fragment.glsl.js} (92%) diff --git a/examples/jsm/helpers/IrradianceProbeGridHelper.js b/examples/jsm/helpers/LightProbeVolumeHelper.js similarity index 86% rename from examples/jsm/helpers/IrradianceProbeGridHelper.js rename to examples/jsm/helpers/LightProbeVolumeHelper.js index 034949a3fad777..bb40da0e67d0bb 100644 --- a/examples/jsm/helpers/IrradianceProbeGridHelper.js +++ b/examples/jsm/helpers/LightProbeVolumeHelper.js @@ -8,25 +8,25 @@ import { } from 'three'; /** - * Visualizes an {@link IrradianceProbeGrid} by rendering a sphere at each + * Visualizes an {@link LightProbeVolume} by rendering a sphere at each * probe position, shaded with the probe's L1 spherical harmonics. * * Uses a single `InstancedMesh` draw call for all probes. * * ```js - * const helper = new IrradianceProbeGridHelper( probeGrid ); + * const helper = new LightProbeVolumeHelper( probeGrid ); * scene.add( helper ); * ``` * * @augments InstancedMesh - * @three_import import { IrradianceProbeGridHelper } from 'three/addons/helpers/IrradianceProbeGridHelper.js'; + * @three_import import { LightProbeVolumeHelper } from 'three/addons/helpers/LightProbeVolumeHelper.js'; */ -class IrradianceProbeGridHelper extends InstancedMesh { +class LightProbeVolumeHelper extends InstancedMesh { /** * Constructs a new irradiance probe grid helper. * - * @param {IrradianceProbeGrid} probeGrid - The probe grid to visualize. + * @param {LightProbeVolume} probeGrid - The probe grid to visualize. * @param {number} [sphereSize=0.12] - The radius of each probe sphere. */ constructor( probeGrid, sphereSize = 0.12 ) { @@ -105,11 +105,11 @@ class IrradianceProbeGridHelper extends InstancedMesh { /** * The probe grid to visualize. * - * @type {IrradianceProbeGrid} + * @type {LightProbeVolume} */ this.probeGrid = probeGrid; - this.type = 'IrradianceProbeGridHelper'; + this.type = 'LightProbeVolumeHelper'; this.update(); @@ -147,7 +147,7 @@ class IrradianceProbeGridHelper extends InstancedMesh { for ( let ix = 0; ix < res.x; ix ++ ) { - // Remap to texel centers (must match irradiance_probe_grid_pars_fragment.glsl.js) + // Remap to texel centers (must match light_probe_volume_pars_fragment.glsl.js) uvwArray[ i * 3 ] = ( ix + 0.5 ) / res.x; uvwArray[ i * 3 + 1 ] = ( iy + 0.5 ) / res.y; uvwArray[ i * 3 + 2 ] = ( iz + 0.5 ) / res.z; @@ -189,4 +189,4 @@ class IrradianceProbeGridHelper extends InstancedMesh { } -export { IrradianceProbeGridHelper }; +export { LightProbeVolumeHelper }; diff --git a/examples/jsm/lighting/IrradianceProbeGrid.js b/examples/jsm/lighting/LightProbeVolume.js similarity index 97% rename from examples/jsm/lighting/IrradianceProbeGrid.js rename to examples/jsm/lighting/LightProbeVolume.js index 713d5607391f50..200931539efc66 100644 --- a/examples/jsm/lighting/IrradianceProbeGrid.js +++ b/examples/jsm/lighting/LightProbeVolume.js @@ -56,9 +56,9 @@ const _savedScissor = /*@__PURE__*/ new Vector4(); * Baking is fully GPU-resident: cubemap rendering, SH projection, and * texture packing all happen on the GPU with zero CPU readback. * - * @three_import import { IrradianceProbeGrid } from 'three/addons/lighting/IrradianceProbeGrid.js'; + * @three_import import { LightProbeVolume } from 'three/addons/lighting/LightProbeVolume.js'; */ -class IrradianceProbeGrid { +class LightProbeVolume { /** * Constructs a new irradiance probe grid. @@ -137,8 +137,8 @@ class IrradianceProbeGrid { this._ensureTextures(); // Prevent feedback: temporarily remove the probe grid from the scene - const savedGrid = scene.irradianceProbeGrid; - scene.irradianceProbeGrid = null; + const savedGrid = scene.lightProbeVolume; + scene.lightProbeVolume = null; const res = this.resolution; const totalProbes = res.x * res.y * res.z; @@ -227,9 +227,9 @@ class IrradianceProbeGrid { renderer.setScissor( _savedScissor ); renderer.setScissorTest( savedScissorTest ); - console.log( `IrradianceProbeGrid: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` ); + console.log( `LightProbeVolume: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` ); - scene.irradianceProbeGrid = savedGrid; + scene.lightProbeVolume = savedGrid; } @@ -511,4 +511,4 @@ function _ensureBatchTarget( totalProbes ) { } -export { IrradianceProbeGrid }; +export { LightProbeVolume }; diff --git a/examples/webgl_lights_irradiance_probe_grid.html b/examples/webgl_lightprobes.html similarity index 90% rename from examples/webgl_lights_irradiance_probe_grid.html rename to examples/webgl_lightprobes.html index bb05d9f30c297b..2d759d53199cbc 100644 --- a/examples/webgl_lights_irradiance_probe_grid.html +++ b/examples/webgl_lightprobes.html @@ -1,7 +1,7 @@ - three.js webgl - irradiance probe grid + three.js webgl - light probe volume @@ -9,7 +9,7 @@
- three.js - irradiance probe grid
+ three.js - light probe volume
Position-dependent diffuse global illumination via L1 SH probe grid
@@ -28,8 +28,8 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; - import { IrradianceProbeGrid } from 'three/addons/lighting/IrradianceProbeGrid.js'; - import { IrradianceProbeGridHelper } from 'three/addons/helpers/IrradianceProbeGridHelper.js'; + import { LightProbeVolume } from 'three/addons/lighting/LightProbeVolume.js'; + import { LightProbeVolumeHelper } from 'three/addons/helpers/LightProbeVolumeHelper.js'; let camera, scene, renderer, controls; let probeGrid, probeGridHelper; @@ -144,7 +144,7 @@ controls.target.set( 0, 2, 0 ); controls.update(); - // Bake irradiance probe grid + // Bake light probe volume const bounds = new THREE.Box3( new THREE.Vector3( - 2.8, 0.1, - 2.8 ), @@ -155,16 +155,16 @@ if ( probeGrid ) probeGrid.dispose(); - probeGrid = new IrradianceProbeGrid( bounds, new THREE.Vector3( resolution, resolution, resolution ) ); + probeGrid = new LightProbeVolume( bounds, new THREE.Vector3( resolution, resolution, resolution ) ); probeGrid.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } ); - scene.irradianceProbeGrid = params.enabled ? probeGrid : null; + scene.lightProbeVolume = params.enabled ? probeGrid : null; // Update debug visualization if ( ! probeGridHelper ) { - probeGridHelper = new IrradianceProbeGridHelper( probeGrid ); + probeGridHelper = new LightProbeVolumeHelper( probeGrid ); probeGridHelper.visible = params.showProbes; scene.add( probeGridHelper ); @@ -190,7 +190,7 @@ const gui = new GUI(); gui.add( params, 'enabled' ).name( 'GI' ).onChange( ( value ) => { - scene.irradianceProbeGrid = value ? probeGrid : null; + scene.lightProbeVolume = value ? probeGrid : null; } ); gui.add( params, 'resolution', 2, 12, 1 ).name( 'Resolution' ).onFinishChange( ( value ) => { diff --git a/examples/webgl_lights_irradiance_probe_grid_complex.html b/examples/webgl_lightprobes_complex.html similarity index 92% rename from examples/webgl_lights_irradiance_probe_grid_complex.html rename to examples/webgl_lightprobes_complex.html index 80c832139ebc51..93ac815fe67fcf 100644 --- a/examples/webgl_lights_irradiance_probe_grid_complex.html +++ b/examples/webgl_lightprobes_complex.html @@ -1,7 +1,7 @@ - three.js webgl - irradiance probe grid + three.js webgl - light probe volume @@ -9,7 +9,7 @@
- three.js - irradiance probe grid
+ three.js - light probe volume
Position-dependent diffuse global illumination via L1 SH probe grid
@@ -28,8 +28,8 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; - import { IrradianceProbeGrid } from 'three/addons/lighting/IrradianceProbeGrid.js'; - import { IrradianceProbeGridHelper } from 'three/addons/helpers/IrradianceProbeGridHelper.js'; + import { LightProbeVolume } from 'three/addons/lighting/LightProbeVolume.js'; + import { LightProbeVolumeHelper } from 'three/addons/helpers/LightProbeVolumeHelper.js'; let camera, scene, renderer, controls; let probeGrid, probeGridHelper; @@ -209,7 +209,7 @@ controls.target.set( 0, 2, 0 ); controls.update(); - // Bake irradiance probe grid + // Bake light probe volume const bounds = new THREE.Box3( new THREE.Vector3( - 3.8, 0.1, - 3.8 ), @@ -220,16 +220,16 @@ if ( probeGrid ) probeGrid.dispose(); - probeGrid = new IrradianceProbeGrid( bounds, new THREE.Vector3( resolution, resolution, resolution ) ); + probeGrid = new LightProbeVolume( bounds, new THREE.Vector3( resolution, resolution, resolution ) ); probeGrid.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } ); - scene.irradianceProbeGrid = params.enabled ? probeGrid : null; + scene.lightProbeVolume = params.enabled ? probeGrid : null; // Update debug visualization if ( ! probeGridHelper ) { - probeGridHelper = new IrradianceProbeGridHelper( probeGrid ); + probeGridHelper = new LightProbeVolumeHelper( probeGrid ); probeGridHelper.visible = params.showProbes; scene.add( probeGridHelper ); @@ -255,7 +255,7 @@ const gui = new GUI(); gui.add( params, 'enabled' ).name( 'GI' ).onChange( ( value ) => { - scene.irradianceProbeGrid = value ? probeGrid : null; + scene.lightProbeVolume = value ? probeGrid : null; } ); gui.add( params, 'resolution', 2, 12, 1 ).name( 'Resolution' ).onFinishChange( ( value ) => { diff --git a/examples/webgl_lights_irradiance_probe_grid_sponza.html b/examples/webgl_lightprobes_sponza.html similarity index 93% rename from examples/webgl_lights_irradiance_probe_grid_sponza.html rename to examples/webgl_lightprobes_sponza.html index c1726b9c88e118..1c1c005209b586 100644 --- a/examples/webgl_lights_irradiance_probe_grid_sponza.html +++ b/examples/webgl_lightprobes_sponza.html @@ -1,7 +1,7 @@ - three.js webgl - irradiance probe grid (Sponza) + three.js webgl - light probe volume (Sponza) @@ -9,7 +9,7 @@
- three.js - irradiance probe grid (Sponza)
+ three.js - light probe volume (Sponza)
WASD to move, mouse to look
@@ -30,8 +30,8 @@ import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { Sky } from 'three/addons/objects/Sky.js'; - import { IrradianceProbeGrid } from 'three/addons/lighting/IrradianceProbeGrid.js'; - import { IrradianceProbeGridHelper } from 'three/addons/helpers/IrradianceProbeGridHelper.js'; + import { LightProbeVolume } from 'three/addons/lighting/LightProbeVolume.js'; + import { LightProbeVolumeHelper } from 'three/addons/helpers/LightProbeVolumeHelper.js'; const MODEL_INDEX_URL = 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/model-index.json'; const SAMPLE_ASSETS_BASE_URL = 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/'; @@ -210,7 +210,7 @@ probeBounds.min.copy( center ).sub( halfSize ); probeBounds.max.copy( center ).add( halfSize ); - probeGrid = new IrradianceProbeGrid( + probeGrid = new LightProbeVolume( probeBounds, new THREE.Vector3( params.countX, params.countY, params.countZ ) ); @@ -220,11 +220,11 @@ far: probeFar } ); - scene.irradianceProbeGrid = params.enabled ? probeGrid : null; + scene.lightProbeVolume = params.enabled ? probeGrid : null; if ( ! probeGridHelper ) { - probeGridHelper = new IrradianceProbeGridHelper( probeGrid, params.probeSize ); + probeGridHelper = new LightProbeVolumeHelper( probeGrid, params.probeSize ); probeGridHelper.visible = params.showProbes; scene.add( probeGridHelper ); @@ -247,7 +247,7 @@ const gui = new GUI(); gui.add( params, 'enabled' ).name( 'GI' ).onChange( ( value ) => { - scene.irradianceProbeGrid = value ? probeGrid : null; + scene.lightProbeVolume = value ? probeGrid : null; } ); @@ -287,7 +287,7 @@ scene.remove( probeGridHelper ); probeGridHelper.dispose(); - probeGridHelper = new IrradianceProbeGridHelper( probeGrid, value ); + probeGridHelper = new LightProbeVolumeHelper( probeGrid, value ); probeGridHelper.visible = params.showProbes; scene.add( probeGridHelper ); diff --git a/examples/webgl_lights_irradiance_probe_grid_usd.html b/examples/webgl_lightprobes_usd.html similarity index 88% rename from examples/webgl_lights_irradiance_probe_grid_usd.html rename to examples/webgl_lightprobes_usd.html index b6c9e4bab982c0..581b28aeb8546a 100644 --- a/examples/webgl_lights_irradiance_probe_grid_usd.html +++ b/examples/webgl_lightprobes_usd.html @@ -1,7 +1,7 @@ - three.js webgl - irradiance probe grid (USD) + three.js webgl - light probe volume (USD) @@ -9,7 +9,7 @@
- three.js - irradiance probe grid (USD)
+ three.js - light probe volume (USD)
WASD to move, mouse to look
@@ -29,8 +29,8 @@ import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import { USDLoader } from 'three/addons/loaders/USDLoader.js'; - import { IrradianceProbeGrid } from 'three/addons/lighting/IrradianceProbeGrid.js'; - import { IrradianceProbeGridHelper } from 'three/addons/helpers/IrradianceProbeGridHelper.js'; + import { LightProbeVolume } from 'three/addons/lighting/LightProbeVolume.js'; + import { LightProbeVolumeHelper } from 'three/addons/helpers/LightProbeVolumeHelper.js'; let camera, scene, renderer, controls, timer; let probeGrid, probeGridHelper; @@ -122,7 +122,7 @@ controls.movementSpeed = 10; controls.lookSpeed = 0.1; - // Bake irradiance probe grid + // Bake light probe volume const probeBounds = new THREE.Box3( new THREE.Vector3( - 40, 0, - 18 ), @@ -139,16 +139,16 @@ if ( probeGrid ) probeGrid.dispose(); - probeGrid = new IrradianceProbeGrid( probeBounds, new THREE.Vector3( resolution * 2, resolution, resolution ) ); + probeGrid = new LightProbeVolume( probeBounds, new THREE.Vector3( resolution * 2, resolution, resolution ) ); probeGrid.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 100 } ); - scene.irradianceProbeGrid = params.enabled ? probeGrid : null; + scene.lightProbeVolume = params.enabled ? probeGrid : null; // Update debug visualization if ( ! probeGridHelper ) { - probeGridHelper = new IrradianceProbeGridHelper( probeGrid, 0.3 ); + probeGridHelper = new LightProbeVolumeHelper( probeGrid, 0.3 ); probeGridHelper.visible = params.showProbes; scene.add( probeGridHelper ); @@ -168,7 +168,7 @@ const gui = new GUI(); gui.add( params, 'enabled' ).name( 'GI' ).onChange( ( value ) => { - scene.irradianceProbeGrid = value ? probeGrid : null; + scene.lightProbeVolume = value ? probeGrid : null; } ); gui.add( params, 'resolution', 2, 12, 1 ).name( 'Resolution' ).onFinishChange( ( value ) => { diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 0053003710e143..26e27a15b317e9 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -2220,7 +2220,7 @@ class WebGLRenderer { } - materialProperties.irradianceProbeGrid = scene.irradianceProbeGrid; + materialProperties.lightProbeVolume = scene.lightProbeVolume; materialProperties.currentProgram = program; materialProperties.uniformsList = null; @@ -2421,7 +2421,7 @@ class WebGLRenderer { needsProgramChange = true; - } else if ( !! materialProperties.irradianceProbeGrid !== !! scene.irradianceProbeGrid ) { + } else if ( !! materialProperties.lightProbeVolume !== !! scene.lightProbeVolume ) { needsProgramChange = true; @@ -2467,9 +2467,9 @@ class WebGLRenderer { } - if ( materialProperties.needsLights && materialProperties.__probeGrid !== ( scene.irradianceProbeGrid || null ) ) { + if ( materialProperties.needsLights && materialProperties.__lightProbeVolume !== ( scene.lightProbeVolume || null ) ) { - materialProperties.__probeGrid = scene.irradianceProbeGrid || null; + materialProperties.__lightProbeVolume = scene.lightProbeVolume || null; refreshMaterial = true; @@ -2656,15 +2656,15 @@ class WebGLRenderer { // irradiance probe grid - if ( materialProperties.needsLights && scene.irradianceProbeGrid ) { + if ( materialProperties.needsLights && scene.lightProbeVolume ) { - const probeGrid = scene.irradianceProbeGrid; + const volume = scene.lightProbeVolume; - m_uniforms.probeGridSH0.value = probeGrid.textures[ 0 ]; - m_uniforms.probeGridSH1.value = probeGrid.textures[ 1 ]; - m_uniforms.probeGridSH2.value = probeGrid.textures[ 2 ]; - m_uniforms.probeGridMin.value.copy( probeGrid.boundingBox.min ); - m_uniforms.probeGridMax.value.copy( probeGrid.boundingBox.max ); + m_uniforms.probeGridSH0.value = volume.textures[ 0 ]; + m_uniforms.probeGridSH1.value = volume.textures[ 1 ]; + m_uniforms.probeGridSH2.value = volume.textures[ 2 ]; + m_uniforms.probeGridMin.value.copy( volume.boundingBox.min ); + m_uniforms.probeGridMax.value.copy( volume.boundingBox.max ); } diff --git a/src/renderers/shaders/ShaderChunk.js b/src/renderers/shaders/ShaderChunk.js index ccc7c059ca82bc..8cb217687905d8 100644 --- a/src/renderers/shaders/ShaderChunk.js +++ b/src/renderers/shaders/ShaderChunk.js @@ -40,7 +40,7 @@ import fog_pars_vertex from './ShaderChunk/fog_pars_vertex.glsl.js'; import fog_fragment from './ShaderChunk/fog_fragment.glsl.js'; import fog_pars_fragment from './ShaderChunk/fog_pars_fragment.glsl.js'; import gradientmap_pars_fragment from './ShaderChunk/gradientmap_pars_fragment.glsl.js'; -import irradiance_probe_grid_pars_fragment from './ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js'; +import light_probe_volume_pars_fragment from './ShaderChunk/light_probe_volume_pars_fragment.glsl.js'; import lightmap_pars_fragment from './ShaderChunk/lightmap_pars_fragment.glsl.js'; import lights_lambert_fragment from './ShaderChunk/lights_lambert_fragment.glsl.js'; import lights_lambert_pars_fragment from './ShaderChunk/lights_lambert_pars_fragment.glsl.js'; @@ -169,7 +169,7 @@ export const ShaderChunk = { fog_fragment: fog_fragment, fog_pars_fragment: fog_pars_fragment, gradientmap_pars_fragment: gradientmap_pars_fragment, - irradiance_probe_grid_pars_fragment: irradiance_probe_grid_pars_fragment, + light_probe_volume_pars_fragment: light_probe_volume_pars_fragment, lightmap_pars_fragment: lightmap_pars_fragment, lights_lambert_fragment: lights_lambert_fragment, lights_lambert_pars_fragment: lights_lambert_pars_fragment, diff --git a/src/renderers/shaders/ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js similarity index 92% rename from src/renderers/shaders/ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js rename to src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js index 16f83d8eb2117d..a78189adc502f1 100644 --- a/src/renderers/shaders/ShaderChunk/irradiance_probe_grid_pars_fragment.glsl.js +++ b/src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js @@ -1,5 +1,5 @@ export default /* glsl */` -#ifdef USE_IRRADIANCE_PROBE_GRID +#ifdef USE_LIGHT_PROBE_VOLUME uniform highp sampler3D probeGridSH0; uniform highp sampler3D probeGridSH1; @@ -8,7 +8,7 @@ uniform highp sampler3D probeGridSH2; uniform vec3 probeGridMin; uniform vec3 probeGridMax; -vec3 getProbeGridIrradiance( vec3 worldPos, vec3 worldNormal ) { +vec3 getLightProbeVolumeIrradiance( vec3 worldPos, vec3 worldNormal ) { vec3 texSize = vec3( textureSize( probeGridSH0, 0 ) ); vec3 texSizeMinusOne = texSize - 1.0; diff --git a/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js index 2ceb052ef91f52..a0303d199673ab 100644 --- a/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js +++ b/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js @@ -194,11 +194,11 @@ IncidentLight directLight; #endif - #ifdef USE_IRRADIANCE_PROBE_GRID + #ifdef USE_LIGHT_PROBE_VOLUME vec3 probeWorldPos = ( inverse( viewMatrix ) * vec4( geometryPosition, 1.0 ) ).xyz; vec3 probeWorldNormal = inverseTransformDirection( geometryNormal, viewMatrix ); - irradiance += getProbeGridIrradiance( probeWorldPos, probeWorldNormal ); + irradiance += getLightProbeVolumeIrradiance( probeWorldPos, probeWorldNormal ); #endif diff --git a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js index 4e21381f1c8583..33c292ccb6ca64 100644 --- a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js +++ b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js @@ -212,5 +212,5 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi #endif -#include +#include `; diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js index b56b72085de792..c46a63162023fa 100644 --- a/src/renderers/webgl/WebGLProgram.js +++ b/src/renderers/webgl/WebGLProgram.js @@ -755,7 +755,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { parameters.numLightProbes > 0 ? '#define USE_LIGHT_PROBES' : '', - parameters.irradianceProbeGrid ? '#define USE_IRRADIANCE_PROBE_GRID' : '', + parameters.lightProbeVolume ? '#define USE_LIGHT_PROBE_VOLUME' : '', parameters.decodeVideoTexture ? '#define DECODE_VIDEO_TEXTURE' : '', parameters.decodeVideoTextureEmissive ? '#define DECODE_VIDEO_TEXTURE_EMISSIVE' : '', diff --git a/src/renderers/webgl/WebGLPrograms.js b/src/renderers/webgl/WebGLPrograms.js index 1db04ff0485c0a..cfec1ca31b947c 100644 --- a/src/renderers/webgl/WebGLPrograms.js +++ b/src/renderers/webgl/WebGLPrograms.js @@ -343,7 +343,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin numLightProbes: lights.numLightProbes, - irradianceProbeGrid: !! scene.irradianceProbeGrid, + lightProbeVolume: !! scene.lightProbeVolume, numClippingPlanes: clipping.numPlanes, numClipIntersection: clipping.numIntersection, @@ -580,7 +580,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin _programLayers.enable( 20 ); if ( parameters.alphaToCoverage ) _programLayers.enable( 21 ); - if ( parameters.irradianceProbeGrid ) + if ( parameters.lightProbeVolume ) _programLayers.enable( 22 ); array.push( _programLayers.mask ); diff --git a/src/scenes/Scene.js b/src/scenes/Scene.js index 29d346ddc64788..4d9b7dca6727cc 100644 --- a/src/scenes/Scene.js +++ b/src/scenes/Scene.js @@ -104,13 +104,13 @@ class Scene extends Object3D { this.environmentRotation = new Euler(); /** - * An optional irradiance probe grid that provides position-dependent + * An optional light probe volume that provides position-dependent * diffuse global illumination via a 3D grid of L1 SH probes. * - * @type {?IrradianceProbeGrid} + * @type {?LightProbeVolume} * @default null */ - this.irradianceProbeGrid = null; + this.lightProbeVolume = null; /** * Forces everything in the scene to be rendered with the defined material. It is possible @@ -144,7 +144,7 @@ class Scene extends Object3D { this.environmentIntensity = source.environmentIntensity; this.environmentRotation.copy( source.environmentRotation ); - this.irradianceProbeGrid = source.irradianceProbeGrid; + this.lightProbeVolume = source.lightProbeVolume; if ( source.overrideMaterial !== null ) this.overrideMaterial = source.overrideMaterial.clone(); From 4b5df930923023cfb0bcdd466df714adddde347c Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Fri, 6 Mar 2026 18:11:23 +0900 Subject: [PATCH 04/22] LightProbeVolume: Use scene.add() API Make LightProbeVolume extend Object3D so it can be added to the scene graph with scene.add(), enabling multiple volumes per scene and per-object volume lookup based on bounding box containment. Co-Authored-By: Claude Opus 4.6 --- examples/jsm/lighting/LightProbeVolume.js | 21 +++++++--- examples/webgl_lightprobes.html | 13 ++++-- examples/webgl_lightprobes_complex.html | 13 ++++-- examples/webgl_lightprobes_sponza.html | 13 ++++-- examples/webgl_lightprobes_usd.html | 13 ++++-- src/renderers/WebGLRenderer.js | 51 +++++++++++++++++++---- src/renderers/webgl/WebGLPrograms.js | 4 +- src/renderers/webgl/WebGLRenderStates.js | 12 +++++- src/scenes/Scene.js | 11 ----- 9 files changed, 108 insertions(+), 43 deletions(-) diff --git a/examples/jsm/lighting/LightProbeVolume.js b/examples/jsm/lighting/LightProbeVolume.js index 200931539efc66..aa304a6e639887 100644 --- a/examples/jsm/lighting/LightProbeVolume.js +++ b/examples/jsm/lighting/LightProbeVolume.js @@ -5,6 +5,7 @@ import { LinearFilter, Mesh, NearestFilter, + Object3D, OrthographicCamera, PlaneGeometry, RGBAFormat, @@ -58,7 +59,7 @@ const _savedScissor = /*@__PURE__*/ new Vector4(); * * @three_import import { LightProbeVolume } from 'three/addons/lighting/LightProbeVolume.js'; */ -class LightProbeVolume { +class LightProbeVolume extends Object3D { /** * Constructs a new irradiance probe grid. @@ -68,6 +69,17 @@ class LightProbeVolume { */ constructor( boundingBox, resolution ) { + super(); + + /** + * This flag can be used for type testing. + * + * @type {boolean} + * @readonly + * @default true + */ + this.isLightProbeVolume = true; + /** * The world-space bounding box for the grid. * @type {Box3} @@ -136,9 +148,8 @@ class LightProbeVolume { this._ensureTextures(); - // Prevent feedback: temporarily remove the probe grid from the scene - const savedGrid = scene.lightProbeVolume; - scene.lightProbeVolume = null; + // Prevent feedback: temporarily hide the volume during baking + this.visible = false; const res = this.resolution; const totalProbes = res.x * res.y * res.z; @@ -229,7 +240,7 @@ class LightProbeVolume { console.log( `LightProbeVolume: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` ); - scene.lightProbeVolume = savedGrid; + this.visible = true; } diff --git a/examples/webgl_lightprobes.html b/examples/webgl_lightprobes.html index 2d759d53199cbc..4ce9b66e6256f5 100644 --- a/examples/webgl_lightprobes.html +++ b/examples/webgl_lightprobes.html @@ -153,12 +153,17 @@ async function bakeWithResolution( resolution ) { - if ( probeGrid ) probeGrid.dispose(); + if ( probeGrid ) { + + scene.remove( probeGrid ); + probeGrid.dispose(); + + } probeGrid = new LightProbeVolume( bounds, new THREE.Vector3( resolution, resolution, resolution ) ); probeGrid.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } ); - - scene.lightProbeVolume = params.enabled ? probeGrid : null; + probeGrid.visible = params.enabled; + scene.add( probeGrid ); // Update debug visualization @@ -190,7 +195,7 @@ const gui = new GUI(); gui.add( params, 'enabled' ).name( 'GI' ).onChange( ( value ) => { - scene.lightProbeVolume = value ? probeGrid : null; + probeGrid.visible = value; } ); gui.add( params, 'resolution', 2, 12, 1 ).name( 'Resolution' ).onFinishChange( ( value ) => { diff --git a/examples/webgl_lightprobes_complex.html b/examples/webgl_lightprobes_complex.html index 93ac815fe67fcf..de7543b8fdda63 100644 --- a/examples/webgl_lightprobes_complex.html +++ b/examples/webgl_lightprobes_complex.html @@ -218,12 +218,17 @@ async function bakeWithResolution( resolution ) { - if ( probeGrid ) probeGrid.dispose(); + if ( probeGrid ) { + + scene.remove( probeGrid ); + probeGrid.dispose(); + + } probeGrid = new LightProbeVolume( bounds, new THREE.Vector3( resolution, resolution, resolution ) ); probeGrid.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } ); - - scene.lightProbeVolume = params.enabled ? probeGrid : null; + probeGrid.visible = params.enabled; + scene.add( probeGrid ); // Update debug visualization @@ -255,7 +260,7 @@ const gui = new GUI(); gui.add( params, 'enabled' ).name( 'GI' ).onChange( ( value ) => { - scene.lightProbeVolume = value ? probeGrid : null; + probeGrid.visible = value; } ); gui.add( params, 'resolution', 2, 12, 1 ).name( 'Resolution' ).onFinishChange( ( value ) => { diff --git a/examples/webgl_lightprobes_sponza.html b/examples/webgl_lightprobes_sponza.html index 1c1c005209b586..5182f17971ac38 100644 --- a/examples/webgl_lightprobes_sponza.html +++ b/examples/webgl_lightprobes_sponza.html @@ -203,7 +203,12 @@ bakeQueued = false; - if ( probeGrid ) probeGrid.dispose(); + if ( probeGrid ) { + + scene.remove( probeGrid ); + probeGrid.dispose(); + + } const halfSize = new THREE.Vector3( params.sizeX / 2, params.sizeY / 2, params.sizeZ / 2 ); const center = new THREE.Vector3( params.boundsX, params.boundsY, params.boundsZ ); @@ -219,8 +224,8 @@ near: 0.05, far: probeFar } ); - - scene.lightProbeVolume = params.enabled ? probeGrid : null; + probeGrid.visible = params.enabled; + scene.add( probeGrid ); if ( ! probeGridHelper ) { @@ -247,7 +252,7 @@ const gui = new GUI(); gui.add( params, 'enabled' ).name( 'GI' ).onChange( ( value ) => { - scene.lightProbeVolume = value ? probeGrid : null; + if ( probeGrid ) probeGrid.visible = value; } ); diff --git a/examples/webgl_lightprobes_usd.html b/examples/webgl_lightprobes_usd.html index 581b28aeb8546a..b9a4f0235ceafe 100644 --- a/examples/webgl_lightprobes_usd.html +++ b/examples/webgl_lightprobes_usd.html @@ -137,12 +137,17 @@ async function bakeWithResolution( resolution ) { - if ( probeGrid ) probeGrid.dispose(); + if ( probeGrid ) { + + scene.remove( probeGrid ); + probeGrid.dispose(); + + } probeGrid = new LightProbeVolume( probeBounds, new THREE.Vector3( resolution * 2, resolution, resolution ) ); probeGrid.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 100 } ); - - scene.lightProbeVolume = params.enabled ? probeGrid : null; + probeGrid.visible = params.enabled; + scene.add( probeGrid ); // Update debug visualization @@ -168,7 +173,7 @@ const gui = new GUI(); gui.add( params, 'enabled' ).name( 'GI' ).onChange( ( value ) => { - scene.lightProbeVolume = value ? probeGrid : null; + probeGrid.visible = value; } ); gui.add( params, 'resolution', 2, 12, 1 ).name( 'Resolution' ).onFinishChange( ( value ) => { diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 26e27a15b317e9..1409e6a5c0af48 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -55,6 +55,32 @@ import { createCanvasElement, probeAsync, warnOnce, error, warn, log } from '../ import { ColorManagement } from '../math/ColorManagement.js'; import { getDFGLUT } from './shaders/DFGLUTData.js'; +const _objectPosition = /*@__PURE__*/ new Vector3(); + +function findLightProbeVolume( volumes, object ) { + + if ( volumes.length === 0 ) return null; + + if ( volumes.length === 1 ) { + + return volumes[ 0 ].textures[ 0 ] !== null ? volumes[ 0 ] : null; + + } + + _objectPosition.setFromMatrixPosition( object.matrixWorld ); + + for ( let i = 0, l = volumes.length; i < l; i ++ ) { + + const v = volumes[ i ]; + + if ( v.textures[ 0 ] !== null && v.boundingBox.containsPoint( _objectPosition ) ) return v; + + } + + return null; + +} + /** * This renderer uses WebGL 2 to display scenes. * @@ -1813,6 +1839,10 @@ class WebGLRenderer { if ( object.autoUpdate === true ) object.update( camera ); + } else if ( object.isLightProbeVolume ) { + + currentRenderState.pushLightProbeVolume( object ); + } else if ( object.isLight ) { currentRenderState.pushLight( object ); @@ -2128,7 +2158,7 @@ class WebGLRenderer { const lightsStateVersion = lights.state.version; - const parameters = programCache.getParameters( material, lights.state, shadowsArray, scene, object ); + const parameters = programCache.getParameters( material, lights.state, shadowsArray, scene, object, currentRenderState.state.lightProbeVolumesArray ); const programCacheKey = programCache.getProgramCacheKey( parameters ); let programs = materialProperties.programs; @@ -2220,7 +2250,7 @@ class WebGLRenderer { } - materialProperties.lightProbeVolume = scene.lightProbeVolume; + materialProperties.lightProbeVolume = currentRenderState.state.lightProbeVolumesArray.length > 0; materialProperties.currentProgram = program; materialProperties.uniformsList = null; @@ -2421,7 +2451,7 @@ class WebGLRenderer { needsProgramChange = true; - } else if ( !! materialProperties.lightProbeVolume !== !! scene.lightProbeVolume ) { + } else if ( !! materialProperties.lightProbeVolume !== ( currentRenderState.state.lightProbeVolumesArray.length > 0 ) ) { needsProgramChange = true; @@ -2467,11 +2497,16 @@ class WebGLRenderer { } - if ( materialProperties.needsLights && materialProperties.__lightProbeVolume !== ( scene.lightProbeVolume || null ) ) { + if ( materialProperties.needsLights ) { - materialProperties.__lightProbeVolume = scene.lightProbeVolume || null; + const objectVolume = findLightProbeVolume( currentRenderState.state.lightProbeVolumesArray, object ); - refreshMaterial = true; + if ( materialProperties.__lightProbeVolume !== objectVolume ) { + + materialProperties.__lightProbeVolume = objectVolume; + refreshMaterial = true; + + } } @@ -2656,9 +2691,9 @@ class WebGLRenderer { // irradiance probe grid - if ( materialProperties.needsLights && scene.lightProbeVolume ) { + if ( materialProperties.needsLights && materialProperties.__lightProbeVolume ) { - const volume = scene.lightProbeVolume; + const volume = materialProperties.__lightProbeVolume; m_uniforms.probeGridSH0.value = volume.textures[ 0 ]; m_uniforms.probeGridSH1.value = volume.textures[ 1 ]; diff --git a/src/renderers/webgl/WebGLPrograms.js b/src/renderers/webgl/WebGLPrograms.js index cfec1ca31b947c..98980d3747ff7e 100644 --- a/src/renderers/webgl/WebGLPrograms.js +++ b/src/renderers/webgl/WebGLPrograms.js @@ -53,7 +53,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin } - function getParameters( material, lights, shadows, scene, object ) { + function getParameters( material, lights, shadows, scene, object, lightProbeVolumes ) { const fog = scene.fog; const geometry = object.geometry; @@ -343,7 +343,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin numLightProbes: lights.numLightProbes, - lightProbeVolume: !! scene.lightProbeVolume, + lightProbeVolume: lightProbeVolumes.length > 0, numClippingPlanes: clipping.numPlanes, numClipIntersection: clipping.numIntersection, diff --git a/src/renderers/webgl/WebGLRenderStates.js b/src/renderers/webgl/WebGLRenderStates.js index 5cc7363554e735..6b6a783f400ef2 100644 --- a/src/renderers/webgl/WebGLRenderStates.js +++ b/src/renderers/webgl/WebGLRenderStates.js @@ -6,6 +6,7 @@ function WebGLRenderState( extensions ) { const lightsArray = []; const shadowsArray = []; + const lightProbeVolumesArray = []; function init( camera ) { @@ -13,6 +14,7 @@ function WebGLRenderState( extensions ) { lightsArray.length = 0; shadowsArray.length = 0; + lightProbeVolumesArray.length = 0; } @@ -28,6 +30,12 @@ function WebGLRenderState( extensions ) { } + function pushLightProbeVolume( volume ) { + + lightProbeVolumesArray.push( volume ); + + } + function setupLights() { lights.setup( lightsArray ); @@ -43,6 +51,7 @@ function WebGLRenderState( extensions ) { const state = { lightsArray: lightsArray, shadowsArray: shadowsArray, + lightProbeVolumesArray: lightProbeVolumesArray, camera: null, @@ -58,7 +67,8 @@ function WebGLRenderState( extensions ) { setupLightsView: setupLightsView, pushLight: pushLight, - pushShadow: pushShadow + pushShadow: pushShadow, + pushLightProbeVolume: pushLightProbeVolume }; } diff --git a/src/scenes/Scene.js b/src/scenes/Scene.js index 4d9b7dca6727cc..151b76fe4810e7 100644 --- a/src/scenes/Scene.js +++ b/src/scenes/Scene.js @@ -103,15 +103,6 @@ class Scene extends Object3D { */ this.environmentRotation = new Euler(); - /** - * An optional light probe volume that provides position-dependent - * diffuse global illumination via a 3D grid of L1 SH probes. - * - * @type {?LightProbeVolume} - * @default null - */ - this.lightProbeVolume = null; - /** * Forces everything in the scene to be rendered with the defined material. It is possible * to exclude materials from override by setting {@link Material#allowOverride} to `false`. @@ -144,8 +135,6 @@ class Scene extends Object3D { this.environmentIntensity = source.environmentIntensity; this.environmentRotation.copy( source.environmentRotation ); - this.lightProbeVolume = source.lightProbeVolume; - if ( source.overrideMaterial !== null ) this.overrideMaterial = source.overrideMaterial.clone(); this.matrixAutoUpdate = source.matrixAutoUpdate; From d495e9c17af37773a8a8b4e007f57f3559130288 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Fri, 6 Mar 2026 19:09:56 +0900 Subject: [PATCH 05/22] LightProbeVolume: Update complex example with two-room layout Co-Authored-By: Claude Opus 4.6 --- examples/webgl_lightprobes_complex.html | 261 ++++++++++++++++-------- 1 file changed, 178 insertions(+), 83 deletions(-) diff --git a/examples/webgl_lightprobes_complex.html b/examples/webgl_lightprobes_complex.html index de7543b8fdda63..21801cfed71a5e 100644 --- a/examples/webgl_lightprobes_complex.html +++ b/examples/webgl_lightprobes_complex.html @@ -1,7 +1,7 @@ - three.js webgl - light probe volume + three.js webgl - light probe volume (multi-room) @@ -9,8 +9,8 @@
- three.js - light probe volume
- Position-dependent diffuse global illumination via L1 SH probe grid + three.js - light probe volume (multi-room)
+ Two rooms with independent probe volumes showcasing multi-volume scene.add() API
- - - - From 1cc496e72d78599436652eea392972d811dd755f Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Tue, 17 Mar 2026 23:25:02 +0100 Subject: [PATCH 12/22] LightProbeVolume: Use texture atlas approach. --- .../jsm/helpers/LightProbeVolumeHelper.js | 66 +++++++--- examples/jsm/lighting/LightProbeVolume.js | 117 ++++++++++++------ src/renderers/WebGLRenderer.js | 13 +- .../light_probe_volume_pars_fragment.glsl.js | 52 +++++--- src/renderers/shaders/UniformsLib.js | 11 +- 5 files changed, 164 insertions(+), 95 deletions(-) diff --git a/examples/jsm/helpers/LightProbeVolumeHelper.js b/examples/jsm/helpers/LightProbeVolumeHelper.js index 9e1527cb3900dc..660edbc5d1492e 100644 --- a/examples/jsm/helpers/LightProbeVolumeHelper.js +++ b/examples/jsm/helpers/LightProbeVolumeHelper.js @@ -37,9 +37,8 @@ class LightProbeVolumeHelper extends InstancedMesh { uniforms: { - probeGridSH0: { value: null }, - probeGridSH1: { value: null }, - probeGridSH2: { value: null } + probeGridSH: { value: null }, + probeGridResolution: { value: new Vector3() }, }, @@ -64,30 +63,58 @@ class LightProbeVolumeHelper extends InstancedMesh { precision highp sampler3D; - uniform sampler3D probeGridSH0; - uniform sampler3D probeGridSH1; - uniform sampler3D probeGridSH2; + uniform sampler3D probeGridSH; + uniform vec3 probeGridResolution; varying vec3 vWorldNormal; varying vec3 vUVW; void main() { - vec4 s0 = texture( probeGridSH0, vUVW ); - vec4 s1 = texture( probeGridSH1, vUVW ); - vec4 s2 = texture( probeGridSH2, vUVW ); - - vec3 c0 = s0.rgb; - vec3 c1 = vec3( s0.a, s1.rg ); - vec3 c2 = vec3( s1.ba, s2.r ); - vec3 c3 = s2.gba; + // Atlas UV mapping — must match light_probe_volume_pars_fragment.glsl.js + float nz = probeGridResolution.z; + float paddedSlices = nz + 2.0; + float atlasDepth = 7.0 * paddedSlices; + float uvZBase = vUVW.z * nz + 1.0; + + vec4 s0 = texture( probeGridSH, vec3( vUVW.xy, ( uvZBase ) / atlasDepth ) ); + vec4 s1 = texture( probeGridSH, vec3( vUVW.xy, ( uvZBase + paddedSlices ) / atlasDepth ) ); + vec4 s2 = texture( probeGridSH, vec3( vUVW.xy, ( uvZBase + 2.0 * paddedSlices ) / atlasDepth ) ); + vec4 s3 = texture( probeGridSH, vec3( vUVW.xy, ( uvZBase + 3.0 * paddedSlices ) / atlasDepth ) ); + vec4 s4 = texture( probeGridSH, vec3( vUVW.xy, ( uvZBase + 4.0 * paddedSlices ) / atlasDepth ) ); + vec4 s5 = texture( probeGridSH, vec3( vUVW.xy, ( uvZBase + 5.0 * paddedSlices ) / atlasDepth ) ); + vec4 s6 = texture( probeGridSH, vec3( vUVW.xy, ( uvZBase + 6.0 * paddedSlices ) / atlasDepth ) ); + + // Unpack 9 vec3 SH L2 coefficients + + vec3 c0 = s0.xyz; + vec3 c1 = vec3( s0.w, s1.xy ); + vec3 c2 = vec3( s1.zw, s2.x ); + vec3 c3 = s2.yzw; + vec3 c4 = s3.xyz; + vec3 c5 = vec3( s3.w, s4.xy ); + vec3 c6 = vec3( s4.zw, s5.x ); + vec3 c7 = s5.yzw; + vec3 c8 = s6.xyz; vec3 n = normalize( vWorldNormal ); + float x = n.x, y = n.y, z = n.z; + + // band 0 vec3 result = c0 * 0.886227; - result += c1 * 2.0 * 0.511664 * n.y; - result += c2 * 2.0 * 0.511664 * n.z; - result += c3 * 2.0 * 0.511664 * n.x; + + // band 1, + result += c1 * 2.0 * 0.511664 * y; + result += c2 * 2.0 * 0.511664 * z; + result += c3 * 2.0 * 0.511664 * x; + + // band 2, + result += c4 * 2.0 * 0.429043 * x * y; + result += c5 * 2.0 * 0.429043 * y * z; + result += c6 * ( 0.743125 * z * z - 0.247708 ); + result += c7 * 2.0 * 0.429043 * x * z; + result += c8 * 0.429043 * ( x * x - y * y ); gl_FragColor = vec4( max( result, vec3( 0.0 ) ), 1.0 ); @@ -173,9 +200,8 @@ class LightProbeVolumeHelper extends InstancedMesh { // Update texture uniforms - this.material.uniforms.probeGridSH0.value = probeGrid.textures[ 0 ]; - this.material.uniforms.probeGridSH1.value = probeGrid.textures[ 1 ]; - this.material.uniforms.probeGridSH2.value = probeGrid.textures[ 2 ]; + this.material.uniforms.probeGridSH.value = probeGrid.texture; + this.material.uniforms.probeGridResolution.value.copy( probeGrid.resolution ); } diff --git a/examples/jsm/lighting/LightProbeVolume.js b/examples/jsm/lighting/LightProbeVolume.js index fa2f444bf7930b..bd3100e4933773 100644 --- a/examples/jsm/lighting/LightProbeVolume.js +++ b/examples/jsm/lighting/LightProbeVolume.js @@ -29,7 +29,7 @@ let _shMaterial = null; let _lastCubemapSize = 0; let _lastFlip = 0; -// Repack materials (one per output texture) +// Repack materials (one per output sub-volume / texture index) let _repackMaterials = null; // Cached bake resources @@ -48,11 +48,29 @@ const _position = /*@__PURE__*/ new Vector3(); const _savedViewport = /*@__PURE__*/ new Vector4(); const _savedScissor = /*@__PURE__*/ new Vector4(); +// Number of padding texels added at each boundary of every sub-volume in the atlas. +const ATLAS_PADDING = 1; + /** * A 3D grid of L2 Spherical Harmonic irradiance probes that provides - * position-dependent diffuse global illumination. The probe data is stored - * in seven RGBA `WebGL3DRenderTarget` instances with `LinearFilter` for - * hardware trilinear interpolation. + * position-dependent diffuse global illumination. + * + * All seven packed SH sub-volumes are stored in a **single** RGBA + * `WebGL3DRenderTarget` using a texture-atlas layout along the Z axis. + * Each sub-volume occupies `( nz + 2 )` atlas slices: one padding slice at + * each end (a copy of the nearest edge data slice) to prevent color bleeding + * when the hardware trilinear filter reads across a sub-volume boundary. + * + * Atlas layout (nz = resolution.z, PADDING = 1): + * ``` + * slice 0 : padding (copy of sub-volume 0, data slice 0) + * slices 1 … nz : sub-volume 0 data + * slice nz + 1 : padding (copy of sub-volume 0, data slice nz-1) + * slice nz + 2 : padding (copy of sub-volume 1, data slice 0) + * slices nz+3 … 2*nz+2 : sub-volume 1 data + * … + * ``` + * Total atlas depth = `7 * ( nz + 2 )`. * * Baking is fully GPU-resident: cubemap rendering, SH projection, and * texture packing all happen on the GPU with zero CPU readback. @@ -93,16 +111,16 @@ class LightProbeVolume extends Object3D { this.resolution = resolution.clone(); /** - * The seven RGBA 3D textures storing packed SH coefficients. - * @type {Data3DTexture[]} + * The single RGBA atlas 3D texture storing all seven packed SH sub-volumes. + * @type {Data3DTexture|null} */ - this.textures = [ null, null, null, null, null, null, null ]; + this.texture = null; /** - * Internal render targets for GPU-resident baking. + * Internal render target for GPU-resident baking. * @private */ - this._renderTargets = [ null, null, null, null, null, null, null ]; + this._renderTarget = null; } @@ -209,27 +227,51 @@ class LightProbeVolume extends Object3D { renderer.shadowMap.autoUpdate = savedShadowAutoUpdate; - // Phase 2: Repack SH data from batch target into 3D textures (GPU-to-GPU) + // Phase 2: Repack SH data from batch target into the atlas 3D texture (GPU-to-GPU). + // + // For each of the 7 packed sub-volumes (texture index t) we write: + // - A leading padding slice (copy of data slice iz = 0) + // - All nz data slices (iz = 0 … nz-1) + // - A trailing padding slice (copy of data slice iz = nz-1) + // + // In the atlas the slices for sub-volume t occupy the range: + // [ t * paddedSlices, t * paddedSlices + paddedSlices - 1 ] + // where paddedSlices = nz + 2 * ATLAS_PADDING. + _ensureRepackResources(); + const paddedSlices = res.z + 2 * ATLAS_PADDING; + const rt = this._renderTarget; + rt.scissorTest = false; + rt.viewport.set( 0, 0, res.x, res.y ); + for ( let t = 0; t < 7; t ++ ) { _repackMaterials[ t ].uniforms.batchTexture.value = batchTarget.texture; _repackMaterials[ t ].uniforms.resolution.value.copy( res ); - const rt = this._renderTargets[ t ]; - rt.scissorTest = false; - rt.viewport.set( 0, 0, res.x, res.y ); - + // Write data slices for ( let iz = 0; iz < res.z; iz ++ ) { _repackMaterials[ t ].uniforms.sliceZ.value = iz; _mesh.material = _repackMaterials[ t ]; - renderer.setRenderTarget( rt, iz ); + renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + iz ); renderer.render( _scene, _camera ); } + // Leading padding: copy of data slice iz = 0 + _repackMaterials[ t ].uniforms.sliceZ.value = 0; + _mesh.material = _repackMaterials[ t ]; + renderer.setRenderTarget( rt, t * paddedSlices ); + renderer.render( _scene, _camera ); + + // Trailing padding: copy of data slice iz = nz - 1 + _repackMaterials[ t ].uniforms.sliceZ.value = res.z - 1; + _mesh.material = _repackMaterials[ t ]; + renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + res.z ); + renderer.render( _scene, _camera ); + } // Restore renderer state @@ -245,31 +287,30 @@ class LightProbeVolume extends Object3D { } /** - * Ensures the 3D render target textures exist with the correct dimensions. + * Ensures the atlas 3D render target exists with the correct dimensions. * @private */ _ensureTextures() { + if ( this._renderTarget !== null ) return; + const res = this.resolution; const nx = res.x, ny = res.y, nz = res.z; - for ( let t = 0; t < 7; t ++ ) { - - if ( this._renderTargets[ t ] !== null ) continue; - - const rt = new WebGL3DRenderTarget( nx, ny, nz, { - format: RGBAFormat, - type: FloatType, - minFilter: LinearFilter, - magFilter: LinearFilter, - generateMipmaps: false, - depthBuffer: false - } ); + // Atlas depth: 7 sub-volumes, each with ATLAS_PADDING slices at both ends + const atlasDepth = 7 * ( nz + 2 * ATLAS_PADDING ); - this._renderTargets[ t ] = rt; - this.textures[ t ] = rt.texture; + const rt = new WebGL3DRenderTarget( nx, ny, atlasDepth, { + format: RGBAFormat, + type: FloatType, + minFilter: LinearFilter, + magFilter: LinearFilter, + generateMipmaps: false, + depthBuffer: false + } ); - } + this._renderTarget = rt; + this.texture = rt.texture; } @@ -278,15 +319,11 @@ class LightProbeVolume extends Object3D { */ dispose() { - for ( let t = 0; t < 7; t ++ ) { - - if ( this._renderTargets[ t ] !== null ) { + if ( this._renderTarget !== null ) { - this._renderTargets[ t ].dispose(); - this._renderTargets[ t ] = null; - this.textures[ t ] = null; - - } + this._renderTarget.dispose(); + this._renderTarget = null; + this.texture = null; } @@ -418,7 +455,7 @@ function _ensureGPUResources( cubemapSize, flip ) { } -// Internal: Ensure GPU resources for repacking SH into 3D textures +// Internal: Ensure GPU resources for repacking SH into the atlas 3D texture function _ensureRepackResources() { if ( _repackMaterials !== null ) return; diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 20f6312637d435..b6fbe9a346d7ba 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -63,7 +63,7 @@ function findLightProbeVolume( volumes, object ) { if ( volumes.length === 1 ) { - return volumes[ 0 ].textures[ 0 ] !== null ? volumes[ 0 ] : null; + return volumes[ 0 ].texture !== null ? volumes[ 0 ] : null; } @@ -73,7 +73,7 @@ function findLightProbeVolume( volumes, object ) { const v = volumes[ i ]; - if ( v.textures[ 0 ] !== null && v.boundingBox.containsPoint( _objectPosition ) ) return v; + if ( v.texture !== null && v.boundingBox.containsPoint( _objectPosition ) ) return v; } @@ -2695,15 +2695,10 @@ class WebGLRenderer { const volume = materialProperties.__lightProbeVolume; - m_uniforms.probeGridSH0.value = volume.textures[ 0 ]; - m_uniforms.probeGridSH1.value = volume.textures[ 1 ]; - m_uniforms.probeGridSH2.value = volume.textures[ 2 ]; - m_uniforms.probeGridSH3.value = volume.textures[ 3 ]; - m_uniforms.probeGridSH4.value = volume.textures[ 4 ]; - m_uniforms.probeGridSH5.value = volume.textures[ 5 ]; - m_uniforms.probeGridSH6.value = volume.textures[ 6 ]; + m_uniforms.probeGridSH.value = volume.texture; m_uniforms.probeGridMin.value.copy( volume.boundingBox.min ); m_uniforms.probeGridMax.value.copy( volume.boundingBox.max ); + m_uniforms.probeGridResolution.value.copy( volume.resolution ); } diff --git a/src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js index 73b3a8c1c3a26d..cbe5ce8aa8b110 100644 --- a/src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js +++ b/src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js @@ -1,36 +1,52 @@ export default /* glsl */` #ifdef USE_LIGHT_PROBE_VOLUME -uniform highp sampler3D probeGridSH0; -uniform highp sampler3D probeGridSH1; -uniform highp sampler3D probeGridSH2; -uniform highp sampler3D probeGridSH3; -uniform highp sampler3D probeGridSH4; -uniform highp sampler3D probeGridSH5; -uniform highp sampler3D probeGridSH6; +// Single atlas 3D texture that stores all 7 SH sub-volumes stacked along Z. +// Atlas depth = 7 * ( nz + 2 ) where nz = probeGridResolution.z. +// Each sub-volume occupies ( nz + 2 ) slices: 1 padding + nz data + 1 padding. +// Padding is a copy of the first / last data slice and prevents color bleeding +// when the hardware linear filter reads across a sub-volume boundary. +uniform highp sampler3D probeGridSH; uniform vec3 probeGridMin; uniform vec3 probeGridMax; +uniform vec3 probeGridResolution; vec3 getLightProbeVolumeIrradiance( vec3 worldPos, vec3 worldNormal ) { - vec3 texSize = vec3( textureSize( probeGridSH0, 0 ) ); - vec3 texSizeMinusOne = texSize - 1.0; + vec3 res = probeGridResolution; vec3 gridRange = probeGridMax - probeGridMin; - vec3 probeSpacing = gridRange / texSizeMinusOne; + vec3 resMinusOne = res - 1.0; + vec3 probeSpacing = gridRange / resMinusOne; // Offset sample position along normal by half a probe spacing vec3 samplePos = worldPos + worldNormal * probeSpacing * 0.5; vec3 uvw = clamp( ( samplePos - probeGridMin ) / gridRange, 0.0, 1.0 ); - uvw = uvw * texSizeMinusOne / texSize + 0.5 / texSize; - vec4 s0 = texture( probeGridSH0, uvw ); - vec4 s1 = texture( probeGridSH1, uvw ); - vec4 s2 = texture( probeGridSH2, uvw ); - vec4 s3 = texture( probeGridSH3, uvw ); - vec4 s4 = texture( probeGridSH4, uvw ); - vec4 s5 = texture( probeGridSH5, uvw ); - vec4 s6 = texture( probeGridSH6, uvw ); + // Remap to texel centers of the probe grid (XY and Z) + uvw = uvw * resMinusOne / res + 0.5 / res; + + // Atlas UV mapping along Z: + // paddedSlices = nz + 2 (1 padding texel at each end of every sub-volume) + // atlasDepth = 7 * paddedSlices + // For sub-volume t the first DATA texel sits at atlas slice t*paddedSlices + 1. + // Given probe-grid texel-centre UVZ = ( iz + 0.5 ) / nz the atlas UV is: + // atlasUvZ = ( uvw.z * nz + t * paddedSlices + 1 ) / atlasDepth + // + // uvZBase encodes the nz-scaled Z plus the intra-volume offset (+ 1 for padding), + // so adding t*paddedSlices steps to each successive sub-volume. + float nz = res.z; + float paddedSlices = nz + 2.0; + float atlasDepth = 7.0 * paddedSlices; + float uvZBase = uvw.z * nz + 1.0; + + vec4 s0 = texture( probeGridSH, vec3( uvw.xy, ( uvZBase ) / atlasDepth ) ); + vec4 s1 = texture( probeGridSH, vec3( uvw.xy, ( uvZBase + paddedSlices ) / atlasDepth ) ); + vec4 s2 = texture( probeGridSH, vec3( uvw.xy, ( uvZBase + 2.0 * paddedSlices ) / atlasDepth ) ); + vec4 s3 = texture( probeGridSH, vec3( uvw.xy, ( uvZBase + 3.0 * paddedSlices ) / atlasDepth ) ); + vec4 s4 = texture( probeGridSH, vec3( uvw.xy, ( uvZBase + 4.0 * paddedSlices ) / atlasDepth ) ); + vec4 s5 = texture( probeGridSH, vec3( uvw.xy, ( uvZBase + 5.0 * paddedSlices ) / atlasDepth ) ); + vec4 s6 = texture( probeGridSH, vec3( uvw.xy, ( uvZBase + 6.0 * paddedSlices ) / atlasDepth ) ); // Unpack 9 vec3 SH L2 coefficients vec3 c0 = s0.xyz; diff --git a/src/renderers/shaders/UniformsLib.js b/src/renderers/shaders/UniformsLib.js index 8ac9598397db72..f18039dd5e7fe5 100644 --- a/src/renderers/shaders/UniformsLib.js +++ b/src/renderers/shaders/UniformsLib.js @@ -194,15 +194,10 @@ const UniformsLib = { ltc_1: { value: null }, ltc_2: { value: null }, - probeGridSH0: { value: null }, - probeGridSH1: { value: null }, - probeGridSH2: { value: null }, - probeGridSH3: { value: null }, - probeGridSH4: { value: null }, - probeGridSH5: { value: null }, - probeGridSH6: { value: null }, + probeGridSH: { value: null }, probeGridMin: { value: /*@__PURE__*/ new Vector3() }, - probeGridMax: { value: /*@__PURE__*/ new Vector3() } + probeGridMax: { value: /*@__PURE__*/ new Vector3() }, + probeGridResolution: { value: /*@__PURE__*/ new Vector3() } }, From 17253fdd9453223ecee0a5bfe61eb03be54f7d04 Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Tue, 17 Mar 2026 23:32:22 +0100 Subject: [PATCH 13/22] Clean up. --- examples/files.json | 3 ++ src/renderers/WebGLRenderer.js | 61 +++++++++++++++++----------------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/examples/files.json b/examples/files.json index 023ca2c07e02e8..569a8edc04587c 100644 --- a/examples/files.json +++ b/examples/files.json @@ -57,6 +57,9 @@ "webgl_lensflares", "webgl_lightprobe", "webgl_lightprobe_cubecamera", + "webgl_lightprobes", + "webgl_lightprobes_complex", + "webgl_lightprobes_sponza", "webgl_lights_hemisphere", "webgl_lights_physical", "webgl_lights_spotlight", diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index b6fbe9a346d7ba..be7d7749c801e0 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -55,32 +55,6 @@ import { createCanvasElement, probeAsync, warnOnce, error, warn, log } from '../ import { ColorManagement } from '../math/ColorManagement.js'; import { getDFGLUT } from './shaders/DFGLUTData.js'; -const _objectPosition = /*@__PURE__*/ new Vector3(); - -function findLightProbeVolume( volumes, object ) { - - if ( volumes.length === 0 ) return null; - - if ( volumes.length === 1 ) { - - return volumes[ 0 ].texture !== null ? volumes[ 0 ] : null; - - } - - _objectPosition.setFromMatrixPosition( object.matrixWorld ); - - for ( let i = 0, l = volumes.length; i < l; i ++ ) { - - const v = volumes[ i ]; - - if ( v.texture !== null && v.boundingBox.containsPoint( _objectPosition ) ) return v; - - } - - return null; - -} - /** * This renderer uses WebGL 2 to display scenes. * @@ -156,6 +130,7 @@ class WebGLRenderer { const uintClearColor = new Uint32Array( 4 ); const intClearColor = new Int32Array( 4 ); + const objectPosition = new Vector3(); let currentRenderList = null; let currentRenderState = null; @@ -2295,6 +2270,30 @@ class WebGLRenderer { } + function findLightProbeVolume( volumes, object ) { + + if ( volumes.length === 0 ) return null; + + if ( volumes.length === 1 ) { + + return volumes[ 0 ].texture !== null ? volumes[ 0 ] : null; + + } + + objectPosition.setFromMatrixPosition( object.matrixWorld ); + + for ( let i = 0, l = volumes.length; i < l; i ++ ) { + + const v = volumes[ i ]; + + if ( v.texture !== null && v.boundingBox.containsPoint( objectPosition ) ) return v; + + } + + return null; + + } + function setProgram( camera, scene, geometry, material, object ) { if ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ... @@ -2501,9 +2500,9 @@ class WebGLRenderer { const objectVolume = findLightProbeVolume( currentRenderState.state.lightProbeVolumesArray, object ); - if ( materialProperties.__lightProbeVolume !== objectVolume ) { + if ( materialProperties.lightProbeVolume !== objectVolume ) { - materialProperties.__lightProbeVolume = objectVolume; + materialProperties.lightProbeVolume = objectVolume; refreshMaterial = true; } @@ -2689,11 +2688,11 @@ class WebGLRenderer { materials.refreshMaterialUniforms( m_uniforms, material, _pixelRatio, _height, currentRenderState.state.transmissionRenderTarget[ camera.id ] ); - // irradiance probe grid + // light probe volume - if ( materialProperties.needsLights && materialProperties.__lightProbeVolume ) { + if ( materialProperties.needsLights && materialProperties.lightProbeVolume ) { - const volume = materialProperties.__lightProbeVolume; + const volume = materialProperties.lightProbeVolume; m_uniforms.probeGridSH.value = volume.texture; m_uniforms.probeGridMin.value.copy( volume.boundingBox.min ); From 77fe75d1bad4b03f69d91c827dd36ee3006eb0cc Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Wed, 18 Mar 2026 09:33:40 +0100 Subject: [PATCH 14/22] Improve nomenclature. --- src/renderers/shaders/ShaderChunk.js | 4 ++-- src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js | 2 +- ...ment.glsl.js => lights_probe_volume_pars_fragment.glsl.js} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename src/renderers/shaders/ShaderChunk/{light_probe_volume_pars_fragment.glsl.js => lights_probe_volume_pars_fragment.glsl.js} (100%) diff --git a/src/renderers/shaders/ShaderChunk.js b/src/renderers/shaders/ShaderChunk.js index 8cb217687905d8..1261deeeae54dc 100644 --- a/src/renderers/shaders/ShaderChunk.js +++ b/src/renderers/shaders/ShaderChunk.js @@ -40,7 +40,6 @@ import fog_pars_vertex from './ShaderChunk/fog_pars_vertex.glsl.js'; import fog_fragment from './ShaderChunk/fog_fragment.glsl.js'; import fog_pars_fragment from './ShaderChunk/fog_pars_fragment.glsl.js'; import gradientmap_pars_fragment from './ShaderChunk/gradientmap_pars_fragment.glsl.js'; -import light_probe_volume_pars_fragment from './ShaderChunk/light_probe_volume_pars_fragment.glsl.js'; import lightmap_pars_fragment from './ShaderChunk/lightmap_pars_fragment.glsl.js'; import lights_lambert_fragment from './ShaderChunk/lights_lambert_fragment.glsl.js'; import lights_lambert_pars_fragment from './ShaderChunk/lights_lambert_pars_fragment.glsl.js'; @@ -52,6 +51,7 @@ import lights_phong_fragment from './ShaderChunk/lights_phong_fragment.glsl.js'; import lights_phong_pars_fragment from './ShaderChunk/lights_phong_pars_fragment.glsl.js'; import lights_physical_fragment from './ShaderChunk/lights_physical_fragment.glsl.js'; import lights_physical_pars_fragment from './ShaderChunk/lights_physical_pars_fragment.glsl.js'; +import lights_probe_volume_pars_fragment from './ShaderChunk/lights_probe_volume_pars_fragment.glsl.js'; import lights_fragment_begin from './ShaderChunk/lights_fragment_begin.glsl.js'; import lights_fragment_maps from './ShaderChunk/lights_fragment_maps.glsl.js'; import lights_fragment_end from './ShaderChunk/lights_fragment_end.glsl.js'; @@ -169,7 +169,6 @@ export const ShaderChunk = { fog_fragment: fog_fragment, fog_pars_fragment: fog_pars_fragment, gradientmap_pars_fragment: gradientmap_pars_fragment, - light_probe_volume_pars_fragment: light_probe_volume_pars_fragment, lightmap_pars_fragment: lightmap_pars_fragment, lights_lambert_fragment: lights_lambert_fragment, lights_lambert_pars_fragment: lights_lambert_pars_fragment, @@ -180,6 +179,7 @@ export const ShaderChunk = { lights_phong_pars_fragment: lights_phong_pars_fragment, lights_physical_fragment: lights_physical_fragment, lights_physical_pars_fragment: lights_physical_pars_fragment, + lights_probe_volume_pars_fragment: lights_probe_volume_pars_fragment, lights_fragment_begin: lights_fragment_begin, lights_fragment_maps: lights_fragment_maps, lights_fragment_end: lights_fragment_end, diff --git a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js index 33c292ccb6ca64..3fde5f0fa20f20 100644 --- a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js +++ b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js @@ -212,5 +212,5 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi #endif -#include +#include `; diff --git a/src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/lights_probe_volume_pars_fragment.glsl.js similarity index 100% rename from src/renderers/shaders/ShaderChunk/light_probe_volume_pars_fragment.glsl.js rename to src/renderers/shaders/ShaderChunk/lights_probe_volume_pars_fragment.glsl.js From 1553bb11a9d4281a74404fac8aa06dd5f428b1fe Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Wed, 18 Mar 2026 09:42:39 +0100 Subject: [PATCH 15/22] LightProbeVolume: Clean up. --- examples/jsm/lighting/LightProbeVolume.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/jsm/lighting/LightProbeVolume.js b/examples/jsm/lighting/LightProbeVolume.js index bd3100e4933773..eba3eab3cd08f4 100644 --- a/examples/jsm/lighting/LightProbeVolume.js +++ b/examples/jsm/lighting/LightProbeVolume.js @@ -100,25 +100,30 @@ class LightProbeVolume extends Object3D { /** * The world-space bounding box for the grid. + * * @type {Box3} */ - this.boundingBox = boundingBox.clone(); + this.boundingBox = boundingBox; /** * The number of probes along each axis. + * * @type {Vector3} */ - this.resolution = resolution.clone(); + this.resolution = resolution; /** * The single RGBA atlas 3D texture storing all seven packed SH sub-volumes. - * @type {Data3DTexture|null} + * + * @type {?Data3DTexture} */ this.texture = null; /** * Internal render target for GPU-resident baking. + * * @private + * @type {?WebGL3DRenderTarget} */ this._renderTarget = null; @@ -187,7 +192,7 @@ class LightProbeVolume extends Object3D { renderer.setRenderTarget( batchTarget ); renderer.clear(); - const t0 = performance.now(); + // const t0 = performance.now(); // Phase 1: Render cubemaps and project to SH into batch target // Note: set viewport/scissor on the render target directly to avoid pixel ratio scaling @@ -280,7 +285,7 @@ class LightProbeVolume extends Object3D { renderer.setScissor( _savedScissor ); renderer.setScissorTest( savedScissorTest ); - console.log( `LightProbeVolume: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` ); + // console.log( `LightProbeVolume: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` ); this.visible = true; @@ -288,6 +293,7 @@ class LightProbeVolume extends Object3D { /** * Ensures the atlas 3D render target exists with the correct dimensions. + * * @private */ _ensureTextures() { From 6212bfdadb23c8ad922ef846a063b0145002d6e8 Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Wed, 18 Mar 2026 10:09:12 +0100 Subject: [PATCH 16/22] LightProbeVolume: More clean up. --- examples/jsm/lighting/LightProbeVolume.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/jsm/lighting/LightProbeVolume.js b/examples/jsm/lighting/LightProbeVolume.js index eba3eab3cd08f4..7c129088772cb0 100644 --- a/examples/jsm/lighting/LightProbeVolume.js +++ b/examples/jsm/lighting/LightProbeVolume.js @@ -116,6 +116,7 @@ class LightProbeVolume extends Object3D { * The single RGBA atlas 3D texture storing all seven packed SH sub-volumes. * * @type {?Data3DTexture} + * @default null */ this.texture = null; @@ -124,6 +125,7 @@ class LightProbeVolume extends Object3D { * * @private * @type {?WebGL3DRenderTarget} + * @default null */ this._renderTarget = null; @@ -362,6 +364,7 @@ function _ensureGPUResources( cubemapSize, flip ) { if ( _shMaterial !== null ) _shMaterial.dispose(); _shMaterial = new ShaderMaterial( { + precision: 'highp', defines: { CUBEMAP_SIZE: cubemapSize, FLIP: flip.toFixed( 1 ) @@ -375,7 +378,8 @@ function _ensureGPUResources( cubemapSize, flip ) { } `, fragmentShader: /* glsl */` - precision highp float; + #include + uniform samplerCube envMap; void main() { @@ -419,14 +423,19 @@ function _ensureGPUResources( cubemapSize, flip ) { vec3 dir = normalize( coord ); vec3 cw = textureCube( envMap, coord ).rgb * weight; + // band 0 accum0 += cw * 0.282095; + + // band 1 accum1 += cw * ( 0.488603 * dir.y ); accum2 += cw * ( 0.488603 * dir.z ); accum3 += cw * ( 0.488603 * dir.x ); - accum4 += cw * ( 1.092548 * dir.x * dir.y ); - accum5 += cw * ( 1.092548 * dir.y * dir.z ); + + // band 2 + accum4 += cw * ( 1.092548 * ( dir.x * dir.y ) ); + accum5 += cw * ( 1.092548 * ( dir.y * dir.z ) ); accum6 += cw * ( 0.315392 * ( 3.0 * dir.z * dir.z - 1.0 ) ); - accum7 += cw * ( 1.092548 * dir.x * dir.z ); + accum7 += cw * ( 1.092548 * ( dir.x * dir.z ) ); accum8 += cw * ( 0.546274 * ( dir.x * dir.x - dir.y * dir.y ) ); } @@ -435,7 +444,7 @@ function _ensureGPUResources( cubemapSize, flip ) { } - float norm = 4.0 * 3.14159265359 / totalWeight; + float norm = 4.0 * PI / totalWeight; vec3 accum; if ( coefIndex == 0 ) accum = accum0; @@ -488,6 +497,7 @@ function _ensureRepackResources() { for ( let t = 0; t < 7; t ++ ) { _repackMaterials[ t ] = new ShaderMaterial( { + precision: 'highp', defines: { TEXTURE_INDEX: t }, @@ -498,7 +508,6 @@ function _ensureRepackResources() { }, vertexShader: repackVertexShader, fragmentShader: /* glsl */` - precision highp float; uniform sampler2D batchTexture; uniform vec3 resolution; uniform int sliceZ; From a3b01bb133319ca21a03370bd421da14c717d676 Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Wed, 18 Mar 2026 10:20:29 +0100 Subject: [PATCH 17/22] LightProveVolume: Remove useless check for coordinate system. --- examples/jsm/lighting/LightProbeVolume.js | 29 ++++++++++------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/examples/jsm/lighting/LightProbeVolume.js b/examples/jsm/lighting/LightProbeVolume.js index 7c129088772cb0..521a7117f8bbb3 100644 --- a/examples/jsm/lighting/LightProbeVolume.js +++ b/examples/jsm/lighting/LightProbeVolume.js @@ -14,7 +14,6 @@ import { Vector3, Vector4, WebGL3DRenderTarget, - WebGLCoordinateSystem, WebGLCubeRenderTarget, WebGLRenderTarget } from 'three'; @@ -24,10 +23,9 @@ let _scene = null; let _camera = null; let _mesh = null; -// SH projection material (depends on cubemapSize + flip) +// SH projection material (depends on cubemapSize) let _shMaterial = null; let _lastCubemapSize = 0; -let _lastFlip = 0; // Repack materials (one per output sub-volume / texture index) let _repackMaterials = null; @@ -169,7 +167,7 @@ class LightProbeVolume extends Object3D { */ bake( renderer, scene, options = {} ) { - const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( renderer, options ); + const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( options ); this._ensureTextures(); @@ -354,20 +352,19 @@ function _ensureScene() { } // Internal: Ensure GPU resources for SH projection are created -function _ensureGPUResources( cubemapSize, flip ) { +function _ensureGPUResources( cubemapSize ) { _ensureScene(); - // Recreate material when cubemap size or flip changes - if ( cubemapSize !== _lastCubemapSize || flip !== _lastFlip ) { + // Recreate material when cubemap size changes + if ( cubemapSize !== _lastCubemapSize ) { if ( _shMaterial !== null ) _shMaterial.dispose(); _shMaterial = new ShaderMaterial( { precision: 'highp', defines: { - CUBEMAP_SIZE: cubemapSize, - FLIP: flip.toFixed( 1 ) + CUBEMAP_SIZE: cubemapSize }, uniforms: { envMap: { value: null } @@ -404,13 +401,14 @@ function _ensureGPUResources( cubemapSize, flip ) { for ( int ix = 0; ix < CUBEMAP_SIZE; ix ++ ) { - float col = ( 1.0 - ( float( ix ) + 0.5 ) * pixelSize ) * FLIP; + // WebGL cubemaps have a left-handed orientation (flip = -1) + float col = ( float( ix ) + 0.5 ) * pixelSize - 1.0; float row = 1.0 - ( float( iy ) + 0.5 ) * pixelSize; vec3 coord; - if ( face == 0 ) coord = vec3( -1.0 * FLIP, row, col * FLIP ); - else if ( face == 1 ) coord = vec3( 1.0 * FLIP, row, -col * FLIP ); + if ( face == 0 ) coord = vec3( 1.0, row, -col ); + else if ( face == 1 ) coord = vec3( -1.0, row, col ); else if ( face == 2 ) coord = vec3( col, 1.0, -row ); else if ( face == 3 ) coord = vec3( col, -1.0, row ); else if ( face == 4 ) coord = vec3( col, row, 1.0 ); @@ -464,7 +462,6 @@ function _ensureGPUResources( cubemapSize, flip ) { } ); _lastCubemapSize = cubemapSize; - _lastFlip = flip; } @@ -557,7 +554,7 @@ function _ensureRepackResources() { } // Internal: Ensure cube render target and camera exist with the right parameters -function _ensureBakeResources( renderer, options ) { +function _ensureBakeResources( options ) { const { cubemapSize = 8, @@ -577,9 +574,7 @@ function _ensureBakeResources( renderer, options ) { } - const flip = renderer.coordinateSystem === WebGLCoordinateSystem ? - 1 : 1; - - _ensureGPUResources( cubemapSize, flip ); + _ensureGPUResources( cubemapSize ); return { cubeRenderTarget: _cubeRenderTarget, cubeCamera: _cubeCamera }; From fe13d180648a69a13086f10b0c1a29474901d8d8 Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Tue, 24 Mar 2026 13:51:59 +0100 Subject: [PATCH 18/22] E2E: Update screenshots. --- examples/screenshots/webgl_lightprobes.jpg | Bin 0 -> 24038 bytes .../screenshots/webgl_lightprobes_complex.jpg | Bin 0 -> 20788 bytes .../screenshots/webgl_lightprobes_sponza.jpg | Bin 0 -> 47792 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/screenshots/webgl_lightprobes.jpg create mode 100644 examples/screenshots/webgl_lightprobes_complex.jpg create mode 100644 examples/screenshots/webgl_lightprobes_sponza.jpg diff --git a/examples/screenshots/webgl_lightprobes.jpg b/examples/screenshots/webgl_lightprobes.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0887e459000a7a509c18e8930aa00ed5d67ff92b GIT binary patch literal 24038 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 literal 0 HcmV?d00001 diff --git a/examples/screenshots/webgl_lightprobes_complex.jpg b/examples/screenshots/webgl_lightprobes_complex.jpg new file mode 100644 index 0000000000000000000000000000000000000000..501c263e213dc1d7ddfd6f49055d22bbb83cf9eb GIT binary patch literal 20788 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 literal 0 HcmV?d00001 diff --git a/examples/screenshots/webgl_lightprobes_sponza.jpg b/examples/screenshots/webgl_lightprobes_sponza.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e74c3a5638bbf41fc006f4c91a7fff5d103bbdab GIT binary patch literal 47792 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%0A`z@)uwZ$$2Q!cqTo`A&`IanP$j5XF9U~uI-KHj% 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 literal 0 HcmV?d00001 From bd68af16e0a47d2a432ca9028eb6356973dece3b Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Wed, 8 Apr 2026 18:04:56 +0900 Subject: [PATCH 19/22] LightProbeVolume: Update API to use width/height/depth. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/jsm/lighting/LightProbeVolume.js | 71 +++++++++++++++++++---- examples/webgl_lightprobes.html | 8 +-- examples/webgl_lightprobes_complex.html | 16 ++--- examples/webgl_lightprobes_sponza.html | 11 +--- 4 files changed, 68 insertions(+), 38 deletions(-) diff --git a/examples/jsm/lighting/LightProbeVolume.js b/examples/jsm/lighting/LightProbeVolume.js index 521a7117f8bbb3..954f399db59c52 100644 --- a/examples/jsm/lighting/LightProbeVolume.js +++ b/examples/jsm/lighting/LightProbeVolume.js @@ -1,4 +1,5 @@ import { + Box3, CubeCamera, FloatType, HalfFloatType, @@ -43,6 +44,7 @@ let _batchTargetProbes = 0; // Reusable temp objects const _position = /*@__PURE__*/ new Vector3(); +const _size = /*@__PURE__*/ new Vector3(); const _savedViewport = /*@__PURE__*/ new Vector4(); const _savedScissor = /*@__PURE__*/ new Vector4(); @@ -80,10 +82,16 @@ class LightProbeVolume extends Object3D { /** * Constructs a new irradiance probe grid. * - * @param {Box3} boundingBox - The world-space bounding box for the grid. - * @param {Vector3} resolution - The number of probes along each axis (x, y, z). + * The volume is centered at the object's position. + * + * @param {number} [width=1] - Full width of the volume along X. + * @param {number} [height=1] - Full height of the volume along Y. + * @param {number} [depth=1] - Full depth of the volume along Z. + * @param {number} [widthProbes] - Number of probes along X. Defaults to `Math.max( 2, Math.round( width ) + 1 )`. + * @param {number} [heightProbes] - Number of probes along Y. Defaults to `Math.max( 2, Math.round( height ) + 1 )`. + * @param {number} [depthProbes] - Number of probes along Z. Defaults to `Math.max( 2, Math.round( depth ) + 1 )`. */ - constructor( boundingBox, resolution ) { + constructor( width = 1, height = 1, depth = 1, widthProbes, heightProbes, depthProbes ) { super(); @@ -97,18 +105,44 @@ class LightProbeVolume extends Object3D { this.isLightProbeVolume = true; /** - * The world-space bounding box for the grid. + * The full width of the volume along X. * - * @type {Box3} + * @type {number} + */ + this.width = width; + + /** + * The full height of the volume along Y. + * + * @type {number} */ - this.boundingBox = boundingBox; + this.height = height; + + /** + * The full depth of the volume along Z. + * + * @type {number} + */ + this.depth = depth; /** * The number of probes along each axis. * * @type {Vector3} */ - this.resolution = resolution; + this.resolution = new Vector3( + widthProbes !== undefined ? widthProbes : Math.max( 2, Math.round( width ) + 1 ), + heightProbes !== undefined ? heightProbes : Math.max( 2, Math.round( height ) + 1 ), + depthProbes !== undefined ? depthProbes : Math.max( 2, Math.round( depth ) + 1 ) + ); + + /** + * The world-space bounding box for the grid. Updated automatically + * by {@link LightProbeVolume#bake}. + * + * @type {Box3} + */ + this.boundingBox = new Box3(); /** * The single RGBA atlas 3D texture storing all seven packed SH sub-volumes. @@ -127,6 +161,8 @@ class LightProbeVolume extends Object3D { */ this._renderTarget = null; + this.updateBoundingBox(); + } /** @@ -140,20 +176,30 @@ class LightProbeVolume extends Object3D { */ getProbePosition( ix, iy, iz, target ) { - const min = this.boundingBox.min; - const max = this.boundingBox.max; + const pos = this.position; const res = this.resolution; + const w = this.width, h = this.height, d = this.depth; target.set( - res.x > 1 ? min.x + ix * ( max.x - min.x ) / ( res.x - 1 ) : ( min.x + max.x ) * 0.5, - res.y > 1 ? min.y + iy * ( max.y - min.y ) / ( res.y - 1 ) : ( min.y + max.y ) * 0.5, - res.z > 1 ? min.z + iz * ( max.z - min.z ) / ( res.z - 1 ) : ( min.z + max.z ) * 0.5 + res.x > 1 ? pos.x - w / 2 + ix * w / ( res.x - 1 ) : pos.x, + res.y > 1 ? pos.y - h / 2 + iy * h / ( res.y - 1 ) : pos.y, + res.z > 1 ? pos.z - d / 2 + iz * d / ( res.z - 1 ) : pos.z ); return target; } + /** + * Updates the world-space bounding box from the current position and size. + */ + updateBoundingBox() { + + _size.set( this.width, this.height, this.depth ); + this.boundingBox.setFromCenterAndSize( this.position, _size ); + + } + /** * Bakes all probes by rendering cubemaps at each probe position * and projecting to L2 SH. Fully GPU-resident with zero CPU readback. @@ -170,6 +216,7 @@ class LightProbeVolume extends Object3D { const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( options ); this._ensureTextures(); + this.updateBoundingBox(); // Prevent feedback: temporarily hide the volume during baking this.visible = false; diff --git a/examples/webgl_lightprobes.html b/examples/webgl_lightprobes.html index 4ce9b66e6256f5..20b184f383fb03 100644 --- a/examples/webgl_lightprobes.html +++ b/examples/webgl_lightprobes.html @@ -146,11 +146,6 @@ // Bake light probe volume - const bounds = new THREE.Box3( - new THREE.Vector3( - 2.8, 0.1, - 2.8 ), - new THREE.Vector3( 2.8, 4.8, 2.8 ) - ); - async function bakeWithResolution( resolution ) { if ( probeGrid ) { @@ -160,7 +155,8 @@ } - probeGrid = new LightProbeVolume( bounds, new THREE.Vector3( resolution, resolution, resolution ) ); + probeGrid = new LightProbeVolume( 5.6, 4.7, 5.6, resolution, resolution, resolution ); + probeGrid.position.set( 0, 2.45, 0 ); probeGrid.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } ); probeGrid.visible = params.enabled; scene.add( probeGrid ); diff --git a/examples/webgl_lightprobes_complex.html b/examples/webgl_lightprobes_complex.html index 21801cfed71a5e..54c2775e85d889 100644 --- a/examples/webgl_lightprobes_complex.html +++ b/examples/webgl_lightprobes_complex.html @@ -276,16 +276,6 @@ // Probe volumes - const boundsLeft = new THREE.Box3( - new THREE.Vector3( - 7.8, 0.1, - 3.8 ), - new THREE.Vector3( 0, 4.8, 3.8 ) - ); - - const boundsRight = new THREE.Box3( - new THREE.Vector3( 0, 0.1, - 3.8 ), - new THREE.Vector3( 7.8, 4.8, 3.8 ) - ); - const params = { enabled: true, showProbes: false, @@ -303,7 +293,8 @@ } - probeGridLeft = new LightProbeVolume( boundsLeft, new THREE.Vector3( resolution, resolution, resolution ) ); + probeGridLeft = new LightProbeVolume( 7.8, 4.7, 7.6, resolution, resolution, resolution ); + probeGridLeft.position.set( - 3.9, 2.45, 0 ); probeGridLeft.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } ); probeGridLeft.visible = params.enabled; scene.add( probeGridLeft ); @@ -317,7 +308,8 @@ } - probeGridRight = new LightProbeVolume( boundsRight, new THREE.Vector3( resolution, resolution, resolution ) ); + probeGridRight = new LightProbeVolume( 7.8, 4.7, 7.6, resolution, resolution, resolution ); + probeGridRight.position.set( 3.9, 2.45, 0 ); probeGridRight.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } ); probeGridRight.visible = params.enabled; scene.add( probeGridRight ); diff --git a/examples/webgl_lightprobes_sponza.html b/examples/webgl_lightprobes_sponza.html index 975accd34b6278..49a5822822f303 100644 --- a/examples/webgl_lightprobes_sponza.html +++ b/examples/webgl_lightprobes_sponza.html @@ -114,7 +114,6 @@ const modelCenter = _box.getCenter( _center ).clone(); const targetY = modelCenter.y + modelSize.y * 0.2; const lightBaseDistance = Math.max( modelSize.x, modelSize.z ); - const probeBounds = new THREE.Box3(); const probeFar = Math.max( modelSize.x, modelSize.y, modelSize.z ) * 2.0; let rebakeTimer = null; let isBaking = false; @@ -210,15 +209,11 @@ } - const halfSize = new THREE.Vector3( params.sizeX / 2, params.sizeY / 2, params.sizeZ / 2 ); - const center = new THREE.Vector3( params.boundsX, params.boundsY, params.boundsZ ); - probeBounds.min.copy( center ).sub( halfSize ); - probeBounds.max.copy( center ).add( halfSize ); - probeGrid = new LightProbeVolume( - probeBounds, - new THREE.Vector3( params.countX, params.countY, params.countZ ) + params.sizeX, params.sizeY, params.sizeZ, + params.countX, params.countY, params.countZ ); + probeGrid.position.set( params.boundsX, params.boundsY, params.boundsZ ); probeGrid.bake( renderer, scene, { cubemapSize: 32, near: 0.05, From f001b260287a8721461c1ec03340884ca400ba61 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Wed, 8 Apr 2026 18:11:23 +0900 Subject: [PATCH 20/22] LightProbeVolume: Improve examples and update screenshots. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/screenshots/webgl_lightprobes.jpg | Bin 24038 -> 22478 bytes .../screenshots/webgl_lightprobes_complex.jpg | Bin 20788 -> 21853 bytes examples/webgl_lightprobes.html | 17 ++++++++++++----- examples/webgl_lightprobes_complex.html | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/examples/screenshots/webgl_lightprobes.jpg b/examples/screenshots/webgl_lightprobes.jpg index 0887e459000a7a509c18e8930aa00ed5d67ff92b..cfed45504cd3d9508bdb09abfca0043be51918f1 100644 GIT binary patch literal 22478 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 diff --git a/examples/screenshots/webgl_lightprobes_complex.jpg b/examples/screenshots/webgl_lightprobes_complex.jpg index 501c263e213dc1d7ddfd6f49055d22bbb83cf9eb..2816b43dd2b048e1898d19dc6e24f1dca87cfd3a 100644 GIT binary patch literal 21853 zcmeFYXIPV45H1=kSP>AB8Wj))5$Q!pwgnJTQ4oT(s7R42y%QCsLj(k*Mw%ckRHcU= zdheakdqN56IiK!*&i!%!+D(7A`|n(ClKhd`i1 zzz>Ky0r~_weCW^P&-39wkE2KaJRduH^vKcU$BrNW`^$2I^*GB3mgC1+PqCgj`R4(= zI(_Qo=|BJc`?15vjvP65lI1wd-^>5Mo|ttYc2*GO(3c~J*g=Qc4;^7Y#B4g02FQ5q zuV{dv|2_^KK63QfaTY*?Q|AD&j~oGnJqAb$-02Hk107{Q#&KEd&T-CXuUM`)o|pa* znaV13x2T>=rw=bI^V;d-iIW$&FY@q;Tot``T})O^UO`bw`QCjsb&Us_4|SjG>B9_O z7@EB?x3IK&Ywhgf>gMj@>E-|Fb3kBFa7a}2*O=J2Z}ACf=^2?nva)~X6ql5i{rX*A zQQ6Sg)ZBt?ZENoz7#tcN866v+!_6-&E-kODt`T;2_x6bgBr@d?=*Yk70MGv~eE`J| z9sXl;mOuI&I_wVo9$`Ov?6TBxjyum-UO94Jk^aDX{%&MyQT+)a867;=Yp1@G7ldW! zLaCL#xtyGqc(jfrZ%116|7R*1wmf5LBb2}SB=f@o^@aA6g`@xC22EaS)0 z)cgU4aX$q*w%xMhJF%%iNM?daA%V^D)_-RUq$DOHWFh|!LrcS*|y_ui|Eq1>SFncC7As~_o`XG%UR0g)Z(4opu;WiW`f8ZP4FUAbFkl1 zW}2S@aFLJ`hviZJZ#d|_r~<>vcbf@{ywOZ$qrHmTS~KKk9M@ukT5`Yw3mqFhTX5PJ zCTK8ho}Bte(PZe$?Qk96DUCZ!P~8}UG^9k@6)E}E!9!ln6mgP^b1?K+rZ|w>a>pb4ersD)8?_Ma;($ zxk;^4auTPQpd%to(7axsAQ-D<2?op+`KibywL%zC=-v|lgj1&%ftLq{HZ0)j1RKz| zX<>rqfhOSn-5Y95&`~OB5paWQC~3M25X3s}w1s4z-vQ&U=MMoF#7JieH;ofDKRM&9 zi{GC2{j<%o7`YbEuD{br$~E3ZQpmAFOwdPbMsE~-O1lw30{?qg&XNh>-8`|nEsJp?l&Z!?4k1}GLB~0P z-4y~IGVUP%SzWfxFiF4%c6n#;kI>_nz1C`Zn4tPkKu~#&+=odstN_&l>FCcJOc2|y zkOS#z+$L+XtHT+@Yy*NJr9sJ!N5}%2-T8A1W1A%G0BAo+q9y&xpF`8w5}BZ|KE{7S zgNeWR{suE4e(i~W==ZKtml*eC*vjPE$(ebA6(Lme=+*`jF zaq7=%gV%moPX9Fy6ZG~K6_^PYO_U|&a!(JrKpVDz1=XhOc2cq1hz0Pt!JkQD7U+?F zSDO#Y9&_U>0Y3=`1bJTagaGUqHmZ7{CEcfehtUpf)HLdNY$VA<6Ik$7CMbso{jVUY z_evPGZ#ewAAP_)OUf7(YC=@)c#8IC_>LZ8B8t{GYG@e-Yk9=TZp;+1uw6ZXu^tBt_3j#%sK+RpiFC^S)D)do zhBj*KKAITVzJvYt?cjKYXwd@BNA~Lw-c@%e76k-+Vf(k+=gUuA>%nk!$@>&Q{hjkF zx5`*Lro210VZXau7C%=>@f3JIz^5E42-r%@s+x5)j!q8pxEc|v*_-+0!v|wmabd|E zSywMnf06RKvEg#-*E-^BSj+D|VinbwZa}{jocy-PG0eZ^bDIjWj$3pa=S1Bx=8_Ti zA8i(B@uv#eP~Mv)nDJ9YoU~*%?hzfQk^_*`lOAdK!?K=d5uxgx{F~6ZMc)R3# z3E*$u9tCcC5l6j=Y9W09}6*e{-Mzf;av-PMSJt9@>|>M55k>zpDv zAW}3Cx$wxs7BT;$HcJwvY}svbYvI8*)KWlVnmZ-<$(W*DW zC%du7l@F|REK#ObRW`GU@!Awre@)2w7xS?E^VXv@xtekzy9!$FaHe`I!Da-e{Fr;T zCEYRn*Bc<3+GO6vp3fC3?nPKWw%#k4QtHWS)7#1)3(H9!3B-4=KR1~_`T^uX@$tze4nOeiL!G-&6%mls0(D63V&C|+jP)of% z9#<->$B#>zeZXY3XXL)Unt8gAQ+d$~yQ%@g~1%4IcWmxSRS~YSTzf?Q2ewX$5|1X~1nISMSN9SQa>s^xEJA z*+%c~Vosn$?pvqJLLUsVC&#QUm45!yxA4YKu8*J7{Q5WL4`SL^Duv>H_ZB!u5Hy@w&r#C?^?gmC@3$*`1ePd&hGBdC!A{0Q*iHMg!s1}p1UbKDhEv%@OP6w z5<<;gHM7z;ok2F$UJ~EBWqSUhcp~2m$!p>aNm_jOt~l+~wjvul?Fr#mMTOBSye zaT$G-^2Q0lVY7;#6lfeXuSw<-oi^R2xxj^kC|%ZxC0{Bm+;47Z@keR4$qku`iM940 zHEeCV0Nr0f?776yJm*Deu?~!y#(hZcbtcHw550G7WKlHxJm~46fB0tD0Hk)=oe469 zQ}vbcyG+o*112cg?-5 zT}9s#(!;w0KjhPYn~lxq-GE(4^t-ZpqZ>bO2DWyKalBpWQNtrQWzq~Pc|@M5KK9hx z@j{zjnDu69WP&C%>MLzT#~&G+DK&fS7aKqH5<1bsvF5$q2qj(9Xt*uJJ0r~R+AWS; zyI6UX7N@dCI;Q7c0{KAlWP*+f(34K4O}E+CNPD1REZ=;eF?QLUmy3L$VL$QDRS^vU zj(!RqQ>VzW<-JJ7`j+jFLTB<8uO!;`MM$=QTk{%UD z!aVU=Z8)t)r*6Sb94>%tc>|z6*>Tm4tMKFjQv4j46LOW&4KuYIE!gzzzNO}~fXkGe zYDQ@UW`>6xjO}hW!7ut;j?a*Xo%4PfvXZXF>5J4<64@4%n3rVt;EJaOs?w=ZqF7@F>vOu>9z!$$TX}fI9qDF>O^=(b_G(zK zArP@x>W|v_Yc*3nQC%CjJa<&F?@<2l&t-~R7oD^Hd+5%Flxl$riDhjI2C4b#3?2he%m- zwvPgKtUT6}oeX;CjRwYe|6pTMS@~Wo%mw-%^co$lWTk=!?*@Q{r%yAz(dfdi5 zT;W-tQ@6HaY7VLX2tXDM_E^;)ru+9s6rKAun|JNm(xs2$WB?yrk_==TSu#GH=+3V+U`}E zTzP+1QxOD9)nMuDf4EV094SMv+*F#)z3_&&VX_svnEZYB1Wn@zLGC~a23jV^BwZ_4 z+ecN_E~6*odvtjM)2pauD(g+C(%~XRrRov22haCnR}a9{#dj=AHhW6%Slx@4KwoOgUy^{+^yQ^0G%I0uq+bvwN@P=xY zC(FjiZQyG+pzEDX(8{VfX}bkZzA;1h4qWrq*JZ5H;VVzk?b=WOk_QU4U;@Gev5IAa ztg)t7eHp7@f@B*igQUjI&YXG(K6>M7DBaRgqZ@ zs^Q-9z+0N1-PFTaJ{ZW1WfqC=W!Omsz3$yNu}e@DYtS6?3TkoG+kD$kN=Alvmq%aj z5U;$LcmEZnYrSBc>!u$sP0uyO4qVSW*f7qLGMBd@{6&o;tmIdnTQ(?4`$&z8PRq;0 zhk8N4PB2W; zm;HW9E?d4xqaC&cF+mN^evmKu@1A!Dm~hSg~(zY@=lrl;tUeIjyU7HEMZ)s>GNn= z4mvjT@Kf@%SgjDFJk!_oRM4AEFPm+Z9I_6=b{7$bVgq8qvDIHA`U&~Xjau)X#l8Wt zeNB;qr{cHX6b*?88olt#!Y+@3bNa*3-X#U}7#|Tk9Q2H}?&Iv9L&&FkheeKpzc_dK zA!BA^MU+g3En8koA|6y^I^3h6Qqt1W-xnNvAZ3X$BBFx6&#VpN?>yG++gApkrpe=f2W=gT3uMR`8+Vl; zV$MCD@O4ZY%6GGH`l-X)&Y}jclenYNK@fTlCbl73lH*aslEnMbv+_TnOPSWFgH7PQ z&-=)&ctcd(rrx0=xFg*3k+Nz!n8G6e~|tw?vC z?HXTpk3jbfi{zVW9)O3LpvnjhLlRVZnIXU%DXlzvdLDdf?TjxI#EM@! zCHE-@XXNHq>}Dj2Hm%{kj^j^t$#WK(18+*c3R5ghndAz4mtW9FwD8PyX^kcJi<>n8>9 zbO0D$pM>hJ6{8MdYfRAH&wkInKKyeGbpBA)?A2!a=zcMlp%zj7=z!i2#nE{v8lMv! z{|$<+=`QGqc)C_Vl$ z7e-|W4@>RSdG@dRzm&#E_;9@di?q9&p2gFNzjG#5lvTgR!A4$CRg){ZW1p6H5Ai3x zu@p?Jrj#s(MvwB_vD^%G^LjMHS}WusKdpMNnE@Iut#W_)(=q$4QzIYvpYHFRdveV> zPp~TDz1f>5uF?|Z_aqcwsgKVmg+vao8osL|+nl%^uV7{&A%m=X<}8mDP>9eE4Ot=W z5@)WA;P(0i#~*>Ia=roT=uf73^UX4?Q1eiuE_dP&{IAF#V;#mDu1;TK=d{{}V(5JPcMBgp1 zYcpZdtHYST`K|<2rjjZ`_GYC-aL6WK1z!^4p?+O$2>MM83xnVd@OLjUy z6@}2zml0KQKgj`R`N*`=8t4y}RXAM3Y>^t@M<9($+BHE{Gc>O5n*kBs1U`3_w<1ZE%; zFtAyPTh2@n_e#V!#2HjN^uk3s?bmS|)1HKU30|L4l+?zl9EyJ;cZX8_itB!{)9 z%6iA9=4`8w{0e0xILOlWG&#cA^upye*E;(`XS-KG%?5CL8k%tIwA5`vG~j6O=-NNq z5mSB+e=0o7AOBW(e#aFt70|sU#NKOIXbKQ!HE3EpmMhcTw;eSApwzo=XY|WvehIn8 zFB`5Ux&XToxu^JocUZ?|sp2X!v*FQDX>^%W8(1SC+p1oYC(lqdM^9Gdt;f%zKCuw& z9sG}F-kSDhhM^?mqj(}1JawP(l%Nlr=S9s0GNO>_jKhzwkFt8Vb06glcUlRV7PymJ7t-_#GLA6u_aLbIZ-e^nnH_UZaw zZN?AdFYpzPD0vulILdX;L=Ka=rk@ozvQ+>8JvC_wcveWu?|hp*i|`&Lxn~+5bc~x!ZT)_Y~sWl zsb0TS*LD{^_nZUFXSHtW4g;B~#O%pY!@plAusaiC(5`z2Z((RJSZ`-3u z&NLSEGW`3ymg5GG?oX^?bmvLYT)ZTY3CxNsq6iwEPg9!pebWtF9)cZ3Q7p_6> z&A3B?0tCjX9pwfI1lYCL#;liypPxoF;SltbwO&8UwG(MQaDg9khyZ9L0lC?w2>msK z3{v&E3qW~(!^Q8LCNzw&7qJ$uy-Y4CrbqIFL*CglLEryG2k$`vTn~$2zSffOQ?#J6 zepQO%7_W-p_3zSh7RaNxU+?2v*9$%=a19)00}8v>TA87|-}8Nz-l4-y zgZ2s|249ees4hOwLhIpWh!8tCs?^9(Ue)6(Z2FbX3w1DKntoPIb1>105L~M;x+cS_ zsnhADkAL96Ch~J%L3W-{9P$yE?5({FPbJBYP?p*7fS8&V zo|R0{OTTP#xH;Ktgm^MG5L>DyTamYXB`w7LtoX;gh-@L{Rmw<=sfLNHrHR5b)2mku zlfS6(B!=LNIF!=@*0SV>|0nn3KhHvomHf;7WQhUX5Bcl*&OraagfQx85**;$MzR=# z4++jQ|C9FNY-F()WrBW-Pz3aHcf=PgkgL@y;kPw!X~=cRdXwHJ{Bqip>yia}$F$50&bLAyKSkt-J{4)H;c|SzYHU}8}$YNuB9KsVmC2{Vk zoGvrEz_9FA7MTUTs??Vb-fmsn%bk_lbv?*4l(HI&z|U_#H|W%_ncse9Aufc2PwZ?U z8Wze$FiV^cw1nFEz;s4S&5LETJt|Tj5X51Ii;iIkv}#WgPJwCuBECy+@*51|7XCm2 zQSPqzJ6?=O)EUM}Om=9f0IPks${lOO&Lwn@qQ-QmHHP92Z9Q1OY0SX{okz$lvyF4C zE6aYXzkK4<%qnf&HUzrKx<`l@jNeH~%MlpzI)2H|m3rZr6*9P~rW=Y2m4)#Bx|HI; zHn)>N*(f2Qd&yxFq*Eh%w^3uyd1&l8O3S=y{+rY?bVg?C!d%R1$-ta;@|~U=GH{KL z-hvkL$}%g^kM4(D7EZEE9{GSRVz>g|>0b$tPd0}S4;u%nGd_wH+p}H~m@b>!i ziiiLNMN6tHol!@BwtKSGu^9nuJ|FL(HsOQ3Ea`NP%%+t6Mt#di?04rVBhjXo#_TAk zecVX=&{3V_a`f5ii0tpc;Pc~HMvXQjJPH$8ZR$SH0_6QU3hFTc#H)P9bTE>tm;Roq z8NoX?F9J9t3r^3DnO{b1MgGAkJ-t3MZa6sA37$G&r0VlEk}e(?pJ^ld#3sS&;N8%f z+nSZm#seCdfSk96Jp&0#QL2-wuQwDf3fOS-36_bCbIA2@UFUmP8-Q&Tuc_>6>mi_l zM9=SnaONb(HT~RmG@dyyBZGB)j=uD{jUz+X)Q1pzQ4g10B-Il)uQ4oJ7Rw~!b^q*L zSsw@dN#NB9`;5B1euGU_Cdd=awXuAPo&f$+q#XCY-RzZoW@26^GMHs))A*Sw4kE8} zfb;=e$Z7f%Vm$8tSG-?`A`9Umxj;00=QraTBP;8@d!xST1G*}`XkTLHXkMbK+%?4B z1$5kVj(R08!3#$oIg?R}R^utR++lIo1gXm5l6pY@qwt-K)fiI$vPRd$aQc_-u9M>x z*lD`eGcIH(!r?X(B!iQDw~YK&1&v$={w1=@XsGVH8!%!t@=@63$VxqA2GLQ%XiIk{yH?} zk@aGIGr|iRzq^bL9k`jWCM>)mEV)jH8m_g@TeohtV5UlzjBqMTr;_XydoulMKANsU zM9=M8UMBd4c$f~`^Bf2CjDe*$9oSz=?lMARJ#0@mwa!^sq9u(4Qd|~XQjkG4*Rj_ zH3x}25A0^hJmbQJFD!Sye+M5>4CyldqPDUC5TyuRM7yGXKsf-7KWR5eTQc6{O9Bbm z3|(LHc18~U*V~Ej0Fya7FF+?=JCM@9EHg|8$RjJ$Y!4^RSmMA&dhCIQye#cir2%JVt|)pQ>plCc#2YE$=_2 z-(MOA%sl|q%6-MK9m`OA8jz#w*#;@bC65$3bK&1of`GQoF_4YN1Kid!lI+R^jR%{m zyB2W$&50iNn@tGf0_I-ouV{W)TN}8q^s1F%$F{*^!C}GFR2zAzF6TDECBdJm4$vCs zcG0rCMfTz;;-Z`bS@1iX&dnkmi$T33BnS8K*R#u{z#Xm#WoSh(Z5hW;E>Cly7SWJ7 zjKkAr4Ji^z>$Blc5epe#2N7d)Ye-}V5E@TDpE-zILPkIiP1+JH(nfn|4-=0t#x+dF zN`adgl6@%8`meB==r2Ev+Ffy$T;%2qG%c?^9 zEyuTqt3CfYuy;wYD13K(ZTTy!MX}8d5V^{0(1}CbMzqSW)LRNH=r>szm`KofpJIY; zwIU+X<#Q{DBDB7TXZis{BLz_pLHnR4yz2%mx7NcDJLhAF6+ova>>f9RiwVjUS}gP< zI%;}i+E5O_`e@3TxeI<#a2b~9ffJkkyz9>aymStbD<+vBp*0z8_cL4onz8%@&|;4g zymK&@5kkXNP7e%=6Bls zSm3+}1+0(=okZ(a(UINThiv$rziz9~&XWm2$oZ0+S{yk^g{{u&zR5Wiw` zx@zE@$w-`4Dk{kjwai5v)u5KlIPY)>4X?V!H!s5D)MK`AFkka`mWa;s#asYW#EQ6@kp$h(0{I`+ezWKP8YBlf_S9kzH{ky}Ek|nkK(CFSZb?QEKQ!Urx&;4^CnM=`sOnT#g@UoC&({`v5O4 zRn{n^9Fdp!ewrn_%R!fQRjs^RM#~T8?ZpJ8Ok1LxRF0**TRdj^TjHUw`Qu-4HdAWr z)5EoKL%fvQ^qnZf5+l_4zCDeNm|Rc9v77%O$U)k)&mM~VKJJrVE?kAJv^jc}Jq>Uy znCTZ3*^CW(Fy-r#TzH;4n$DUk(0vcb7N8NxVD-5*(j+Y4{glAfSg|r~9DD8ubbAmL zV-rAMI$Mf~dI}JAU$@DZqP5sNru_5)>n`?8oV5e0S(n(;4;T%PEB1CX(Y3Rb*v1Y3 zYVZSP!2nxO^cG(G6Ee$RwudeffjQj;r-=g}<;U#(@Gf^{bRxe%?L0BKQ2icS4Hn=N zv{|z{n(@GjVT1zZkW|injgECy#0#-X@Ks#z2r8mXCinZ`$=!#lHUas%9!*(}k~N;L zIRfW`pTN+9HTTl4oJ`P@d?b}Bg1Am9*ltGB*y-Bo9nh&-b3wBlp2|xiz)t2yp1G$Qr;yio8kHIA90V zUff}VKC^nEyTD)?Ym#J0*;&-iDO+Ek*7YiRBvsZIq3)Yvr?avqX#uu>?E!`F?$N|Q zicdl;bOwpw(EySWqc=geLg<9#0Lf;V6r#|9%2D;naSvBp@C#-Pd2J`+_4|M}dFNg5 z!qBWbc!PuNXL;s~6vlHmm7tGGU5#E>&L0gJ9=;HZ(~&7U|0c`gT%AR~vANCCFnIpgwfETH;~1Wr_4zKp zIu=Kk9757Re)^gYSF7B5t$_65!f8=EdO+5ww*zUFR*}CgUx5TF7&ngdQ+SE^Qi5N- zuZ27c4wVVvS{XJToa&v=_opH9UA(0Wd=jbF)`AXAKLN{~4J7kY?ZKZ5bB9pSMCi0n z#1Z0|njM5K^7vy^Ri0E2z1WM&zXT-&;ecimW-r61<t}%k(!Cns;Q=jUvsuc|2?c@6v!f3vE`57pWsFK3mp6ngs))E}YN(yt0u}s>Pv$fp@&?vO1vdT)yw&p= zi#-UeWweZ7EEFtN?F|@Z%d-ObZJPG3@@^1t3`m0}MMK77tk;nW#P5XrnCT&H0NS=R z-L~LTg)JbRPh;k!|1gw8j2`a0V4kKi0Ba~gGrcQps*#*>@N#m>T4nYTzT8oW8@3&c z*MWxw-W{rznj0-RXt2wQ(<^qFM4pfMa1n{)3He9kJEUyFzqx}&K@_hw6&FMF3*6K2 zSLD&S)F~xnx~SQ}{iOduNL04!tzr%IUh7R73{8s3SFyu6gzG0*B<8k%=}tKkE)jO< z^qtswTdU_m6Jo3{-S8e)DhtKWWnNRx;Z+dSGSu!i#6*wu(ggz~&ji%H#=p?x(eGa@=+{+V+ZTkhRIGd55B1SLN#_4eMzB3-pbtxdnwD9LN;+V<3sar+TB z=pCS)ZepN;H=PA2n>aF%C_PABju=OB85g4)*@K+0Ug@CTwbAjy0zq}iSK!6O2{lit zx%qTu_=IGGe*?JKeS-;747$XAyYB}D?27?P^f$kiDchMMpnoso$Fur3)aaVEXDE-+Et_J{e z+2M9(_eGX-2eDCHzHH_K~wIH_@(OcA$u2fk+-N{HtqRO$FJp!AD{)RyP|GP zMcL*7j1bD=UmUj_5Ny8ERL&#Xy|9wg);QkY z@E6w#jKr&a*jJtJsOaI?&UjUq!<7xJ zTG%yh(t63B&%ct5llSP_X%H3wLN)W@fS!Vm8-M~ciuK!}704WgIxTvoVD2tNjl`oU zcnt&86jFhV`wzFeUNiE+K-$oYe_pAayNaDKT4x{(chkY|$gA4ZKpjKHVLw97(H;%> zSkBX3Wj7%bDf}7V*>mW=6D zd6P0SuFcK0kSO=#r6!cgK^+9>RD>y_x?wpBHwM@gW=L36o{dJ{0ncsOXG;P=kqd@%NbI7l1`k1|oliv)q z#DJxp6yqRd_DRd=qtPgvc>b%iT(}dreNE^<-752Ti644&W_Sb;8VgaOJ^D>T{nVH@ z65u7ajPEp`$#5rpiZb|c+q_QnWaEOyT{KO6@XgJ}Q1!Dd1{~ zh}akHm`zuU5pR?2-X?mGqzYyn+YoKJwU&q2;T!3<0MJR8_IXStkVZUsY=9))4ngl& zj>3PXWZ2`1MJVtl1Yr)&RWD1n)&mX_V}#b6N2uB8ZOKjoD%k5w6gN3=YRwlxckTPC zKEx6cSMDh@6_DI&pZC}=-XB?sjA+u*cbX9}2fB+|+RK@McD-kO1rHhK?m@g|5f!Fc zo>cBGWFV!ll?nP%3>+=VL|e1s>Y-Cm-NDRC`pSaqw&^gZixZA$={;+AKdpe@cV?Hi zzJYg`goyIj?5?nl-Q8&B@8;wAnlRfjvn7nFk?h>)n)d=`0)X<8Pitoi#^kW zJv&_Uui><7&-dX`s(H#lB{^=2+_ij+mtSedx%ej=g`$CR7ZtTRz!@t~-`wiA; z0cn?a{OYQ`$d!OnnIi6;Z|CWtj zdh2ej3Dqy4l$7HoKS2b2I;vG>C+nwH*FpINU zvI0FlB%~~MS#diLx7>9#uTnVfewCl<*gp40O?C`P2EvyQl+yhbQcp;j^4Ux<(RCNX zPA?9N_HiFFxc++e+{Jxz?igBU)XSzV?k;3?8->?Lup(YEI!(C;Z2PonSH=TUau>ru zg>`Lpd$X|g`rYN`p$x{P2)KHeptb4dPEGFVK#t>{POJ8hX|vqdZanB88F*J?^fZoy zx$k~$44f$_Up_SpJ?T8q!0Yg*cNe*YWSj?3P6#|vH3?WeON}=0uWAh84kwE2jRcr# zbUD7(r&^brKIW%!(~NI+rj`84{;cxI-i8fIdiKma-?#0>#|~c?>2i9WXO(vE#y2!1 zU)Lb58o9lXmM+tM9wg$E1dGwPh*ax{u(d_fZ!7$v_2N~Yy5oR?f|1p0PI<+6!$lsO z{M)}uMa%?ZZZnD}rQdE| zRPwQTHNNDerROUlO5uZ#zcZcYOSF@oZo7ER$VO+86LeOgL_x8wO(ojETY+_~ii{y$ zfL>~uUtco#`Pv5KrH^?W1&@-jiUSi=zb;cTt@?WN)UTFZ8Hj?c5ojS3H0Geo| ziyPZ`csE;e!de|Pe*W6*SrFGRu^N{5ncQ++StdY(iBsuGm|a2Wj*P2>xKYSLn|!oe zx~kz+;NC&rW#uRGI&jYgO+D@B-GOt9NlteP5LTnP$MD^0t)iiduk1&Kgg&a;Ai1t) zX~)pZ{j4?{)&ui0YCCB08{l)YsIw@NXk6WTB7$wqx4_}NRhw*wb;_HHh~>)>(qjh& zKOfrggbs~ePw_dg8h!@)b=?IBUPc@;v)%}-HXM)cSq??)?$b9Y=IY=M5}!WgSg~Ty z2MOfJ6K%%T!dPkepNoDi;PzS^g~Y9mr3L9@ND{!c63PN*d7U~+4}(VfVn6u}r11Y5 zsFn55zetlgQ|`d(5lJg8)jO@<7O`QLy`D0(Uvb7=SG<5caj@#TE$iVKqlgvInxX=% zS^tYV66J+%y&&iA)1Qzt13^8G3t_x_?^adJZ4fZpT~=pdgRJ_o(%+Lm9c;t3J|tgP zXy5YLddnO6aY_F+ax}J`0yKnwRmsJC?cW$0l!}lZrT0@#60f&7#Z?wK&WUiTP3@~* zU6r(367)?1i0k{n@%gfVu(W-Rt5+SP+$^0aFmtoHLqBf@6EyIsEaXSKeN`EB9y z*nViqv7Wd$-E7mgBNvQ;#+f7m3dDT9H7reh2lAGlu_#J>KQdJ=`pm3n&Q$&;z!k{- zsTs`90OwiO{zUvccvDSO)5%NW$+exMdOWJR6YYrL9TnrT%)qKGuWXY?Kq~k;b5M`| zUIKAQ5a=+>%zWRz}f>5k_F8PkgM>xj5JJW|1* z9kDjP4V=qV|K-Wz6!Jv8ziuX0BN6EsmhL=*vG5Mg_hZvKPbgNHuxKx=-!vXgmU39o zJD(z8)JfLBAp>WcF~1@{d}p1C0%#vY7oC{L6Hg~WO~`*TPz_a%x_&M4E8%Z5FMl8FA8WFQctI4+}dC3x1Xl|&bnt+zLV*^XW1fSStrP)#014_JOQ?;!TL^E z*NjvyLzG-H(TFCx{HawRXg3bq?p22ViMqx3ai40fzfo8ICHgZ&Pctwd@j$&|w?HTi zv#F=tuqgD_SNz2A_4~mys%PE}6OWMn{>U0?M3aBGuq^D122qixSN*)5;ZkSt7^S>uicC? zO1uBlKk3{?=h(#PX^YPfOD)Lx4#@y5xXTZmvtL=h_@M6-Z)x=KRKn_-(ZCkP`I#c> zT87n1+&!wjvBCl3OMx|k-mdIf8oWQVq2S9es9Kcp=0=G03LoG5&6F9#4)zQ9$D$T4 zugs}*?JOTn{PJ8@NSTtoOV6PfVpJ4PbxJkOW z-~p{t#z1JJSKt_L|E;`YxsnLCc_GDpw9U0nsq!S(i|ZXd0`zCt@XU*^#GqT4R{Z7> zeQmH`E8@7yVvA8O#Eza%vzwa< z0+%kozfmnW=b09TI7%vRS2Vv?`(|LZi{DbI?od#-&04W->MN)AzL>IPpg?=yz8w7R zwTZEvxLX2s2zip8W?SH+YCeZJH8U1;a}Ra&t-eBQ!9`DkQO<)q0_wA#1(mYtr}bWF zynWoU?XD!H6V=!tJ*6gD4)jZpk_PO`X2y3L#!beTlUp@cvkXbn zuM6HhY|qnt{U&f|Y!wrPu-Fc$y(&)so#C5qcKq3mPw)NW>(SkXr8#q3Kc9cK&re@r zXiVrQi6c0!Pv`{Fq=iCbjL2_p52 zMGAR`1Aykf^4!8%v~9e~TE(ZhF%1AQ<6!`Fw*26Cj--QYc--Q^UI~r^?`$pAu3u}& zlXi25+=dpc-&N2b90%HTw>(6j5aQ~Q7qz`eQXcW=UpZsPdPjy{t}8o@k}1z?GB5as zL5_Nu-Uy7N|TxUt{#$ae`3+hmaXG^{;Q2SeDlpL2~9mz^VQ8%0(jHe{6ZX6V}1A?hn{F) zl&5eb5A-K+2C&+p(=t&eA5kCot-rK4=WNPHnmoM#}EP2frshS;nwsZ=E*0-0RNVZh*P*hGT+6!_8%~ zq6AK~jyq3;?A?0e=B8phw9^g=%$o3!Z}0EQ-?M3n;1>QG_VUd^v+KjJqT+4Vo{`Pn z?8RREQ^k8ynAXc9uD{XzNTZEhAD*W9={*+{I{rJ`hF0z_)42LuY3=o3#`t|isYIUF;|*ow`x|9NA?JIH#SYQMcWFid9Ei(> z44f*sAlb18fs-2XNMq9WVIO7ubgG@5r%THN^Qx)ErCV?oD`Crvtt>pN@W=JvMqkQg z8a`YV{hgpxqLgExAUXxUO;e*sK&?;#cedml0uQ)}KcgIE&4?LwYY(%!)2F_|wn$Gd zbr5*3j}g5Pbu{hda4$J|%Ws_}M(w@Yr-V`)g~GTXpLmIo!i2s=r^pgYLc3nz8}%I` z7gi^~{XuXd*l;fQ`OpZ1^xwfT7ulx{WYk8^Ikd(kr}J%SACl-li2ct(S-V2)et#t@$^ds#O2 z2x=lF8%6?GxqOd3vfw25k$v77j1YNxftJuIh;ZRCpUa-e0~XSVaFGX$7_NVchZZJ; z$A-ADvA0>~St&f#EMtXz+Bxa}#e4AiPxZ{BxB;=}xw_WUp^qq7qGZ481cGzIR6Bht zLkuH}wy)P1jQn6+o@dGt!8wxvhxTsN_o9`@)#%ufgriRlrzPmk6pllOSf2Pk_POei z(1(3BEy#Z0P_?rIL7nhVKAF<5t?Qf7TXvCFHOKdJ?G!((Q|$cZoT8P==xfkM2md&$ zb>G=dF|{x3S2{p1>-LWdeji?EzovS1sJe4T_V>L_KOWlI;@?_Y4o9sB_d~ciZoka= z?gBddF@HYGyE1~2;Ut?Esfbsoe*m-%Kiz%^SV85OT)ZZt1A7oU_IU3+Nne2;4aiQfNQgv#X~RKHohydAC0VpI0opj7t^A9kbc{p0wnUw0ew z3cH8@M>|*A)3ekwQQIw#K9Xl?JrB<0NADFl33aj9Q zc_Zd)-Dm3k%^y_S>(Z^Wlag%4#i|TPFso*DpU&M^Z|44@Aj2=Cd)HC-Ur*d69f@9i z#Hq2LK57g7oc}sLo?JOR{^j_Zfr$<)&*Z#Km7}R8FQZ7ow{ynq?z?PkH*R<0{%STq zm}Y0rszKa>s)dbX*lmV5Jh5WXJ#<=76CgR?u%wi|dHAHEpHXIPN@)EB$B@M`r@E7t zHixp~ZrKdCkPW@L2{1n&w|RxxvsIPsw?1g?-gII)(ok}1`q`5ra&f;c5zbNa?eXsK zl&RgQh?n^B(P-VUPc+P8Q#CyF*?9Qk#n3M~H%LCW4ctc|iHj5V!7G4-p{UG%VdLL&6Mb$V+dzssht$(dd3d|hSlCZRn9gZ( z1IiA(IZdH<(4Y2rKkWunl6o&o;`C$AeR{rLO*c)gtkKI2UVL4)Jfh*=UqttPuFa~s zQ9VHf?Ye9wq;=;C*V_A+%S0<=mmhEDWF$z>gb6J2xVN6^^_?X@st!As?3eM&^AnVN ziBHb&H7|?}%4^e-ReCw>XiIZZ6r;n$Io12g7p6aT%um^QL6Bo_OH>pvbh2ne1 z8eKIF9b7+i4J5Xj+#a3OTk`qXIznH z@R(k%nrU~6#qX!CMWsv)HU@DM#20*R06l{fs3hEgm-ElbJoxFv118zZ5g`i`kQYWL zdBT(RB?Z!OOC^x%Y($AAZdrzQX9B-sM0_DCtU9>VC2$KChTk^-c-*VN?7nz^dOmuf zW|DO^V!509qjOCEb0C|U*c84tua4ij1aU)SGUlsZ_IS8G|F}LSJ!?<@MtCHCzE4ns zrpRNU#b}C}2wDRfoI}R-Ndiv1=bu-ZSzm0^pAPZV8ER<`^Q%o)vzm_lX#Ai#x~W?b zliT_PiDq7T(mLk5LcVzM>ROWcrOuGUuf;h6!S1;z3 zR+skSXG$}%hBDz@@l%(5{*ifEa4oq%Q{@b?D?!fGl4>6{A$lVxD;b=OZL{Z#k`?#W z2kzEa+{F(;#*TF{*{^#`H5#ba$b>%DWAccmlJ#u`u8Vz&}p@{1o;D`KOo_u%rId{7TZ>z;QOO8f^T!K7dC8R$Vzj*fyDH# zA^rD6gv*?%1G=IvA!Uby)j2%2&bnCwst$-I{9gpG8X)4-gVTM0BP2q$L+dFXL=k`* zo8D21#CQ`sdzFp~MwyWm0~{h%b_Wov8b$(9sTxRGw^yPA9daOcXxA8`3=i)4TebrG9b{<@7A^dx;mza)S=U%QEE|uAEGI>kKQLNyw`?07*fL zqkE~T=RYqWf64@ZjvuwE@P-uAH>r~?B435az1wTIWwWlvD)X*?>!5bx_UX1p<(mW* z)Cg&cCNh@A^yt{BpZ(5vQ7$6(A9c3corkjSMs+``+YQywL`Jx_h~%;CH6$Zl zs;&&k_Qe|nJ;T)abz(idqC!t8)(pQ zN6%FmgQ3ZaPS93AJQ8(Lc~rh$dhfjmF{1}3MZ?0wonhuP;|u6H!t20&VjE@&#|xm3 z#B$jOoc$iLkRcz6R?1e4cj*R=&y%ob2|tXXk{M{^s)BTmO=_ih@fs;uX=fNv$qv+p z0^Z@KGSmOdNRWGIfjL}_LK8YLAJlFaaAY680H=QEMZ;mr4DB7FhpK*69bH8EO616H z;E=~; zsSx8!!s++Vej4|>0Gt4v8uP)&bPk$ToExJZK+_U1r-O0smb)gw5dFa_K$>FbZ7rH5 zxHw?J0BBzaFo#vl!P~KO!mCzxzT}kkyG^+=x+U8b8H$iplQGp9GNge+{pX&)p|o8B z=HRA^d+Kz%*;F;PmOV7pS%S6|JQBX*f(Bq$s>DCie1Y|KX9?lsk*7AM=k9`s9=)N; zT$G~=MnmRgKB=Eo;$9tAxLNqHAXoM6*sHl};=wZMYo@lC6_QPQ7iKqu|4 z@9eiqAe&){vFoubywRC%!q-8aRXksZ+Dl@OH7Q!t*|C}L*0V^An@eKia{#@Q$SQ{F z3VT?_y;3jCKHPlSLGR)N$G9Wc)upX3LTX}4AD6DadHI%g|7lUm{!7lxqis{v!oib6 iKI^K~O-D-lEbJ1XKhOA$b%90Rg2;i;56B0@5K-0a03{3j$FAks4{z z2|aYAMtbipp(P=OyM6xeIronHetGZxd^RIHWMpNn+4fv>{^ndqe~xBAXLU5SH9^OY zfk4NAKhV)QC=_)3*x&2#{rKPO1k>O9$rC4-PB5QjX8!lZ!g`9Cg_VVw`PAuCtZaWT z;J-7c+0Oj^@SmR@Kgq;&l8uF#<=?OWKi!TRKwPIlhsQoL9peHW=Q_s3b?m6+*f)U2 zlmC(i5c;3%*m0&4Cz)9Q5>B55$Yx>!h&>5V3XBW}z5|`$I>~+E+8yR|kDswz^x(Pv zCid$o@w>&1yoUV*3B~80Z&}&+_yq)oB&DP;U6#3_qAt_Vo`u*ZORwBe6PXqI+=*eFz*4yB z*e2{59p>d~nk0DET=#?G$pNS@?cATQGrlK>rc96ol_Sv3$#wS-o3jbg12%|roD8Yr zWFC9nEW|PcUaYZl1Zqg+D=9nzy}=!UJ}TTHNH<;wSy*l%SF4XeM5U0q?*3P(i6c<= z{t8_r2%52rIRb@unhS+lva@KPq^9m^SEsiiI|2YDi!!) z6obV{&|S4|gMR#LM+F}f;P-#IVOGv0ls21wp%_|wtY10;v2EnEq=Z&HrebH%vN43) zAumabi7*-ml=fyS=nOrjUYMr&+YM8`x_?29f9bs3R-$Ild##h4x8!W}UE2if*~)D7 zG<#siRIK8RqQMz*Aj|uOkX&c)xFgVlf&L)$sry_0*Zvi%BuMh=5vXmw3H|=bUVz3C z=opd~yL<#{fwb539f6L+XdiyBVhMWNqlX)u%G)g(kkkYu&8aG9PV1~(0+hgi1nM}= zxNvZP3Q6V$=GG6_Jp#S8MD$dIvbw7o0P_t4a-BEnf}X34QNIjGPvHm@$4L&S@cRQF zfWfh0+9}`gwj;{a}ykH`4 zCA$27&3XiSR3bU@FWDC?8K+9A7g3b=WWZop9sx!Yf&71s-I3n~%qp3}y9J$zf|XeP zlQ^Vi3z*F(B)L^-4s-4x=Lqx~%XolXEu2|M`{9ftjV+pvXa zYhZ#t_B9{c6UikKu;u|Umq&Uz;qlpooC&Tv0;O~qy)F5)u}H$~sLju7ZG;g}4XJj8 zBX6XBidnuz9YhX2i_Rj3@*bpcc)@PuUK-MV0@GM3tqoyGHjK)x$X^5WD0BoW&Dqc+ zz~*8?kU#vs>ACm(s^z+Wpu2ac;;Gt_;sk!>iTx7|LB6G_DDsA-d7$~eFpMk(-k5|a zddwkdLV(!Y@~3l}p9EESQt=cqIhVFF&O6FK{k2fytdRzIw zgS;QE3-W)10j|Ldu(Y=th%SEfU4>dpvJ(+WOhvAJH?8gh1n_)!!rhL9Y*gMr&O~8L zOd7=OjzCfVdn3;PA@iWw7M%;~_ZfHSHPIXO+hi%gs_z|vMvvb~8VZ5~3a$hcY;ahZ zp$_>`jU-a7Py=ZD z2eMHNX(wz&2?&p@+#k94*F3WPv}A(q0JZTS9NaDeJVn|;KCtRn`lpT3g=xQh#>xEq zf1f3jMvCtG-`SAvwza^L($HLF<8>rl_n(Jm2uYeZ;D5-&4wZ_R{+*0-bHn$qv~F!0 zFj(gae0>lT{Ck!CA;8w>z4K<$()ZDT9I%@c?*H;g#Ezj3T*=M^uO?;HiZFR4sha9x zzT*|iGV6;JPsOvxIXwAymG)|tW>K8>4_s?L>0r5eOSXlg$klR#Rg*{l^!-37ISC`H$BJ&n~^L4X*?o zpE?Wq@ezo7bar_c52!H~@L0&LsNxs&D;-G_1gIKO zdz=D^rJ^Z`(uAZ$@T4PhyO_b-YP3RZxHe{j{nw+j-19?S1)M#yQAY2JIga!YO#+s% zsFWcyHT?X)2Ptm6stKMZ=)|v%^wS5X)BO{J*k^vY%1o0*kQ64rWpi9LU~gQJcE>6z zKgNIhdM1rDl~4PC=?S_|*nUSgI)6CDHMHy%Wp*RpVzTa**}Xn3CREFF=O3LTm+`Y& zWjvrq$NmxZeY4OTzL+FYI8F#Wo_hqsW8(VeU5xFfp!v`*K_V?)cArwAU4SJME{z|7 zTvaVKew+0^q%X7eI4iGOC30ppDcZEhuzU7-t^SM}E|Wf9l@-h{Yiu_XvIlCfe&lxXfw{9 zgbvdB+DpmEvipZAiHQ$_|sEN zyd5`lx#4EntMehhe!8?-2=%XKMiuI{S1u1~ksKOOk;gS zH#`Hj(yroMy^Ufm)9kBqf;S2S{GJ51DgK$??@{v#qsy-SG_ZYJq9%TjSF!U-RBgU+ z9l64cHJa)1ICli%TnmEoB5RuQd#Vb5-cI)rVt!7grXGP-$}wX*;MuMrTJ!alVD|6k zuR8Jg$MCK>j!2!Kl<+Q_P@C`%W^<~O=k!H_$`sjqb94q9a9!jfWksK9gGZSW@7kW| z@-hiKTEC7nzq~mX`Lj4S&m6>|b;qJLZaB8nIq5vp4*s5bi~_6_e3N(Zn23Fi1LRr6 zN`zpm^9VXHV)db z1o0VU2Juoejyc5WEG>Q%F&q4CA!cH97m_%6F7B@GAmQtlv5uK^>O$VDfBLE+=MEQlT~FV3vT_sTZ6xcK}NVc&*QVL36;!a3-Yh6vfgEW3+(SXG&xMt2wH5T!u*tP)_ zZ_VR%g;ULTlXtviHy9O$gHz;Xf)euE^i~Q;?zUP7kBC+mq8ID3#gYqMgFfx|1e6}n{Fd(D&2*l3fn$!DKQvxG`j1D3lsC_RADk#2YA+}Ei z`Qe(eqDY5G=t;k(#-cLZz;c|WhGN^9w-4^7x0{r{a|;(CVa};GsxkBGxpyabc30^c zcdhlR>L{1)V<})a1RlwR;0=-^Frx}+s)Qhmpi-w?-l2T2D`A;CSt|7T3gXFC8(#ae z;+aMB0=S!2F;b41<-b=DILroX>hI1wYAf6FJ$Be?v4P@oT0e{}_N;&Yn? z$_06xGNofCGDe{0iqDNgyNpoh5fF_!W`e-2^Jh_;b1^Y4kb$`=Ou z(@yigKP<58!CZ$0s%g#jMWWtaN!_~E6qRPYXpVF@OW1eri{n?5HKJ$Uyqy){Cjn+u zP-wDyQDT}KsgQtrJ05>h+Yt!dI3Inyuk#3$WEO@J`{BG@>CALR?4sEk)@Z>mgjYmU zOnLiI)O{m){#4NA8M=`_81g-f6E+z^zbV_2ch+|+2qJ!Fy^zzqoIJZ&ohL4GAS%0d z_0LKtE+n#ROx^P7cxlz1Fy<&f&jbG@)c-80s!tpMy3Hd(gQiKY8QaJz`F%_vf&Z zfgmkgFU8}gm)sIRy%~DVq36%LWNt_*(%7$X(fNa|tU!d~n_?W14+BI~sR%4a;9C94rngJ%uQSVKW!Hox+gAXHhDw1SPVzXGqwXfQD#kG9QpB6D2nX&Cj zG|X4Gla>qSLH4A`SnW$BYcxrb*LgRl9-`hNYgKoxJ$_CJW-hyr&$skDSEIvRZ6Eq8 zlRV?s>%M`hl8D%bpNrmhlQ;sF5|iNnyjSZ1JsIiX`06veH-dBhZJ$wb#IAm2;Kh6o zcNvx#bOca&*Q>ppb^~O|>1HL0|*s<60g3@#tl)Ltvi+hmS zjH`ar$@4czd{JlNIXwgQ7@0YQX!p8|ik0=Po=sj!U$;&Y{If-v11_cYPfs*Kb}hDP zoI*_~#s3eZ#G zT(3T>E66TrA@YjfYQ%ESRX^lnXf}`mXe-dm4y{rl3yUj{Fii#y|DqE&gIKQVQh#*4 zJV;k>&VeQ&6>a1LIb1FVqgVO{)ULDyid}xh@Y6;BxyZPFeSftVOz;NM=vkK2C{$-L z_4nbGD?QGaRlTfAwFaKqJ6Y&96gi5o{M=P&RS+z&P!I6FA+kekWfju@R*Vl4tv&6l zb`Q1xn(?-l)^_tPd?zQun2zQjO5W8}AXKabOH!e2)m6msFovNL^0?E8on@wfQk)c& zxJHT6J@Ecv%y`nuxJ2&$h3FGx^+|wD^b>D31cpPThOe+pChNeZe;2%m9@xS%?TIkc z&>G!6__zmOu|fBE)iG~D9{4sKjoKm+-;3{Yhz$apGB8>9L50RSPzO_u%ISJjS_3hNr%mh=&KlBP69MMPn zCp0+gpHWeYHzRSs3JA>YYzpREML20dbgUpuq>~PS4KGP2&6N9F}nEF{xx(n#CS-a(O+K9(lY>t7XpDUcWkD zEG7ykSUImwR>O}hqT3MRJxj<&n;b8KMyFqnYbf!wA4QV1h$X3uw!;Hs-I>e3C^>XT<>g$l_FC>>qKxmADz zWp8W=puyIzm`9nU0GDp<%}&(1&7`zn2H!P{cdFUox==hlA1WAxx2CcOu*d`&?jK*0L4;w}?|%^H9Y zoDn03yW0{Pwyt7|+v3QLVbTrpj)d8HhBO8F@#hhU2bF%-edui3%gOZzir*~7w5ZP} z9|Q$wI`#~xv)lCs)($Wop2rN5FG#*i!`2-t?B1HIYeOUjY5cTiYOX&;cx|0|AJ}`J zpL0{Dn0iQE8{gW)v?w}FjVoe|C^zLs=7zRZ3xZM)TDICk*FqiRI?B0d&> z0zOghSNTEoqh?UgNUhqvm>(C_tx%<-Ck2>CKae2KQH*~$8_bByQRZm(Hp4Im+e_56 zKXMZ{YbuBSQA+X`ht3B!ZcR>aY{Wmg8BHq=C~?f>+#YS zV~FJjTqdZF$cdG!R&&H5E=xG7jtK+67N)i^z`VknZMqw&vxIBfGlG7uwG1mgRJuI~ z`|I97hdBp#YQh*Qj>=}-t}#TLDUB#vQ6)4x$jvG3WsGj-L)u(u;qA=XzL2}idgK)4 z6mPB=gW|q3#0nFcJL2n!m_e_5*>4Q32O-<)sfn_gIYXoICc#^Z8O^(NFDoAWR^R)y zy7^J)Gf~Gf)L2MU@*QIJ&VxV8#UJ{1kJlqVTCTxJQhoDeSrZ9dvstIZq{UwhG00ilTTr0DZ-wx(IZ;+Dzh0@QIil-Qj9GtW0A= zujY_NMHi_R-f924FrP@m&1zjL2+E|oSj7|7rKGo4egY|Ha62?R*i92Vig4dkxmIny zgZ)!sM&;iuCv3SJ@LOQ$d*jH$;H8T9<|tXg~D%JhU}zBR0F66GE0#5 z8k3Hrj)vHvV^c-(Und=pfzr_On{j0Z-1J-sK9A-Yt`o_Aez5RMSn!`>rriL#QOKf@KT>gPdY>d9 zxdqOJl_igEB4nmleWK2oUc4;$XF2BY zsQB^s2dAdzWDg8ZT=s(RuN`~`phPWNN?)4nvu^<0sTs5u_tXP66q^y=Rrx_8qg?UR zJa=|WBF+?k@8aXC_^H~wUDYp9)La%de6)?P>P&=@lAqrP(Z?D}nT(|O{2)t>H)!)A zbaxz5h#^koJhxweqph!uf+X-{9)hi3qvW|S<-!DffMgEz=wuxmOxmLCW5&r&H ztwk$xyVtCYRy8tqWnrI@|M}TE^2!`zRmx{%;1%X|9JGY6d&AYP?N*Uv=47++jZLW; zvm0}PWT)?mWsLp4OBsu7t?fnEriwk(b-ed@w^Y{e-tVn{m($#J1d?uLtD?3;)9s?d z2+OmOEBSL*%hj^#m$2Q3r~}&}L z0KLe2lj%11V9Q~7?xU+$F{*D+KSazXP4vG%%%=arwo`-JFl7y`B;7qFZ_;?4A}=T| z0ExE3vrAG-=ht+nZevEn%~j2CNH&|?^YTTn)Al-580CxbSCpOV{H}eioP-{=d&z#K z`_uDD#eA96t84eiRmwiJscYo&63HrFPqS)KQnK;a47w?|wT2sTeKsz+8T-A-4@ZjM z)K@JY7goMFqTj#EC`Vrq?=ABn0RWNJdozeDy=*Il+}pIkmGpb)Ww2CgeWo~S%VL+# zwKPA4i$rv!R{TM_xF56TZ!i#ea&B+ew4PC`&@50+??7mE06R0=tK<|CoW=u$AeQJ; zmV5|41~-6F!O(;&mOU`C9R++RMc#^OAC|IV_CNie#) zOUdU9KfOS2T4O2fdVzZ+wG9Qr=t?B&R$vtV_e{o*U60R-f9m7t0h6R$fwn~?Gd7Yex z4#I|C1J$8)AF+Q%y^#s&levWrt;0$#A@2&Gx4!UF?fqVP%W36(DkPtLAx{cTRdW1n z2^xKH1p443O~Ln?T2Z3v4D@85;lEN8s1KaNjmYzJ-xtIFDmR|%G)3?JF$LP zzBD^{GHt15_B)|)DMDUx*-7Y%*^c~#9r1B-9EhF(%eXPejyuk1XARRpo>ct?juCgsG9CT&?l)ip|yGw7l|#S z>5LSWl8XM=V_DPoZ(kZ*{Em8ubW=C-eSahW>cn$X@zpY<1%16?1+gn=R`QlQfo_GJgrYpJxI#~wiijP@=MS+1}g zli(jYr2J&pY*LXwkbms6pMF9d!mGt@&aS2Mk;L{xzXjfK=F6rzsn8lPj(*cu%8w-hBz|`{+{st4E+s>;d36)Khu; zj?*y;wVQ~hU|jkQ%)%x^Y0v)((pPfHKP9AJOqi;^276A=RTK6`SIyO2gl_Mct#F7; z+>V6pZ6ObJA=*%~aVA5Of(D8kjhg%SYYB#Nv@66oKw9-ouN^BsD`-)`y6J(HtO1yA z3U%(+7qR~OpjlRN(HeYad^0I~-i}(@yyU)@EU0!(1ixl}iTZ=wtwBoZrCHuqyWr&M zUk}t(gz7&l!0Hg;y-O(4>7i}ti==77y0NsHGs_X^zGPsr8cmvj&Rvywb3LBEM*3-B z>|u-G3-E2ODfxKZfUtiAf-a|0AIj2437=LNghS|oL=9i{iRg@yYjo$HuKfnwk~dtJ z!DF`rfsh;h5BkRVTW`XqiR}GAdAHEU3AyPFZ2_LCvAADsaf~$@>Jgdc)rAqaZ0YQe zU@`%4orh=mis60sx|23Jc}QnYi|yk1QGuzO|q65{Qi;>ajGC5 zPi-}jD&B{a$%x-yD#5lUP<%th=xwRKY@+rfk1!dZrLH67aFn13RLiuS15hlhIK8^qdgNMZ4jxQnD3RmR$JGtsT|7bjRQOs3vL(>cb$V^{wC$kW5oiRnvVC%PW2fN0 z{*LYzj9_q1#VnV1!C$Da6)`JREhZd!SB++v_~^0ATgSTfm_iMQmK+Z`|HM2|t`GWV zn2U89-!Hko)-KoeTSNtvtq4MEPjkpFD+a-aukr#lGYLC|D&<#3s2d47Zridc63?H+ zm}e|uLo#wkKm0|o|E-}!CpN@QvEDpxF?AiMZuzutJ`LYAf1x8Hbr+dJH)s;TxHne< zvA3rzurZp_*-!g>^mu54o$r1Pmkk`n?O;98I=WLbz+zn+gkmK`0H#e6t)(K7Z*;!LTb(|@1=gUwsk>)(rfJ4hvBqT4P@^cI+E@ttx z(Udv=j;cv4_95W6G${C#yO7o0eR!@yh&S^uA|Lu?A*1M!e}3#fbfC0VBQ+EsPm4*b z#>gvkcGInXD>ddL`bY=v(%cT7xwPj>L_s^KH^vAfw)q~LvNQELe;y2Rj(gSHYYP;S z?2+TX2dd=9-+FTVx|)PcD{|XQw0xB@P9@j1(|8}vY(Da9Sda07@qV_WnHULPHVQdF z+DYLv`_~RLqFv;fjV}(nnPnmuo(5@>dk9{0T2Wa;h_lV?WqP2boq>UiAKO}p!WqF!M!d2-g+S~kE|{HzLzl7;RuANL;SM@k7f*!>RnP_On9Bh zPup#T5i!2fF>w?zx99XBKj|jR|=>Os^+B+h85W+5jkN)&{J&rV=)OOtGO}-0Ab?IA=eyk3uS% z_zte)Q?{91Fq{nWoYAgXT3?6dJk8w7oG-pkN(mzdxR4=(X}}_!^key}S7nARcOb~? z$V!_Wf2z1%#cXeGJc^tK#*rT5E7+g3hWdP`8)yb%r{yh0@D-txBIZ5MdjWqf6w@Bw zGcHbJm>ie`;ndu-1lG$&z1tUyvNn2Jy zDO~xT2K2lfayF&(Pa1O65&0Gxr*1SejE|RGs72;S!6BOr-aS7SKgDH5$t-;m)dY#P zd9(NgEyfte3T*?>ibL415`1dsxDHiSa)OW|US9Rc;FIy08Mm1j4bc4n0W0Ix{zT@1 zr~~&|Lj%Wv%)`vro)ZxM*O!c1jiMinc>S0yaikU-1|z^7>KwW@xD!Zp#h2T6Fh z+OAt$E?Z@n5j{?BC$4k|QiHxMRg&cr0=lMfCoKUesN-ZYBR zRy)?N>2A~rB7wxb;ummSs_SD0zE}-PJnHJ7D+)YwmuCxI6>dMm3J34j+Lu zdcI-DUesVZ08K+2fdv0mQ-&n)_Q}s@_1Gn)O?$FvCQ7Dv=NUdn2AZR^wd5U~%mzdp z5!8vtE1HlU2%d~aF0Gjv!nYqT?<8c8{r(}g zk~aNecS0V@q{@>%5<}WJyohfSHLzQdsK08AH|16?>ZcSeZV8|(mS5AX=fPvYrVR;g zD9L6UKWCllq@d?F#`zPI+(PJP`r0@UzA&_9mQcyRY3KT)5PvvyD8sApR@?uRm;?bD zB_U<}`@8JMNq^&Oey&w0sX{jDpRU1IbeL|J54U6;kTcxHo1BBv$6BbOBw=#9( z>aI-?iiRp}Is)Yn%LH1PUS!`xsaEtErG(78Z)~1;fg}1hWf$+f!(UhjkSsHn0~mKj zAsd=}8fbtTIb3E#_Kp49Vx@?yKmRA_0?{BszE=BdkSif6&CPCFf@?i#NBmar^3tO_ z>JOIV$Lf<=L`Gsnjq=~+%6c!vQCxx;&$OgkhP^T)ayaE*;Fn^SXGkraAyAGhFUqRO z^Rt2vH-zTAitfJfQ~^CBkNzbf54)P$Po1Yxu0W51S|J^NRB$DJ0VA={al%{zv(4B=Y`?rq_o=h_&he9rn za`LQlYskN^NahQ$?sn0xymn&u#U9&}*RV%bj0-T`kOtjjPA2Y~SF$C?NG1Cv_@42v zM8Tq)oY6m)-$fV?Iw~C$*6h_!>Cg=>lf46;lSjWis7=Hdo}x)vP_3@GC(iNHZ=)G; zeUWK}pA3;xs3lYd09w~+Y%*9Aec_9|1R5vD!>N@0G@v`_k5th5{51Di7X!t^u(eJ6 z#{iNznrIl8A1GiebH8D3BS?MDRezy8BkUDL4Y6L12nlO_l+BYZzPSFV(9q%{3WsF% z3u#7jmaW&p4IJ)`eR^cRiQ?M;C!5E^bx3fJ;r(MXQ@p$<)Ib0?9XB80lZSVUTFOfp3mev+v)c8Ufx~S z(gjW)eS30#LHm5tFkE_lNr*Ui1R9e;eqgv9IAJD?V3Vi?lrdSu*+1~0ulW01zSuS` zpdKw>69TbSiVK&c^-fd8tpO#vFWaDH)sW7|s@>8FnX?K}{BwwGf`KkxiEi3cxb3E} ze>ywD+~;DM;dnvDy|tI&+?s!0u>3d?M89HlVHl(Fd2(s2gG8;@S}9lTo_wKVB_gY& zFGJMUT;2BToahgz%PMnq@o$}Of{p6z&UprDyN{yvAH-=$)5N));))V_joju>38P5A zw#Q=!%e?~w^3aldx8XQ7J+OGCl(WmcO-j-S#85SvN z1{1!hEF5b%G|RoO|7sAbjCm}xQ{y^qHe9-jjHn3FB|mWGneP~Ui_nhPeW^g|$cT1W zw_ODKouoKJa)yx(dcHU_pPq%4u@h4dqgc-M$^1A1;Z6+yx|xD^-FL|=^z_Vp3+y7H z4kR;Vd0l;u-D$OC)jN?)_0yk$Iu<53yT|OEwbFq-dI@XP8!o3x+wKtqF#Hn( zI{UH_7h!HmaOqqrtYXOTefzrI-DRM~FnYO7-D;~ndj8H4DCjc79{WR09nl=N;=c=} zrC@$jHUjFUV0eva-H@2_!!xoOUat#1RvbHjdUAEh2c$0LnwN94N))k`>)CSGqHOG( zzUOkAgX325+maQX!PnPv_9PHW6tf6Fj9rt-Gulola>ov+qV9Q;r+LuMAI)}V_wM{x zpf8-tleHRg`b$&2LK1BBk0^32Q|rl zRDOoBAFuUQC)fTKeN9Av#Z-b{Dl=ilqzB1Hwrxd-TNaw#{q0s%_^SQkSXlStbE4HC zfBO}cUKXQ}h_-Tz8-;=$(sJBubk6j-fZrLq7VKMCsgwZ!3dfSCdmzR=p&rd)F7mLT z1w1q8@=)jG{iF@q)KJ$CcDHO;#sbjI>ijq;NA0coZ`PQTd84|5O!lo)vvpp(Tx2#% z?WU?qxboikE+k$Ctqw6jFWk?j@fOG4Rr>ZZkQehgL-G~8xNE_|3ehdbiZ7Hb8-67) z3I7n={y6RI9ftR-U7aDJm$3;eO_rIh3}!|*mdyq00*=74V(P&wz9YXFA_MX_%7$JA z`a}BaC&X)?em|SEkjNw*-xF*je!Fd;{pX+eJ(Hb|Yt%_^YEcmFs<~52x|5qs$NfTn zTcAbzZ4jdm%v67o(E)e~_lWwlgu}Rg$s#ew>##AQ{p8yACFY3#9zhD z?xvQ_T4+1I$Ql^AOj7Q8Np|`qpkq8YRmblWm<2B3-{{UNwp{V% z5lCSf2tB%Jy|^%7E9(Oq1ZQMvAAqPGXgRSrJ%N60b9$bU9cjS0X|PTKjQ>*vnk<6t zP7ZIvP<+9(lgQXvG~toKC}sxs2Ew)~CQiUKfzLP&YR%X2N@w(GRJgwius=&~Pj`KxO7@E! zh2J~Scih6L7=&o37z|eWnB1MXUnvtr7W%!-sz6IbD_I0pR(ozrQZo0Zap=-lVz)|E z5&6hlq4Sr|g-MUAV7fh5?fqR%%sA$|YjPOy$+;w9LA|Ez4XOZ?&`~coxJbQ^e&?zi z6J?e*-Nuaju9K zW3Tk0zHqfC)tnKB6Av|w%qoq@4~BP!76VEU?z@m*1+F@$%gh#n<*3|#7GNIS9Dj=9 z!s&lh7^HL{5hAk{@ucje)G6K4WWehcn$}MOEkodJ-#{NfM%yKVdyLOzjvCQag)W}K zFs8wm;DP!;HcByX_7p51(pziT^a$mE=@7X&s-9oAP@+x+jye>|(>SvYuaH_<;`xE&P@GbF3@ix#m>EjY#fa1HlL?qC7)cNya!bqF?BDp3_tXA}oBo7ki9Ryc8OOCYj`S^xC;x+@}i`J?T(0`38hiEw?B9anL5` z#pip)dxofbgny9r5MrFDVO7~jYY|0mqmvKK_KrZTU4Ir-{E*%VRYp$lT;h7yP@{I; z`ua1g1rPonAOzYg6b+0_uU($^99ylITU|Mv4jsb@Zwr5F%8!-7kM zq3sBh+2yk}p>;@&y=nIR9kUk0t#6Da{F+0n{4L(u8;EB8ug`ClPL+7verB8^t;8X1 z<;+PnsFOE!+>Gv#ZV|gQ@M?P_Ygl2^RX$Vh;F)5Um$*lIoq$L%hpZrQ?!{_Euc#WF z=wT9&uaau$dp^k1-e;^DlhkEDCns}T(+BrH zA6WLgsKE)EQ?9lxT;`UJJ9Z0k>I=mppzG?KWV22WYnk#jEl%y5mv7U@8SvkByomx- zZI?tWrgCkg;L4rrPaX{1GvDxyx_n;b#cO@2DsBDhSJ<)0MApttOZOY-wO}Q-{uVm(%|)l9z>q)g}*;~?07 z^@rz-*P?h%`(7C9-PrIn@WkyFp2O^uYNrB}jJMQFsYtf~ zfiRf|$cKAe4cPDR!92KgukS%F5?MvaaWrBGBZ0;!A3viErkG85brx!LAAwvxPoD$M zB^=ut5ME!$g_sR~HxH?edQcPj@zyO-=h4nf{W+0(5@pT=wc1XjBwmaXC2e{0_RJ=K zW4+Lay?g1*`E#e~hGgBB11vR_JKZHgE%P3C8s;O~3ewi@(s}E3pQagK)|s?sxKOrd zTRZO)^Z!ZJylrMxZjbwt;=H=kGAW4J!nRX_jOesrvn2$+M^k3? zETmAf=TchGJW46aDz2t8C@Oh)=haO_0aZ;i$>zZ!mBHGXPfwit{ElTQwa07x%{4~( zUZue-ST;4tgMz+Cj?;TCD=zgx0M7LNq|g1Fszt{k%J^LJn;?(Eb*{ z#a-{#^CvXvM?Wdgb6ftc7R)yE^mA!Mac2ji3wLc-I@BFEGMaCvgLw`q4Tm|30UNNK zZOe5(1Wvb>P}7t&;&HD47RL+fZmH~EXca;&wvabsx%{a* zEdHZs$WCGA^IJJy=;0gRpCVsDWv1j#r^3bO+x6pTqAi|()-7;Ex1#ni(zjEMFV-1& zT`)|#s7C=`3ev1=F=J6bi|^4eMdecOBR@ckk8T^AQa6wl8S@HO&2+On^9pMLqCu?-`0)ZWpB5J%gm{&HjI}PB5 z@iv$WrA4l^!@1J*XQN!TUtY?_Wm_gBwx`sj&gL5T->Wz_o^=^Uar7Lgte2aC56>aI z$u}eYGJ;I`P;wt(m&vIa6U-bEb`c0kExVbsgagoRJ zx%}iYFn2MCC^x+LLj0}HaCH2?L1@b*RS1In^F@J?_*9u>#ne21u0sB zTmHPWIHki8WuD6#6K*|qkgcg4)aNCW%QH@@*URE2X1v`+$dPSkL?;``q3CYnKR?@+ zlpbnk&A$A?aou@5Mkq;4{>E@DCECR<$V5M&q~^-#Wb)mteSrcy;cIt&d{t5Jgp#Bx zZr4OC<#Slw8OT^(xw%nM(BnI)F}euN8>&_^OS8GgQ&R5I9~ZJ z+W>CsXzUWF5QgP=f^a`I3N8?*kqS+8rn|c*iVVO_)9<>DK-Ys&;%QB?CKlDyhdptq zcWCy;Gin!XV}DbaP`jxkDp7Z-2HO|1E|a|ovwmXdibarp7jOcFwh=7~zuI4-p2hiz z&kn)Gn@wf$SsHcjBHnhj(^Cc|)~F=p%#Uh?cYD*}HlkI(U#-Zs4@*5!OKkHJfOZQV zltewu$2)c&XrVu8#D#4Ham}9oRYCrmZI!#EuEPfoeT`qCSQiX#0NKhc(VY}A**AI{ z^&#M)NbnbM8}@Qe%G03=s78`dVFW*nuWX2#g>I|cPx&qd%NpTt&2u?kThOgn*t_?i!i_o;^cWJ0NvZXimB-I;HU`zgVBb@t6KOy zW$a8eMAlV!Mj_(SrQ5Yu1`M4>m{I8WJYBDb?8i-!kow5Egxd!ltm{K&zaKT-cIdla zFK4}C5k6;{L7sYmF-#KrrWWug?_rH2kx9=7heqWV{rR11+npX@2EDx>84ryq7k;bN z7c7f#(+lzup<3x5`atUGuglHLrdgd!I?fx(?Vb0+pF+0PXR-+QLyT@XnrQVu^83bp zRSKN$y`$Lac@NjZbrbS5r?b6suuxO;m=#-As^oXg!zPYUlW#5&$rhl4jC!g$f{CtPg4Kc7n%$-2Jp${`1nyoyJe?S(rFmzc8GWxQrI+YHI z&tDxO`BYr{v?W*coKxgSK)&n?u1(9qLKSTt`5so;+nMvzW5AioWmm5&3>h+)jAf^< zTD@3xGFR0)^#dyfI^m`+sKvWq<;&f9S%VitN?yYndk;K6b?tf4zDtmcY?q2jlpN3WZ4~N(U zAu2B%IgqthMMz0-B2vvS`TfC!W+v@t1kI9e06$)+$*`5(9W47VRQGsq(w}M>19o9J zq#^e3jg=pH_nF-et?$dM(-aQ!jryy8uirxg^Vxyksl?vL>1Xh0#Mm0BXTZykKpDL` za*UPj{Z%V(;@Rvh?_g|qYUSZ8zUtC?!VpmNl^VYj+Kl}m&r07wo9-P5@Xq0*b+-1E zc2-dGMbAu2hgV$7qb%Cc%IQc&fvpP;3m58LxMCEis=f*u{>TxFS>lxx8cLHaHNDu+ zI28m@IXAHer;q<=LlQgJGvHC~`mib>_FcwZZj>(p&LoQZgixVDidZ{czw!^&n8NX3CN)`b6PklvEck_uPPgF1{W7MQ*DntDF)6)@q)%fq zYQ;7ISfjB9yL{%N$5}W!~L53{QYn@{Om&79GV})9WqaB~=QvDxf<9_mZ=)80 z8LMd=q`f5A)knHxB-2KGH62nNx-u7aHdPjaH3MGlk*=$Gb$Q5p zp{_ek!I__h9Zrfps8L*K2Y@4LADds1D7+P-RHjVO7s2H;ZHvz=gML1$n$Pf9ao&qk z;A!KT{+TsVc#x_~sH3Z*#m&yg|47&Qpdj~TPbqoxG5c_DSLhh4NIV1Cfr)8e;I_zI zg{NrPzZ!$LSqq!}(?12$zfDZt|FKx^=oTdzk)xD(!?o5{wB>7N8rL*)-2|_&p4A-Y zu{i-lzMNE9G0jwV{+|goBSDr*usx9c0|{zmyu2x$GklIEKo-9TzYm~V=_7oD2T0=a z1-n|90wZqR?9bEml-&fNeS~h1=@w-CvE4%ZHB-Iw+SMJBfiRbFd*2X3*3sfQH%aG# z=vdt)(M=gs2N4mc)038EI3LLtWk{XbU7MLU>fU4sMf+MNKkiprsjZT@ z!o8$yYLzxGehb4bC;`BmzDn+jW4Y;VI{OzevD;XtE3ny-H}V;W-+CllnzS77Y?Y5d zf(vv%%$LU546$TcUx@qFR=QhWf=Qe@-(cI$>uWEOs%wiMmMi3!Ld8^)!2lv}aqFVI zV|-3bHh2fWpS>|>n0x)sd=3-+8ErLC!mRuwmXOYUD=E1|qbqvOLci?|n@!$#)IQ0!dF#48@3Kd!D&(2# zrbuPrGJ>4Q>V)li{REdF+su3`%}@&`AF^d6k^}>^YgdvpP;<2^EJy^Sw>qAABS$r1 zD!dITjI{&0)M@9F^mLR|e?@SZ8J52fIeP^9p4)+1kE{E?+PU_xrtTyj>$*^@jRY)O zB>@ou!4n=L2$9sHu!tD-pga~xRgeYt6ltrVK(5Ln0+oml9%36PhlLa+R)UIHt^xr< zkhm(ZxZMbew8$$22sh;BW{1b>{t^E0J^5ua-CZE^O@~pQJSy0duh&LSp?cP zHFWqNc+ubrea3hDO6#gD`^=n|oV_=6HmyZ5`-@<((^W8?YttN16EK#|E*(1(yv6u? zL80fnks%v}dB^H}zximaiQ+{i4}}J&^L-EeRd_0Ty4&1hPmRnaaMqUp65}WypE-E@ zKYader6SMdJZlTnP0Q`!#%aTP*LRl*k3z2m_$9u#RdSf$zhxI# z$CN1bhpM6|ZMKe<4L8xwRG;gp_B0UXY~xWC8qF|fOhNn>9eFeQT%G`^Ip9XZ_Cj#iiuCY|N ziF8fE3yqkMLsdb^9jU5;mb@ylATMz*v7Dye%G6pvsM2aP+u?= z6_nY!XWNvmU;P|pS<%%`b^Lj{46eNZB)H;bEqdf`QH3ahn{waz9?g)xxSxbHwzurq znj$A$!>_#N*d<^zErY4u0H{@}1yu9_=I;oLGDW-JI`X{mw+Rn;m$)$lxan@zIge79 z>cAveWB}q17kmn(J_8BQNARpua$@<%I%mI`20I>3ks}k?PgUk-Ol>B>@cUtr^@;sb zt%0;Cz9Sj4iy3*$9ON5FZzwuo9%LX@G9ZP10egcu@YN{%K@x$0;B&%5{>aQEI%rct zs5&bRP}lZI<@)#3R!B9&3(^iuGYEyX!5ueq*Bp`3%%sG}beLh__IUk)cdV@pr0Veh zqqb(@X0Do}JG||Y1yK$VC5bHkQrH&ym6RF81JyVnDL{!B=yAGU&3s(~aQqqdsy|-fkOrow3f{q zp1lUP!HGrwMFqV3ej|FP&8UgLkpSP50FRBfL&9Y9EOQ)UPYeejUWGM*eR%TugEHk# zAfth*r4d;fXzsaVP~5Et8e!G5CRL`%^E1yD|Fga;xx(_s^96^Syu?<}p-{V3{Lkb^ zPQ-Zc*G7U{$e!6-)ZI5;_y?7PFH6l$c+oD{W`bfMZTN&PTiU20Ka`99^c9@Bu$PIA zrtB9F*N*#4um?}8rB3nve2l5~09XWzpLGb{+PGvOa$raoSNDl2toW^Em6 zMEDww#^`)82S~_G5$;WqYr6xVcHO&MK&j}9-fMXws59b=w@v4UBU?<#)2B|a%|GV1 zI?%46A;w2X=Dg&s;25XccBMKfxRhcC$!$(R<vAy{+6KelY1rOMM+-!fxY;6+i7X;*Go#3|pMp-!X8#*tzn~^CZ^f_IgzIs(-h_ zl%CuBdk?FwmB^{Scsv=Yr9Xi&-S$(O={hp6>^RnKUt&Uwg~A6H+v%x*?8!2wslYam%6QQ$&XK|(Kr8mO}|kp30H#H?O2F%j)O zv&-+OXexC=ADzgafcNgXAs{Vw22zMRK?@;$MS`d6Y-#gKI5AZR*?o2@4xNUtdP~EP za;Ler|I+~JgA_BT!etP4**g6dd4))dm&}pzX2n+#sT|K|D`|ae7O1ZM4vJpnw3BBz zj;0V&4h9n8e2BQKlrNf&tg~L&)#XwTZuW^L5b}r|%gq7_Km^b+kff z^S13UarqDDHfIQf44|RM|F%9+U|n>#N;E7(Q4j zCYZF$(NwE4_|1_nE=%;o+aXD|+Jy%e(B~f{=ib~!V$|RTiMoj7KLN63~g26dosfUSW6U+aY&B3Lynze+)Uh@&4L@FdB zE`cc_kqn8w%q=b=Nhr|9#jc=k_H;w)aVg_rRBZ1qQQnEXtNLe7Xvu0VH6|;hqrLXK ov2uI2KVJP!!*9*MvFl`G2uwU!Y})(q^T84yoM9mzmKgf}3%3T!MgRZ+ diff --git a/examples/webgl_lightprobes.html b/examples/webgl_lightprobes.html index 20b184f383fb03..c581caf31cec7e 100644 --- a/examples/webgl_lightprobes.html +++ b/examples/webgl_lightprobes.html @@ -41,7 +41,7 @@ // Camera camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 100 ); - camera.position.set( 0, 3, 8 ); + camera.position.set( 0, 2.5, 8 ); // Scene @@ -55,24 +55,31 @@ const greenMaterial = new THREE.MeshStandardMaterial( { color: 0x00ff00 } ); // Floor - const floor = new THREE.Mesh( new THREE.PlaneGeometry( 6, 6 ), wallMaterial.clone() ); + const floor = new THREE.Mesh( new THREE.PlaneGeometry( 6, 6 ), wallMaterial ); floor.rotation.x = - Math.PI / 2; floor.receiveShadow = true; scene.add( floor ); // Ceiling - const ceiling = new THREE.Mesh( new THREE.PlaneGeometry( 6, 6 ), wallMaterial.clone() ); + const ceiling = new THREE.Mesh( new THREE.PlaneGeometry( 6, 6 ), wallMaterial ); ceiling.rotation.x = Math.PI / 2; ceiling.position.y = 5; ceiling.receiveShadow = true; scene.add( ceiling ); // Back wall - const backWall = new THREE.Mesh( new THREE.PlaneGeometry( 6, 5 ), wallMaterial.clone() ); + const backWall = new THREE.Mesh( new THREE.PlaneGeometry( 6, 5 ), wallMaterial ); backWall.position.set( 0, 2.5, - 3 ); backWall.receiveShadow = true; scene.add( backWall ); + // Front wall + const frontWall = new THREE.Mesh( new THREE.PlaneGeometry( 6, 5 ), wallMaterial ); + frontWall.rotation.y = Math.PI; + frontWall.position.set( 0, 2.5, 3 ); + frontWall.receiveShadow = true; + scene.add( frontWall ); + // Left wall (red) const leftWall = new THREE.Mesh( new THREE.PlaneGeometry( 6, 5 ), redMaterial ); leftWall.rotation.y = Math.PI / 2; @@ -141,7 +148,7 @@ // Controls controls = new OrbitControls( camera, renderer.domElement ); - controls.target.set( 0, 2, 0 ); + controls.target.set( 0, 2.5, 0 ); controls.update(); // Bake light probe volume diff --git a/examples/webgl_lightprobes_complex.html b/examples/webgl_lightprobes_complex.html index 54c2775e85d889..9a7ee2f206e484 100644 --- a/examples/webgl_lightprobes_complex.html +++ b/examples/webgl_lightprobes_complex.html @@ -42,7 +42,7 @@ // Camera camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 100 ); - camera.position.set( 0, 5, 16 ); + camera.position.set( 0, 2.5, 16 ); // Scene @@ -271,7 +271,7 @@ // Controls controls = new OrbitControls( camera, renderer.domElement ); - controls.target.set( 0, 2, 0 ); + controls.target.set( 0, 2.5, 0 ); controls.update(); // Probe volumes From b8d89905e5cd3ed7b5332a332390102266d12c31 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Wed, 8 Apr 2026 18:19:11 +0900 Subject: [PATCH 21/22] LightProbeVolume: Add progress bar to Sponza example. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/webgl_lightprobes_sponza.html | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/examples/webgl_lightprobes_sponza.html b/examples/webgl_lightprobes_sponza.html index 49a5822822f303..550618bbb111a7 100644 --- a/examples/webgl_lightprobes_sponza.html +++ b/examples/webgl_lightprobes_sponza.html @@ -13,6 +13,8 @@ WASD to move, mouse to look + +