Просмотр исходного кода

ProgressiveLightMap: Add WebGPU version. (#29749)

* ProgressiveLightMapGPU: Initial commit.

* ProgressiveLightMap: Apply some fixes.

* ProgressiveLightMapGPU: More fixes.

* ProgressiveLightMap: Finalize implementation.

* E2E: Update screenshot.
Michael Herzog 1 год назад
Родитель
Сommit
f827e4585b

+ 1 - 0
examples/files.json

@@ -412,6 +412,7 @@
 		"webgpu_shadertoy",
 		"webgpu_shadowmap",
 		"webgpu_shadowmap_opacity",
+		"webgpu_shadowmap_progressive",
 		"webgpu_shadowmap_vsm",
 		"webgpu_skinning",
 		"webgpu_skinning_instancing",

+ 293 - 0
examples/jsm/misc/ProgressiveLightMapGPU.js

@@ -0,0 +1,293 @@
+import { DoubleSide, FloatType, HalfFloatType, PlaneGeometry, Mesh, RenderTarget, Scene } from 'three';
+import { add, float, mix, MeshPhongNodeMaterial, NodeMaterial, output, sub, texture, uniform, uv, vec2, vec4 } from 'three/tsl';
+
+import { potpack } from '../libs/potpack.module.js';
+
+/**
+ * Progressive Light Map Accumulator, by [zalo](https://github.com/zalo/)
+ *
+ * To use, simply construct a `ProgressiveLightMap` object,
+ * `plmap.addObjectsToLightMap(object)` an array of semi-static
+ * objects and lights to the class once, and then call
+ * `plmap.update(camera)` every frame to begin accumulating
+ * lighting samples.
+ *
+ * This should begin accumulating lightmaps which apply to
+ * your objects, so you can start jittering lighting to achieve
+ * the texture-space effect you're looking for.
+ *
+ * @param {WebGPURenderer} renderer An instance of WebGPURenderer.
+ * @param {number} resolution The side-long dimension of you total lightmap.
+ */
+class ProgressiveLightMap {
+
+	constructor( renderer, resolution = 1024 ) {
+
+		this.renderer = renderer;
+		this.resolution = resolution;
+
+		this._lightMapContainers = [];
+		this._scene = new Scene();
+		this._buffer1Active = false;
+		this._labelMesh = null;
+		this._blurringPlane = null;
+
+		// Create the Progressive LightMap Texture
+
+		const type = /(Android|iPad|iPhone|iPod)/g.test( navigator.userAgent ) ? HalfFloatType : FloatType;
+		this._progressiveLightMap1 = new RenderTarget( this.resolution, this.resolution, { type: type } );
+		this._progressiveLightMap2 = new RenderTarget( this.resolution, this.resolution, { type: type } );
+		this._progressiveLightMap2.texture.channel = 1;
+
+		// uniforms
+
+		this._averagingWindow = uniform( 100 );
+		this._previousShadowMap = texture( this._progressiveLightMap1.texture );
+
+		// materials
+
+		const uvNode = uv( 1 ).flipY();
+
+		this._uvMat = new MeshPhongNodeMaterial();
+		this._uvMat.vertexNode = vec4( sub( uvNode, vec2( 0.5 ) ).mul( 2 ), 1, 1 );
+		this._uvMat.outputNode = vec4( mix( this._previousShadowMap.uv( uv( 1 ) ), output, float( 1 ).div( this._averagingWindow ) ) );
+
+	}
+
+	/**
+	 * Sets these objects' materials' lightmaps and modifies their uv1's.
+	 * @param {Object3D} objects An array of objects and lights to set up your lightmap.
+	 */
+	addObjectsToLightMap( objects ) {
+
+		// Prepare list of UV bounding boxes for packing later...
+		const uv_boxes = [];
+
+		const padding = 3 / this.resolution;
+
+		for ( let ob = 0; ob < objects.length; ob ++ ) {
+
+			const object = objects[ ob ];
+
+			// If this object is a light, simply add it to the internal scene
+			if ( object.isLight ) {
+
+				this._scene.attach( object ); continue;
+
+			}
+
+			if ( object.geometry.hasAttribute( 'uv' ) === false ) {
+
+				console.warn( 'THREE.ProgressiveLightMap: All lightmap objects need uvs.' ); continue;
+
+			}
+
+			if ( this._blurringPlane === null ) {
+
+				this._initializeBlurPlane();
+
+			}
+
+			// Apply the lightmap to the object
+			object.material.lightMap = this._progressiveLightMap2.texture;
+			object.material.dithering = true;
+			object.castShadow = true;
+			object.receiveShadow = true;
+			object.renderOrder = 1000 + ob;
+
+			// Prepare UV boxes for potpack
+			// TODO: Size these by object surface area
+			uv_boxes.push( { w: 1 + ( padding * 2 ), h: 1 + ( padding * 2 ), index: ob } );
+
+			this._lightMapContainers.push( { basicMat: object.material, object: object } );
+
+		}
+
+		// Pack the objects' lightmap UVs into the same global space
+		const dimensions = potpack( uv_boxes );
+		uv_boxes.forEach( ( box ) => {
+
+			const uv1 = objects[ box.index ].geometry.getAttribute( 'uv' ).clone();
+			for ( let i = 0; i < uv1.array.length; i += uv1.itemSize ) {
+
+				uv1.array[ i ] = ( uv1.array[ i ] + box.x + padding ) / dimensions.w;
+				uv1.array[ i + 1 ] = 1 - ( ( uv1.array[ i + 1 ] + box.y + padding ) / dimensions.h );
+
+			}
+
+			objects[ box.index ].geometry.setAttribute( 'uv1', uv1 );
+			objects[ box.index ].geometry.getAttribute( 'uv1' ).needsUpdate = true;
+
+		} );
+
+	}
+
+	/**
+	 * Frees all internal resources.
+	 */
+	dispose() {
+
+		this._progressiveLightMap1.dispose();
+		this._progressiveLightMap2.dispose();
+
+		this._uvMat.dispose();
+
+		if ( this._blurringPlane !== null ) {
+
+			this._blurringPlane.geometry.dispose();
+			this._blurringPlane.material.dispose();
+
+		}
+
+		if ( this._labelMesh !== null ) {
+
+			this._labelMesh.geometry.dispose();
+			this._labelMesh.material.dispose();
+
+		}
+
+	}
+
+	/**
+	 * This function renders each mesh one at a time into their respective surface maps
+	 * @param {Camera} camera Standard Rendering Camera
+	 * @param {number} blendWindow When >1, samples will accumulate over time.
+	 * @param {boolean} blurEdges  Whether to fix UV Edges via blurring
+	 */
+	update( camera, blendWindow = 100, blurEdges = true ) {
+
+		if ( this._blurringPlane === null ) {
+
+			return;
+
+		}
+
+		// Store the original Render Target
+		const currentRenderTarget = this.renderer.getRenderTarget();
+
+		// The blurring plane applies blur to the seams of the lightmap
+		this._blurringPlane.visible = blurEdges;
+
+		// Steal the Object3D from the real world to our special dimension
+		for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
+
+			this._lightMapContainers[ l ].object.oldScene = this._lightMapContainers[ l ].object.parent;
+			this._scene.attach( this._lightMapContainers[ l ].object );
+
+		}
+
+		// Set each object's material to the UV Unwrapped Surface Mapping Version
+		for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
+
+			this._averagingWindow.value = blendWindow;
+			this._lightMapContainers[ l ].object.material = this._uvMat;
+			this._lightMapContainers[ l ].object.oldFrustumCulled = this._lightMapContainers[ l ].object.frustumCulled;
+			this._lightMapContainers[ l ].object.frustumCulled = false;
+
+		}
+
+		// Ping-pong two surface buffers for reading/writing
+		const activeMap = this._buffer1Active ? this._progressiveLightMap1 : this._progressiveLightMap2;
+		const inactiveMap = this._buffer1Active ? this._progressiveLightMap2 : this._progressiveLightMap1;
+
+		// Render the object's surface maps
+		this.renderer.setRenderTarget( activeMap );
+		this._previousShadowMap.value = inactiveMap.texture;
+
+		this._buffer1Active = ! this._buffer1Active;
+		this.renderer.render( this._scene, camera );
+
+		// Restore the object's Real-time Material and add it back to the original world
+		for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
+
+			this._lightMapContainers[ l ].object.frustumCulled = this._lightMapContainers[ l ].object.oldFrustumCulled;
+			this._lightMapContainers[ l ].object.material = this._lightMapContainers[ l ].basicMat;
+			this._lightMapContainers[ l ].object.oldScene.attach( this._lightMapContainers[ l ].object );
+
+		}
+
+		// Restore the original Render Target
+		this.renderer.setRenderTarget( currentRenderTarget );
+
+	}
+
+	/**
+	 * Draw the lightmap in the main scene.  Call this after adding the objects to it.
+	 * @param {boolean} visible Whether the debug plane should be visible.
+	 * @param {Vector3} position Where the debug plane should be drawn.
+	*/
+	showDebugLightmap( visible, position = null ) {
+
+		if ( this._lightMapContainers.length === 0 ) {
+
+			console.warn( 'THREE.ProgressiveLightMap: Call .showDebugLightmap() after adding the objects.' );
+
+			return;
+
+		}
+
+		if ( this._labelMesh === null ) {
+
+			const labelMaterial = new NodeMaterial();
+			labelMaterial.colorNode = texture( this._progressiveLightMap1.texture ).uv( uv().flipY() );
+			labelMaterial.side = DoubleSide;
+
+			const labelGeometry = new PlaneGeometry( 100, 100 );
+
+			this._labelMesh = new Mesh( labelGeometry, labelMaterial );
+			this._labelMesh.position.y = 250;
+
+			this._lightMapContainers[ 0 ].object.parent.add( this._labelMesh );
+
+		}
+
+		if ( position !== null ) {
+
+			this._labelMesh.position.copy( position );
+
+		}
+
+		this._labelMesh.visible = visible;
+
+	}
+
+	/**
+	 * Creates the Blurring Plane.
+	 */
+	_initializeBlurPlane() {
+
+		const blurMaterial = new NodeMaterial();
+		blurMaterial.polygonOffset = true;
+		blurMaterial.polygonOffsetFactor = - 1;
+		blurMaterial.polygonOffsetUnits = 3;
+
+		blurMaterial.vertexNode = vec4( sub( uv(), vec2( 0.5 ) ).mul( 2 ), 1, 1 );
+
+		const uvNode = uv().flipY().toVar();
+		const pixelOffset = float( 0.5 ).div( float( this.resolution ) ).toVar();
+
+		const color = add(
+			this._previousShadowMap.uv( uvNode.add( vec2( pixelOffset, 0 ) ) ),
+			this._previousShadowMap.uv( uvNode.add( vec2( 0, pixelOffset ) ) ),
+			this._previousShadowMap.uv( uvNode.add( vec2( 0, pixelOffset.negate() ) ) ),
+			this._previousShadowMap.uv( uvNode.add( vec2( pixelOffset.negate(), 0 ) ) ),
+			this._previousShadowMap.uv( uvNode.add( vec2( pixelOffset, pixelOffset ) ) ),
+			this._previousShadowMap.uv( uvNode.add( vec2( pixelOffset.negate(), pixelOffset ) ) ),
+			this._previousShadowMap.uv( uvNode.add( vec2( pixelOffset, pixelOffset.negate() ) ) ),
+			this._previousShadowMap.uv( uvNode.add( vec2( pixelOffset.negate(), pixelOffset.negate() ) ) ),
+		).div( 8 );
+
+		blurMaterial.fragmentNode = color;
+
+		this._blurringPlane = new Mesh( new PlaneGeometry( 1, 1 ), blurMaterial );
+		this._blurringPlane.name = 'Blurring Plane';
+		this._blurringPlane.frustumCulled = false;
+		this._blurringPlane.renderOrder = 0;
+		this._blurringPlane.material.depthWrite = false;
+		this._scene.add( this._blurringPlane );
+
+	}
+
+}
+
+export { ProgressiveLightMap };

BIN
examples/screenshots/webgpu_shadowmap_progressive.jpg


+ 248 - 0
examples/webgpu_shadowmap_progressive.html

@@ -0,0 +1,248 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - progressive lightmap accumulation</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+		<div id="container"></div>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Progressive Lightmaps by <a href="https://github.com/zalo" target="_blank" rel="noopener">zalo</a><br/>
+			[Inspired by <a href="http://madebyevan.com/shaders/lightmap/" target="_blank" rel="noopener">evanw's Lightmap Generation</a>]
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+			import * as THREE from 'three';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { TransformControls } from 'three/addons/controls/TransformControls.js';
+			import { ProgressiveLightMap } from 'three/addons/misc/ProgressiveLightMapGPU.js';
+
+			// ShadowMap + LightMap Res and Number of Directional Lights
+			const shadowMapRes = 1024, lightMapRes = 1024, lightCount = 4;
+			let camera, scene, renderer, controls, control, control2,
+				object = new THREE.Mesh(), lightOrigin = null, progressiveSurfacemap;
+			const dirLights = [], lightmapObjects = [];
+			const params = { 'Enable': true, 'Blur Edges': true, 'Blend Window': 200,
+							 'Light Radius': 50, 'Ambient Weight': 0.5, 'Debug Lightmap': false };
+			init();
+			createGUI();
+
+			function init() {
+
+				// renderer
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.shadowMap.enabled = true;
+				document.body.appendChild( renderer.domElement );
+
+				// camera
+				camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.set( 0, 100, 200 );
+				camera.name = 'Camera';
+
+				// scene
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x949494 );
+				scene.fog = new THREE.Fog( 0x949494, 1000, 3000 );
+
+				// progressive lightmap
+				progressiveSurfacemap = new ProgressiveLightMap( renderer, lightMapRes );
+
+				// directional lighting "origin"
+				lightOrigin = new THREE.Group();
+				lightOrigin.position.set( 60, 150, 100 );
+				scene.add( lightOrigin );
+
+				// transform gizmo
+				control = new TransformControls( camera, renderer.domElement );
+				control.addEventListener( 'dragging-changed', ( event ) => {
+
+					controls.enabled = ! event.value;
+
+				} );
+				control.attach( lightOrigin );
+				scene.add( control.getHelper() );
+
+				// create 8 directional lights to speed up the convergence
+				for ( let l = 0; l < lightCount; l ++ ) {
+
+					const dirLight = new THREE.DirectionalLight( 0xffffff, Math.PI / lightCount );
+					dirLight.name = 'Dir. Light ' + l;
+					dirLight.position.set( 200, 200, 200 );
+					dirLight.castShadow = true;
+					dirLight.shadow.camera.near = 100;
+					dirLight.shadow.camera.far = 5000;
+					dirLight.shadow.camera.right = 150;
+					dirLight.shadow.camera.left = - 150;
+					dirLight.shadow.camera.top = 150;
+					dirLight.shadow.camera.bottom = - 150;
+					dirLight.shadow.mapSize.width = shadowMapRes;
+					dirLight.shadow.mapSize.height = shadowMapRes;
+					dirLight.shadow.bias = - 0.001;
+					lightmapObjects.push( dirLight );
+					dirLights.push( dirLight );
+
+				}
+
+				// ground
+				const groundMesh = new THREE.Mesh(
+					new THREE.PlaneGeometry( 600, 600 ),
+					new THREE.MeshPhongMaterial( { color: 0xffffff, depthWrite: true } )
+				);
+				groundMesh.position.y = - 0.1;
+				groundMesh.rotation.x = - Math.PI / 2;
+				groundMesh.name = 'Ground Mesh';
+				lightmapObjects.push( groundMesh );
+				scene.add( groundMesh );
+
+				// model
+				function loadModel() {
+
+					object.traverse( function ( child ) {
+
+						if ( child.isMesh ) {
+
+							child.name = 'Loaded Mesh';
+							child.castShadow = true;
+							child.receiveShadow = true;
+							child.material = new THREE.MeshPhongMaterial();
+
+							// This adds the model to the lightmap
+							lightmapObjects.push( child );
+							progressiveSurfacemap.addObjectsToLightMap( lightmapObjects );
+
+						} else {
+
+							child.layers.disableAll(); // Disable Rendering for this
+
+						}
+
+					} );
+					scene.add( object );
+					object.scale.set( 2, 2, 2 );
+					object.position.set( 0, - 16, 0 );
+					control2 = new TransformControls( camera, renderer.domElement );
+					control2.addEventListener( 'dragging-changed', ( event ) => {
+
+						controls.enabled = ! event.value;
+
+					} );
+					control2.attach( object );
+					scene.add( control2.getHelper() );
+					const lightTarget = new THREE.Group();
+					lightTarget.position.set( 0, 20, 0 );
+					for ( let l = 0; l < dirLights.length; l ++ ) {
+
+						dirLights[ l ].target = lightTarget;
+
+					}
+
+					object.add( lightTarget );
+
+				}
+
+				const manager = new THREE.LoadingManager( loadModel );
+				const loader = new GLTFLoader( manager );
+				loader.load( 'models/gltf/ShadowmappableMesh.glb', function ( obj ) {
+
+					object = obj.scene.children[ 0 ];
+
+				} );
+
+				// controls
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
+				controls.dampingFactor = 0.05;
+				controls.screenSpacePanning = true;
+				controls.minDistance = 100;
+				controls.maxDistance = 500;
+				controls.maxPolarAngle = Math.PI / 1.5;
+				controls.target.set( 0, 100, 0 );
+			
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function createGUI() {
+
+				const gui = new GUI( { title: 'Accumulation Settings' } );
+				gui.add( params, 'Enable' );
+				gui.add( params, 'Blur Edges' );
+				gui.add( params, 'Blend Window', 1, 500 ).step( 1 );
+				gui.add( params, 'Light Radius', 0, 200 ).step( 10 );
+				gui.add( params, 'Ambient Weight', 0, 1 ).step( 0.1 );
+				gui.add( params, 'Debug Lightmap' ).onChange( ( value ) => progressiveSurfacemap.showDebugLightmap( value ) );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				// Update the inertia on the orbit controls
+				controls.update();
+
+				// Accumulate Surface Maps
+				if ( params[ 'Enable' ] ) {
+
+					progressiveSurfacemap.update( camera, params[ 'Blend Window' ], params[ 'Blur Edges' ] );
+
+				}
+
+				// Manually Update the Directional Lights
+				for ( let l = 0; l < dirLights.length; l ++ ) {
+
+					// Sometimes they will be sampled from the target direction
+					// Sometimes they will be uniformly sampled from the upper hemisphere
+					if ( Math.random() > params[ 'Ambient Weight' ] ) {
+
+						dirLights[ l ].position.set(
+							lightOrigin.position.x + ( Math.random() * params[ 'Light Radius' ] ),
+							lightOrigin.position.y + ( Math.random() * params[ 'Light Radius' ] ),
+							lightOrigin.position.z + ( Math.random() * params[ 'Light Radius' ] ) );
+
+					} else {
+
+						// Uniform Hemispherical Surface Distribution for Ambient Occlusion
+						const lambda = Math.acos( 2 * Math.random() - 1 ) - ( 3.14159 / 2.0 );
+						const phi = 2 * 3.14159 * Math.random();
+						dirLights[ l ].position.set(
+							        ( ( Math.cos( lambda ) * Math.cos( phi ) ) * 300 ) + object.position.x,
+							Math.abs( ( Math.cos( lambda ) * Math.sin( phi ) ) * 300 ) + object.position.y + 20,
+							          ( Math.sin( lambda ) * 300 ) + object.position.z
+						);
+
+					}
+
+				}
+
+				// Render Scene
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -99,6 +99,7 @@ const exceptionList = [
 	'webgl_renderer_pathtracer',
 	'webgl_shadowmap',
 	'webgl_shadowmap_progressive',
+	'webgpu_shadowmap_progressive',
 	'webgl_test_memory2',
 	'webgl_tiled_forward',
 	'webgl_points_dynamic',

粤ICP备19079148号