three
Version:
JavaScript 3D library
167 lines (120 loc) • 4.15 kB
JavaScript
import {
InstancedBufferAttribute,
InstancedMesh,
Matrix4,
NodeMaterial,
SphereGeometry,
Vector3
} from 'three/webgpu';
import { array, attribute, Fn, getShIrradianceAt, normalWorld, texture3D, uniform, vec3, vec4 } from 'three/tsl';
/**
* Visualizes a {@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.
*
* This helper can only be used with {@link WebGPURenderer}.
* When using {@link WebGLRenderer}, import from `LightProbeGridHelperWebGL.js`.
*
* ```js
* const helper = new LightProbeGridHelper( probes );
* scene.add( helper );
* ```
*
* @private
* @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 NodeMaterial();
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';
// Atlas and resolution are swappable uniforms, so the shading node builds once.
this._atlas = texture3D( probes.texture );
this._resolution = uniform( new Vector3() );
const nz = this._resolution.z;
const paddedSlices = nz.add( 2.0 );
const atlasDepth = paddedSlices.mul( 7.0 );
material.fragmentNode = Fn( () => {
const uvw = attribute( 'instanceUVW', 'vec3' );
const uvZBase = uvw.z.mul( nz ).add( 1.0 );
const slice = ( t ) => this._atlas.sample( vec3( uvw.xy, uvZBase.add( paddedSlices.mul( t ) ).div( atlasDepth ) ) );
const s0 = slice( 0 ), s1 = slice( 1 ), s2 = slice( 2 ), s3 = slice( 3 );
const s4 = slice( 4 ), s5 = slice( 5 ), s6 = slice( 6 );
const sh = array( [
s0.xyz,
vec3( s0.w, s1.xy ),
vec3( s1.zw, s2.x ),
s2.yzw,
s3.xyz,
vec3( s3.w, s4.xy ),
vec3( s4.zw, s5.x ),
s5.yzw,
s6.xyz
] );
return vec4( getShIrradianceAt( normalWorld, sh ).max( vec3( 0.0 ) ), 1.0 );
} )();
this.update();
}
/**
* Rebuilds instance matrices and UVW attributes from the current probe grid,
* and rebinds the shading node to its atlas. 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 LightProbeGridNode).
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 ) );
this._atlas.value = probes.texture;
this._resolution.value.copy( res );
}
/**
* 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 };