Przeglądaj źródła

LightProbeGrid: Add indirect bounces option to bake() (#33657)

mrdoob 1 miesiąc temu
rodzic
commit
c1fccc30d4

+ 83 - 67
examples/jsm/lighting/LightProbeGrid.js

@@ -205,7 +205,10 @@ 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.
@@ -213,17 +216,16 @@ class LightProbeGrid extends Object3D {
 	 * @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;
 
@@ -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 );

BIN
examples/screenshots/webgl_lightprobes_sponza.jpg


+ 24 - 11
examples/webgl_lightprobes_sponza.html

@@ -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();
@@ -94,6 +96,7 @@
 					progressBar.value = loaded / total * 100;
 
 				};
+
 				manager.onLoad = function () {
 
 					progressBar.remove();
@@ -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;
@@ -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
 				};
 
@@ -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 ) {
 
@@ -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();
@@ -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;

粤ICP备19079148号