222 lines
5.4 KiB
JavaScript
222 lines
5.4 KiB
JavaScript
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 L2 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 );
|
|
|
|
#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 };
|