Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions build/three.webgpu.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions build/three.webgpu.nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -45972,7 +45972,7 @@ const mx_select = /*@__PURE__*/ Fn( ( [ b_immutable, t_immutable, f_immutable ]
const t = float( t_immutable ).toVar();
const b = bool( b_immutable ).toVar();

return select( b, t, f );
return select( b, t, f ).uniformFlow();

} ).setLayout( {
name: 'mx_select',
Expand All @@ -45989,7 +45989,7 @@ const mx_negate_if = /*@__PURE__*/ Fn( ( [ val_immutable, b_immutable ] ) => {
const b = bool( b_immutable ).toVar();
const val = float( val_immutable ).toVar();

return select( b, val.negate(), val );
return select( b, val.negate(), val ).uniformFlow();

} ).setLayout( {
name: 'mx_negate_if',
Expand Down Expand Up @@ -51870,6 +51870,8 @@ class NodeBuilder {

let data = nodeData[ shaderStage ];

if ( this.subBuildLayers.length === 0 ) return data;

const subBuilds = nodeData.any ? nodeData.any.subBuilds : null;
const subBuild = this.getClosestSubBuild( subBuilds );

Expand Down Expand Up @@ -65465,7 +65467,6 @@ let _color4 = null;
* implement the interface.
*
* @abstract
* @private
*/
class Backend {

Expand Down
2 changes: 1 addition & 1 deletion build/three.webgpu.nodes.min.js

Large diffs are not rendered by default.

150 changes: 83 additions & 67 deletions examples/jsm/lighting/LightProbeGrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,25 +205,27 @@ class LightProbeGrid extends Object3D {

/**
* Bakes all probes by rendering cubemaps at each probe position
* and projecting to L2 SH. Fully GPU-resident with zero CPU readback.
* and projecting to L2 SH. Optionally iterates additional passes to
* capture indirect bounces — each extra pass samples the previous pass's
* atlas as indirect light, so a grid added to the scene before baking
* accumulates one bounce per extra pass.
*
* @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.
* @param {number} [options.bounces=0] - Additional bounce passes after the initial direct pass.
*/
bake( renderer, scene, options = {} ) {

const { bounces = 0 } = options;
const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( options );

this._ensureTextures();
this.updateBoundingBox();

// Prevent feedback: temporarily hide the volume during baking
this.visible = false;

const res = this.resolution;
const totalProbes = res.x * res.y * res.z;

Expand All @@ -245,99 +247,113 @@ class LightProbeGrid extends Object3D {

}

// Clear pooled batch target so skipped probes read as zero
batchTarget.scissorTest = false;
batchTarget.viewport.set( 0, 0, 9, totalProbes );
renderer.setRenderTarget( batchTarget );
renderer.clear();
// Disable shadow map auto-update across all passes — lights don't move.
// Force a single shadow update on the first render so maps are initialized.
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true;

_ensureRepackResources();

const paddedSlices = res.z + 2 * ATLAS_PADDING;
const rt = this._renderTarget;

// 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;
// Pass 0 captures direct light only (grid hidden, so probesSH is not sampled
// — the atlas at this point may be uninitialized or hold a prior bake).
// Each subsequent pass keeps the grid visible so the cube cameras read the
// previous pass's atlas as indirect light, accumulating one bounce per pass.
// Phase 1 writes to the batch target and Phase 2 only swaps it into the atlas
// at the very end of each pass, which gives an implicit ping-pong for free.

// 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 currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true;
for ( let pass = 0; pass <= bounces; pass ++ ) {

this.visible = pass > 0;

for ( let iz = 0; iz < res.z; iz ++ ) {
// Clear pooled batch target so skipped probes read as zero
batchTarget.scissorTest = false;
batchTarget.viewport.set( 0, 0, 9, totalProbes );
renderer.setRenderTarget( batchTarget );
renderer.clear();

for ( let iy = 0; iy < res.y; iy ++ ) {
// 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;

for ( let ix = 0; ix < res.x; ix ++ ) {
for ( let iz = 0; iz < res.z; iz ++ ) {

const probeIndex = ix + iy * res.x + iz * res.x * res.y;
for ( let iy = 0; iy < res.y; iy ++ ) {

this.getProbePosition( ix, iy, iz, _position );
cubeCamera.position.copy( _position );
cubeCamera.update( renderer, scene );
for ( let ix = 0; ix < res.x; ix ++ ) {

// SH projection
_shMaterial.uniforms.envMap.value = cubeRenderTarget.texture;
_mesh.material = _shMaterial;
batchTarget.viewport.set( 0, probeIndex, 9, 1 );
batchTarget.scissor.set( 0, probeIndex, 9, 1 );
renderer.setRenderTarget( batchTarget );
renderer.render( _scene, _camera );
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, 9, 1 );
batchTarget.scissor.set( 0, probeIndex, 9, 1 );
renderer.setRenderTarget( batchTarget );
renderer.render( _scene, _camera );

}

}

}

}
// 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.

renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
rt.scissorTest = false;
rt.viewport.set( 0, 0, res.x, res.y );

// 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.
for ( let t = 0; t < 7; t ++ ) {

_ensureRepackResources();
_repackMaterials[ t ].uniforms.batchTexture.value = batchTarget.texture;
_repackMaterials[ t ].uniforms.resolution.value.copy( res );

const paddedSlices = res.z + 2 * ATLAS_PADDING;
const rt = this._renderTarget;
rt.scissorTest = false;
rt.viewport.set( 0, 0, res.x, res.y );
// Write data slices
for ( let iz = 0; iz < res.z; iz ++ ) {

for ( let t = 0; t < 7; t ++ ) {
_repackMaterials[ t ].uniforms.sliceZ.value = iz;
_mesh.material = _repackMaterials[ t ];
renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + iz );
renderer.render( _scene, _camera );

_repackMaterials[ t ].uniforms.batchTexture.value = batchTarget.texture;
_repackMaterials[ t ].uniforms.resolution.value.copy( res );
}

// Write data slices
for ( let iz = 0; iz < res.z; iz ++ ) {
// 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 );

_repackMaterials[ t ].uniforms.sliceZ.value = iz;
// 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 + iz );
renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + res.z );
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 );

}

renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;

// Restore renderer state
renderer.setRenderTarget( currentRenderTarget );
renderer.setViewport( _currentViewport );
Expand Down
Binary file modified examples/screenshots/webgl_lightprobes_sponza.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/screenshots/webgpu_materialx_noise.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/screenshots/webgpu_portal.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/screenshots/webgpu_shadowmap.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/screenshots/webgpu_tsl_wood.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 24 additions & 11 deletions examples/webgl_lightprobes_sponza.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
let camera, scene, renderer, controls, timer;
let probes = null, probesHelper = null;
let modelSize = null;
let dirLight = null, sky = null, sun = new THREE.Vector3();
let dirLight = null, sky = null;

const sun = new THREE.Vector3();

const _box = new THREE.Box3();
const _size = new THREE.Vector3();
Expand Down Expand Up @@ -94,6 +96,7 @@
progressBar.value = loaded / total * 100;

};

manager.onLoad = function () {

progressBar.remove();
Expand Down Expand Up @@ -139,7 +142,7 @@
let isBaking = false;
let bakeQueued = false;

dirLight = new THREE.DirectionalLight( 0xfff2dc, 50.0 );
dirLight = new THREE.DirectionalLight( 0xfff2dc, 100.0 );
dirLight.target.position.set( modelCenter.x, targetY, modelCenter.z );
scene.add( dirLight.target );
dirLight.castShadow = true;
Expand All @@ -156,19 +159,20 @@
const params = {
enabled: true,
showProbes: false,
probeSize: 0.25,
probeSize: 0.2,
boundsX: - 0.5,
boundsY: 6,
boundsZ: - 0.3,
sizeX: 19,
sizeX: 21,
sizeY: 11,
sizeZ: 7,
countX: 7,
sizeZ: 9,
countX: 10,
countY: 7,
countZ: 3,
countZ: 7,
bounces: 1,
lightAzimuth: - 45,
lightElevation: 55,
lightIntensity: 50.0,
lightIntensity: 100.0,
shadows: true
};

Expand Down Expand Up @@ -234,13 +238,17 @@
params.countX, params.countY, params.countZ
);
probes.position.set( params.boundsX, params.boundsY, params.boundsZ );
// Add to the scene before baking so bounce passes can sample the prior pass's atlas.
scene.add( probes );
// Hide the helper spheres so they don't appear in the cubemap captures.
if ( probesHelper ) probesHelper.visible = false;
probes.bake( renderer, scene, {
cubemapSize: 32,
near: 0.05,
far: probeFar
far: probeFar,
bounces: params.bounces
} );
probes.visible = params.enabled;
scene.add( probes );

if ( ! probesHelper ) {

Expand Down Expand Up @@ -283,7 +291,7 @@
scheduleRebake();

} );
gui.add( params, 'lightIntensity', 0, 50, 0.1 ).name( 'Light Intensity' ).onChange( ( value ) => {
gui.add( params, 'lightIntensity', 0, 100, 0.1 ).name( 'Light Intensity' ).onChange( ( value ) => {

dirLight.intensity = value;
scheduleRebake();
Expand All @@ -296,6 +304,11 @@

} );

gui.add( params, 'countX', 2, 32, 1 ).name( 'Probes X' ).onChange( scheduleRebake );
gui.add( params, 'countY', 2, 16, 1 ).name( 'Probes Y' ).onChange( scheduleRebake );
gui.add( params, 'countZ', 2, 16, 1 ).name( 'Probes Z' ).onChange( scheduleRebake );
gui.add( params, 'bounces', 0, 2, 1 ).name( 'Bounces' ).onChange( scheduleRebake );

gui.add( params, 'showProbes' ).name( 'Show Probes' ).onChange( ( value ) => {

if ( probesHelper ) probesHelper.visible = value;
Expand Down
2 changes: 2 additions & 0 deletions src/nodes/core/NodeBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -1852,6 +1852,8 @@ class NodeBuilder {

let data = nodeData[ shaderStage ];

if ( this.subBuildLayers.length === 0 ) return data;

const subBuilds = nodeData.any ? nodeData.any.subBuilds : null;
const subBuild = this.getClosestSubBuild( subBuilds );

Expand Down
4 changes: 2 additions & 2 deletions src/nodes/materialx/lib/mx_noise.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const mx_select = /*@__PURE__*/ Fn( ( [ b_immutable, t_immutable, f_immut
const t = float( t_immutable ).toVar();
const b = bool( b_immutable ).toVar();

return select( b, t, f );
return select( b, t, f ).uniformFlow();

} ).setLayout( {
name: 'mx_select',
Expand All @@ -32,7 +32,7 @@ export const mx_negate_if = /*@__PURE__*/ Fn( ( [ val_immutable, b_immutable ] )
const b = bool( b_immutable ).toVar();
const val = float( val_immutable ).toVar();

return select( b, val.negate(), val );
return select( b, val.negate(), val ).uniformFlow();

} ).setLayout( {
name: 'mx_negate_if',
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const exceptionList = [
'webgpu_postprocessing_ssgi_ballpool',
'webgpu_postprocessing_sss',
'webgpu_postprocessing_traa',
'webgpu_tsl_vfx_linkedparticles',
'webgpu_volume_lighting_traa',

// Timming issues?
Expand Down Expand Up @@ -202,6 +203,7 @@ async function main() {
'--disable-vulkan-surface',
'--ignore-gpu-blocklist',
'--disable-gpu-driver-bug-workarounds',
'--disable-gpu-watchdog',
'--no-sandbox'
];

Expand Down
Loading