Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
626d40a
IrradianceProbeGrid: Add position-dependent diffuse GI
mrdoob Mar 5, 2026
7150518
IrradianceProbeGrid: Add examples
mrdoob Mar 5, 2026
1d3a6ba
Rename IrradianceProbeGrid to LightProbeVolume
mrdoob Mar 5, 2026
4b5df93
LightProbeVolume: Use scene.add() API
mrdoob Mar 6, 2026
d495e9c
LightProbeVolume: Update complex example with two-room layout
mrdoob Mar 6, 2026
9ee109e
LightProbeVolume: Upgrade from L1 to L2 Spherical Harmonics
mrdoob Mar 6, 2026
bd1f65f
LightProbeVolume: Avoid per-fragment inverse(viewMatrix)
mrdoob Mar 6, 2026
38bab39
LightProbeVolume: Increase lookSpeed in Sponza example
mrdoob Mar 6, 2026
0b6d13d
LightProbeVolume: Simplify view-to-world position transform
mrdoob Mar 7, 2026
6d670ad
LightProbeVolumeHelper: Add tonemapping and colorspace conversion
mrdoob Mar 7, 2026
1426bca
Remove webgl_lightprobes_usd example
mrdoob Mar 7, 2026
1cc496e
LightProbeVolume: Use texture atlas approach.
Mugen87 Mar 17, 2026
17253fd
Clean up.
Mugen87 Mar 17, 2026
77fe75d
Improve nomenclature.
Mugen87 Mar 18, 2026
1553bb1
LightProbeVolume: Clean up.
Mugen87 Mar 18, 2026
6212bfd
LightProbeVolume: More clean up.
Mugen87 Mar 18, 2026
a3b01bb
LightProveVolume: Remove useless check for coordinate system.
Mugen87 Mar 18, 2026
fe13d18
E2E: Update screenshots.
Mugen87 Mar 24, 2026
bd68af1
LightProbeVolume: Update API to use width/height/depth.
mrdoob Apr 8, 2026
f001b26
LightProbeVolume: Improve examples and update screenshots.
mrdoob Apr 8, 2026
b8d8990
LightProbeVolume: Add progress bar to Sponza example.
mrdoob Apr 8, 2026
f00eea2
Updated builds.
mrdoob Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 91 additions & 6 deletions build/three.cjs

Large diffs are not rendered by default.

99 changes: 92 additions & 7 deletions build/three.module.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/three.module.min.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
221 changes: 221 additions & 0 deletions examples/jsm/helpers/LightProbeVolumeHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import {
InstancedBufferAttribute,
InstancedMesh,
Matrix4,
ShaderMaterial,
SphereGeometry,
Vector3
} from 'three';

/**
* 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 LightProbeVolumeHelper( probeGrid );
* scene.add( helper );
* ```
*
* @augments InstancedMesh
* @three_import import { LightProbeVolumeHelper } from 'three/addons/helpers/LightProbeVolumeHelper.js';
*/
class LightProbeVolumeHelper extends InstancedMesh {

/**
* Constructs a new irradiance probe grid helper.
*
* @param {LightProbeVolume} 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: {

probeGridSH: { value: null },
probeGridResolution: { value: new Vector3() },

},

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 probeGridSH;
uniform vec3 probeGridResolution;

varying vec3 vWorldNormal;
varying vec3 vUVW;

void main() {

// 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;

// 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 );

#include <tonemapping_fragment>
#include <colorspace_fragment>

}

`

} );

const res = probeGrid.resolution;
const count = res.x * res.y * res.z;

super( geometry, material, count );

/**
* The probe grid to visualize.
*
* @type {LightProbeVolume}
*/
this.probeGrid = probeGrid;

this.type = 'LightProbeVolumeHelper';

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 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;

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.probeGridSH.value = probeGrid.texture;
this.material.uniforms.probeGridResolution.value.copy( probeGrid.resolution );

}

/**
* 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 { LightProbeVolumeHelper };
Loading