Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
26b6b53
LightProbeVolume: Rename to LightProbeGrid and improve nomenclature.
mrdoob Apr 16, 2026
f63ab2f
Merge branch 'dev' into gi
mrdoob Apr 16, 2026
7d70745
E2E: Update screenshots.
mrdoob Apr 16, 2026
cc7a4c9
E2E: Update screenshot.
mrdoob Apr 16, 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
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/LightProbeGridHelper.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 LightProbeGrid} 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 LightProbeGridHelper( probes );
* scene.add( helper );
* ```
*
* @augments InstancedMesh
* @three_import import { LightProbeGridHelper } from 'three/addons/helpers/LightProbeGridHelper.js';
*/
class LightProbeGridHelper extends InstancedMesh {

/**
* Constructs a new irradiance probe grid helper.
*
* @param {LightProbeGrid} probes - The probe grid to visualize.
* @param {number} [sphereSize=0.12] - The radius of each probe sphere.
*/
constructor( probes, sphereSize = 0.12 ) {

const geometry = new SphereGeometry( sphereSize, 16, 16 );

const material = new ShaderMaterial( {

uniforms: {

probesSH: { value: null },
probesResolution: { 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 probesSH;
uniform vec3 probesResolution;

varying vec3 vWorldNormal;
varying vec3 vUVW;

void main() {

// Atlas UV mapping — must match lightprobes_pars_fragment.glsl.js
float nz = probesResolution.z;
float paddedSlices = nz + 2.0;
float atlasDepth = 7.0 * paddedSlices;
float uvZBase = vUVW.z * nz + 1.0;

vec4 s0 = texture( probesSH, vec3( vUVW.xy, ( uvZBase ) / atlasDepth ) );
vec4 s1 = texture( probesSH, vec3( vUVW.xy, ( uvZBase + paddedSlices ) / atlasDepth ) );
vec4 s2 = texture( probesSH, vec3( vUVW.xy, ( uvZBase + 2.0 * paddedSlices ) / atlasDepth ) );
vec4 s3 = texture( probesSH, vec3( vUVW.xy, ( uvZBase + 3.0 * paddedSlices ) / atlasDepth ) );
vec4 s4 = texture( probesSH, vec3( vUVW.xy, ( uvZBase + 4.0 * paddedSlices ) / atlasDepth ) );
vec4 s5 = texture( probesSH, vec3( vUVW.xy, ( uvZBase + 5.0 * paddedSlices ) / atlasDepth ) );
vec4 s6 = texture( probesSH, 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 );
Comment thread
Mugen87 marked this conversation as resolved.

#include <tonemapping_fragment>
#include <colorspace_fragment>

}

`

} );

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

super( geometry, material, count );

/**
* The probe grid to visualize.
*
* @type {LightProbeGrid}
*/
this.probes = probes;

this.type = 'LightProbeGridHelper';

this.update();

}

/**
* Rebuilds instance matrices and UVW attributes from the current probe grid.
* Call this after changing `probes` or after re-baking.
*/
update() {

const probes = this.probes;
const res = probes.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 lightprobes_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;

probes.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.probesSH.value = probes.texture;
this.material.uniforms.probesResolution.value.copy( probes.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 { LightProbeGridHelper };
Loading
Loading