浏览代码

WebGPURenderer: Support BatchMesh (#27937)

* init batch and fixes

* smaller example

* pup

* fix dynamic example

* fix demo
Renaud Rohlinger 1 年之前
父节点
当前提交
7059b85ca3

+ 1 - 0
examples/files.json

@@ -383,6 +383,7 @@
 		"webgpu_multisampled_renderbuffers",
 		"webgpu_materials_texture_anisotropy",
 		"webgpu_storage_buffer",
+		"webgpu_mesh_batch",
 		"webgpu_instancing_morph"
 	],
 	"webaudio": [

+ 1 - 0
examples/jsm/nodes/Nodes.js

@@ -84,6 +84,7 @@ export { default as CameraNode, cameraProjectionMatrix, cameraProjectionMatrixIn
 export { default as VertexColorNode, vertexColor } from './accessors/VertexColorNode.js';
 export { default as CubeTextureNode, cubeTexture } from './accessors/CubeTextureNode.js';
 export { default as InstanceNode, instance } from './accessors/InstanceNode.js';
+export { default as BatchNode, batch } from './accessors/BatchNode.js';
 export { default as MaterialNode, materialAlphaTest, materialColor, materialShininess, materialEmissive, materialOpacity, materialSpecularColor, materialSpecularStrength, materialReflectivity, materialRoughness, materialMetalness, materialNormal, materialClearcoat, materialClearcoatRoughness, materialClearcoatNormal, materialRotation, materialSheen, materialSheenRoughness, materialIridescence, materialIridescenceIOR, materialIridescenceThickness, materialLineScale, materialLineDashSize, materialLineGapSize, materialLineWidth, materialLineDashOffset, materialPointWidth } from './accessors/MaterialNode.js';
 export { default as MaterialReferenceNode, materialReference } from './accessors/MaterialReferenceNode.js';
 export { default as RendererReferenceNode, rendererReference } from './accessors/RendererReferenceNode.js';

+ 84 - 0
examples/jsm/nodes/accessors/BatchNode.js

@@ -0,0 +1,84 @@
+import Node, { addNodeClass } from '../core/Node.js';
+import { normalLocal } from './NormalNode.js';
+import { positionLocal } from './PositionNode.js';
+import { nodeProxy, vec3, mat3, mat4, int, ivec2, float } from '../shadernode/ShaderNode.js';
+import { textureLoad } from './TextureNode.js';
+import { textureSize } from './TextureSizeNode.js';
+import { Float32BufferAttribute } from 'three';
+import { bufferAttribute } from './BufferAttributeNode.js';
+import { tangentLocal } from './TangentNode.js';
+
+class BatchNode extends Node {
+
+	constructor( batchMesh ) {
+
+		super( 'void' );
+
+		this.batchMesh = batchMesh;
+
+
+		this.instanceColorNode = null;
+
+		this.batchingIdNode = null;
+
+	}
+
+	setup( builder ) {
+
+		// POSITION
+
+		if ( this.batchingIdNode === null ) {
+
+			const batchingAttribute = this.batchMesh.geometry.getAttribute( 'batchId' );
+			const array = new Float32Array( batchingAttribute.array );
+
+			const buffer = new Float32BufferAttribute( array, 1 );
+
+			this.batchingIdNode = bufferAttribute( buffer, 'float' ).toVar();
+
+		}
+
+		const matriceTexture = this.batchMesh._matricesTexture;
+
+		const size = textureSize( textureLoad( matriceTexture ), 0 );
+		const j = float( int( this.batchingIdNode ) ).mul( 4 ).toVar();
+
+		const x = int( j.mod( size ) );
+		const y = int( j ).div( int( size ) );
+		const batchingMatrix = mat4(
+			textureLoad( matriceTexture, ivec2( x, y ) ),
+			textureLoad( matriceTexture, ivec2( x.add( 1 ), y ) ),
+			textureLoad( matriceTexture, ivec2( x.add( 2 ), y ) ),
+			textureLoad( matriceTexture, ivec2( x.add( 3 ), y ) )
+		);
+
+
+		const bm = mat3(
+			batchingMatrix[ 0 ].xyz,
+			batchingMatrix[ 1 ].xyz,
+			batchingMatrix[ 2 ].xyz
+		 );
+
+		positionLocal.assign( batchingMatrix.mul( positionLocal ) );
+
+		const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) );
+
+		const batchingNormal = bm.mul( transformedNormal ).xyz;
+
+		normalLocal.assign( batchingNormal );
+
+		if ( builder.hasGeometryAttribute( 'tangent' ) ) {
+
+			tangentLocal.mulAssign( bm );
+
+		}
+
+	}
+
+}
+
+export default BatchNode;
+
+export const batch = nodeProxy( BatchNode );
+
+addNodeClass( 'batch', BatchNode );

+ 8 - 0
examples/jsm/nodes/materials/NodeMaterial.js

@@ -6,6 +6,8 @@ import { materialAlphaTest, materialColor, materialOpacity, materialEmissive, ma
 import { modelViewProjection } from '../accessors/ModelViewProjectionNode.js';
 import { transformedNormalView } from '../accessors/NormalNode.js';
 import { instance } from '../accessors/InstanceNode.js';
+import { batch } from '../accessors/BatchNode.js';
+
 import { positionLocal, positionView } from '../accessors/PositionNode.js';
 import { skinningReference } from '../accessors/SkinningNode.js';
 import { morphReference } from '../accessors/MorphNode.js';
@@ -209,6 +211,12 @@ class NodeMaterial extends ShaderMaterial {
 
 		}
 
+		if ( object.isBatchedMesh ) {
+
+			batch( object ).append();
+
+		}
+
 		if ( this.positionNode !== null ) {
 
 			positionLocal.assign( this.positionNode );

+ 1 - 0
examples/jsm/renderers/common/Animation.js

@@ -35,6 +35,7 @@ class Animation {
 	dispose() {
 
 		self.cancelAnimationFrame( this.requestId );
+		this.requestId = null;
 
 	}
 

+ 3 - 3
examples/jsm/renderers/common/Renderer.js

@@ -312,7 +312,7 @@ class Renderer {
 
 		if ( this._initialized === false ) await this.init();
 
-		this._renderScene( scene, camera );
+		await this._renderScene( scene, camera );
 
 	}
 
@@ -330,7 +330,7 @@ class Renderer {
 
 	}
 
-	_renderScene( scene, camera ) {
+	async _renderScene( scene, camera ) {
 
 		// preserve render tree
 
@@ -506,7 +506,7 @@ class Renderer {
 		sceneRef.onAfterRender( this, scene, camera, renderTarget );
 
 		//
-		this.backend.resolveTimestampAsync( renderContext, 'render' );
+		await this.backend.resolveTimestampAsync( renderContext, 'render' );
 
 		return renderContext;
 

+ 30 - 26
examples/jsm/renderers/webgl/WebGLBackend.js

@@ -10,6 +10,7 @@ import WebGLTextureUtils from './utils/WebGLTextureUtils.js';
 import WebGLExtensions from './utils/WebGLExtensions.js';
 import WebGLCapabilities from './utils/WebGLCapabilities.js';
 import { GLFeatureName } from './utils/WebGLConstants.js';
+import { WebGLBufferRenderer } from './WebGLBufferRenderer.js';
 
 //
 
@@ -39,6 +40,8 @@ class WebGLBackend extends Backend {
 		this.capabilities = new WebGLCapabilities( this );
 		this.attributeUtils = new WebGLAttributeUtils( this );
 		this.textureUtils = new WebGLTextureUtils( this );
+		this.bufferRenderer = new WebGLBufferRenderer( this );
+
 		this.state = new WebGLState( this );
 		this.utils = new WebGLUtils( this );
 
@@ -644,21 +647,22 @@ class WebGLBackend extends Backend {
 
 		//
 
-		let mode;
-		if ( object.isPoints ) mode = gl.POINTS;
-		else if ( object.isLineSegments ) mode = gl.LINES;
-		else if ( object.isLine ) mode = gl.LINE_STRIP;
-		else if ( object.isLineLoop ) mode = gl.LINE_LOOP;
+		const renderer = this.bufferRenderer;
+
+		if ( object.isPoints ) renderer.mode = gl.POINTS;
+		else if ( object.isLineSegments ) renderer.mode = gl.LINES;
+		else if ( object.isLine ) renderer.mode = gl.LINE_STRIP;
+		else if ( object.isLineLoop ) renderer.mode = gl.LINE_LOOP;
 		else {
 
 			if ( material.wireframe === true ) {
 
 				state.setLineWidth( material.wireframeLinewidth * this.renderer.getPixelRatio() );
-				mode = gl.LINES;
+				renderer.mode = gl.LINES;
 
 			} else {
 
-				mode = gl.TRIANGLES;
+				renderer.mode = gl.TRIANGLES;
 
 			}
 
@@ -666,46 +670,46 @@ class WebGLBackend extends Backend {
 
 		//
 
-		const instanceCount = this.getInstanceCount( renderObject );
+
+		let count;
+
+		renderer.object = object;
 
 		if ( index !== null ) {
 
 			const indexData = this.get( index );
 			const indexCount = ( drawRange.count !== Infinity ) ? drawRange.count : index.count;
 
-			if ( instanceCount > 1 ) {
-
-				gl.drawElementsInstanced( mode, index.count, indexData.type, firstVertex, instanceCount );
+			renderer.index = index.count;
+			renderer.type = indexData.type;
 
-			} else {
+			count = indexCount;
 
-				gl.drawElements( mode, index.count, indexData.type, firstVertex );
+		} else {
 
-			}
+			renderer.index = 0;
 
-			info.update( object, indexCount, 1 );
+			const vertexCount = ( drawRange.count !== Infinity ) ? drawRange.count : geometry.attributes.position.count;
 
-		} else {
+			count = vertexCount;
 
-			const positionAttribute = geometry.attributes.position;
-			const vertexCount = ( drawRange.count !== Infinity ) ? drawRange.count : positionAttribute.count;
+		}
 
-			if ( instanceCount > 1 ) {
+		const instanceCount = this.getInstanceCount( renderObject );
 
-				gl.drawArraysInstanced( mode, 0, vertexCount, instanceCount );
+		if ( object.isBatchedMesh ) {
 
-			} else {
+			renderer.renderMultiDraw( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount );
 
-				gl.drawArrays( mode, 0, vertexCount );
+		} else if ( instanceCount > 1 ) {
 
-			}
+			renderer.renderInstances( firstVertex, count, instanceCount );
 
-			//gl.drawArrays( mode, vertexCount, gl.UNSIGNED_SHORT, firstVertex );
+		} else {
 
-			info.update( object, vertexCount, 1 );
+			renderer.render( firstVertex, count );
 
 		}
-
 		//
 
 		gl.bindVertexArray( null );

+ 99 - 0
examples/jsm/renderers/webgl/WebGLBufferRenderer.js

@@ -0,0 +1,99 @@
+class WebGLBufferRenderer {
+
+	constructor( backend ) {
+
+		this.gl = backend.gl;
+		this.extensions = backend.extensions;
+		this.info = backend.renderer.info;
+		this.mode = null;
+		this.index = 0;
+		this.type = null;
+		this.object = null;
+
+	}
+
+	render( start, count ) {
+
+		const { gl, mode, object, type, info, index } = this;
+
+		if ( index !== 0 ) {
+
+			gl.drawElements( mode, count, type, start );
+
+		} else {
+
+			gl.drawArrays( mode, start, count );
+
+		}
+
+		info.update( object, count, mode, 1 );
+
+	}
+
+	renderInstances( start, count, primcount ) {
+
+		const { gl, mode, type, index, object, info } = this;
+
+		if ( primcount === 0 ) return;
+
+		if ( index !== 0 ) {
+
+			gl.drawElementsInstanced( mode, count, type, start, primcount );
+
+		} else {
+
+			gl.drawArraysInstanced( mode, start, count, primcount );
+
+		}
+
+		info.update( object, count, mode, primcount );
+
+	}
+
+	renderMultiDraw( starts, counts, drawCount ) {
+
+		const { extensions, mode, object, info } = this;
+
+		if ( drawCount === 0 ) return;
+
+		const extension = extensions.get( 'WEBGL_multi_draw' );
+
+		if ( extension === null ) {
+
+			for ( let i = 0; i < drawCount; i ++ ) {
+
+				this.render( starts[ i ], counts[ i ] );
+
+			}
+
+		} else {
+
+			if ( this.index !== 0 ) {
+
+				extension.multiDrawElementsWEBGL( mode, counts, 0, this.type, starts, 0, drawCount );
+
+			} else {
+
+				extension.multiDrawArraysWEBGL( mode, starts, 0, counts, 0, drawCount );
+
+			}
+
+			let elementCount = 0;
+			for ( let i = 0; i < drawCount; i ++ ) {
+
+				elementCount += counts[ i ];
+
+			}
+
+			info.update( object, elementCount, mode, 1 );
+
+		}
+
+	}
+
+	//
+
+}
+
+
+export { WebGLBufferRenderer };

二进制
examples/screenshots/webgpu_mesh_batch.jpg


+ 385 - 0
examples/webgpu_mesh_batch.html

@@ -0,0 +1,385 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<title>three.js webgpu - mesh - batch</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">
+	<style>
+		#info {
+			background-color: rgba(0,0,0,0.75);
+		}
+	</style>
+</head>
+<body>
+
+	<div id="info">
+
+		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - mesh - batch
+
+	</div>
+
+	<div id="backend" style="position: absolute; top: 200px; left: 0; color: #fff; background-color: rgba(0,0,0,0.75); padding: 5px;">
+		Active Backend: WebGPU
+	</div>
+
+	<script type="importmap">
+		{
+			"imports": {
+				"three": "../build/three.module.js",
+				"three/addons/": "./jsm/",
+				"three/nodes": "./jsm/nodes/Nodes.js",
+				"stats-gl": "https://www.unpkg.com/[email protected]/dist/main.js"
+			}
+		}
+	</script>
+
+	<script type="module">
+		import * as THREE from 'three';
+
+		import Stats from 'stats-gl';
+		import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+		import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+		import { radixSort } from 'three/addons/utils/SortUtils.js';
+		import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+		import { MeshNormalNodeMaterial } from 'three/nodes';
+
+		let camera, scene, renderer;
+		let controls, stats;
+		let gui;
+		let geometries, mesh, material;
+		const ids = [];
+		
+		const matrix = new THREE.Matrix4();
+
+		//
+
+		const position = new THREE.Vector3();
+		const rotation = new THREE.Euler();
+		const quaternion = new THREE.Quaternion();
+		const scale = new THREE.Vector3();
+
+		//
+
+		const MAX_GEOMETRY_COUNT = 20000;
+
+		const api = {
+			webgpu: true,
+			count: 512,
+			dynamic: 16,
+
+			sortObjects: true,
+			perObjectFrustumCulled: true,
+			opacity: 1,
+			useCustomSort: true,
+		};
+
+
+		init();
+
+		//
+
+		function randomizeMatrix( matrix ) {
+
+			position.x = Math.random() * 40 - 20;
+			position.y = Math.random() * 40 - 20;
+			position.z = Math.random() * 40 - 20;
+
+			rotation.x = Math.random() * 2 * Math.PI;
+			rotation.y = Math.random() * 2 * Math.PI;
+			rotation.z = Math.random() * 2 * Math.PI;
+
+			quaternion.setFromEuler( rotation );
+
+			scale.x = scale.y = scale.z = 0.5 + ( Math.random() * 0.5 );
+
+			return matrix.compose( position, quaternion, scale );
+
+		}
+
+		function randomizeRotationSpeed( rotation ) {
+
+			rotation.x = Math.random() * 0.01;
+			rotation.y = Math.random() * 0.01;
+			rotation.z = Math.random() * 0.01;
+			return rotation;
+
+		}
+
+		function initGeometries() {
+
+			geometries = [
+				new THREE.ConeGeometry( 1.0, 2.0 ),
+				new THREE.BoxGeometry( 2.0, 2.0, 2.0 ),
+				new THREE.SphereGeometry( 1.0, 16, 8 ),
+			];
+
+		}
+
+		function createMaterial() {
+
+			if ( ! material ) {
+
+				material = new MeshNormalNodeMaterial();
+
+			}
+
+			return material;
+
+		}
+
+		function cleanup() {
+
+			if ( mesh ) {
+
+				mesh.parent.remove( mesh );
+
+				if ( mesh.dispose ) {
+
+					mesh.dispose();
+
+				}
+
+			}
+
+		}
+
+		function initMesh() {
+
+			cleanup();
+			initBatchedMesh();
+
+		}
+
+		function initBatchedMesh() {
+
+			const geometryCount = api.count;
+			const vertexCount = api.count * 512;
+			const indexCount = api.count * 1024;
+
+			const euler = new THREE.Euler();
+			const matrix = new THREE.Matrix4();
+			mesh = new THREE.BatchedMesh( geometryCount, vertexCount, indexCount, createMaterial() );
+			mesh.userData.rotationSpeeds = [];
+
+			// disable full-object frustum culling since all of the objects can be dynamic.
+			mesh.frustumCulled = false;
+
+			ids.length = 0;
+
+			for ( let i = 0; i < api.count; i ++ ) {
+
+				const id = mesh.addGeometry( geometries[ i % geometries.length ] );
+				mesh.setMatrixAt( id, randomizeMatrix( matrix ) );
+
+				const rotationMatrix = new THREE.Matrix4();
+				rotationMatrix.makeRotationFromEuler( randomizeRotationSpeed( euler ) );
+				mesh.userData.rotationSpeeds.push( rotationMatrix );
+
+				ids.push( id );
+
+			}
+
+			scene.add( mesh );
+
+		}
+
+
+
+		function init( forceWebGL = false ) {
+
+			if ( renderer ) {
+
+				renderer.dispose();
+				controls.dispose();
+				document.body.removeChild( stats.dom );
+				document.body.removeChild( renderer.domElement );
+
+			}
+
+			document.getElementById( 'backend' ).innerText = 'Active Backend: ' + ( forceWebGL ? 'WebGL' : 'WebGPU' );
+			// camera
+
+			const aspect = window.innerWidth / window.innerHeight;
+
+			camera = new THREE.PerspectiveCamera( 70, aspect, 1, 100 );
+			camera.position.z = 50;
+
+			// renderer
+
+			renderer = new WebGPURenderer( { antialias: true, forceWebGL } );
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+
+			renderer.setAnimationLoop( animate );
+
+			// scene
+
+			scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0xffffff );
+
+
+			if ( forceWebGL ) {
+
+				scene.background = new THREE.Color( 0xf10000 );
+
+			} else {
+
+				scene.background = new THREE.Color( 0x0000f1 );
+
+			}
+
+			document.body.appendChild( renderer.domElement );
+
+
+
+			initGeometries();
+			initMesh();
+
+			// controls
+
+			controls = new OrbitControls( camera, renderer.domElement );
+			controls.autoRotate = true;
+			controls.autoRotateSpeed = 1.0;
+
+			// stats
+
+			stats = new Stats( {
+				precision: 3,
+				horizontal: false
+			} );
+			stats.init( renderer );
+			document.body.appendChild( stats.dom );
+			stats.dom.style.position = 'absolute';
+
+			// gui
+
+			gui = new GUI();
+			gui.add( api, 'webgpu', true ).onChange( () => {
+
+				init( ! api.webgpu );
+
+			} );
+			gui.add( api, 'count', 1, MAX_GEOMETRY_COUNT ).step( 1 ).onChange( initMesh );
+			gui.add( api, 'dynamic', 0, MAX_GEOMETRY_COUNT ).step( 1 );
+
+			gui.add( api, 'opacity', 0, 1 ).onChange( v => {
+
+				if ( v < 1 ) {
+
+					material.transparent = true;
+					material.depthWrite = false;
+
+				} else {
+
+					material.transparent = false;
+					material.depthWrite = true;
+
+				}
+
+				material.opacity = v;
+				material.needsUpdate = true;
+
+			} );
+			gui.add( api, 'sortObjects' );
+			gui.add( api, 'perObjectFrustumCulled' );
+			gui.add( api, 'useCustomSort' );
+
+
+			// listeners
+
+			window.addEventListener( 'resize', onWindowResize );
+
+
+
+
+			function onWindowResize() {
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				const aspect = window.innerWidth / window.innerHeight;
+
+				camera.aspect = aspect;
+
+				const frustumHeight = camera.top - camera.bottom;
+
+				camera.left = - frustumHeight * aspect / 2;
+				camera.right = frustumHeight * aspect / 2;
+
+				camera.updateProjectionMatrix();
+
+			}
+
+
+			async function animate() {
+
+				animateMeshes();
+
+				controls.update();
+
+		
+				if ( mesh.isBatchedMesh ) {
+
+					mesh.sortObjects = api.sortObjects;
+					mesh.perObjectFrustumCulled = api.perObjectFrustumCulled;
+					mesh.setCustomSort( api.useCustomSort ? sortFunction : null );
+
+				}
+
+				await renderer.renderAsync( scene, camera );
+
+				stats.update();
+
+			}
+
+			function animateMeshes() {
+
+				const loopNum = Math.min( api.count, api.dynamic );
+
+
+				for ( let i = 0; i < loopNum; i ++ ) {
+
+					const rotationMatrix = mesh.userData.rotationSpeeds[ i ];
+					const id = ids[ i ];
+
+					mesh.getMatrixAt( id, matrix );
+					matrix.multiply( rotationMatrix );
+					mesh.setMatrixAt( id, matrix );
+
+				}
+
+			}
+
+		}
+
+		//
+
+		function sortFunction( list, camera ) {
+
+			// initialize options
+			this._options = this._options || {
+				get: el => el.z,
+				aux: new Array( this.maxGeometryCount )
+			};
+
+			const options = this._options;
+			options.reversed = this.material.transparent;
+
+			// convert depth to unsigned 32 bit range
+			const factor = ( 2 ** 32 - 1 ) / camera.far; // UINT32_MAX / max_depth
+			for ( let i = 0, l = list.length; i < l; i ++ ) {
+
+				list[ i ].z *= factor;
+
+			}
+
+			// perform a fast-sort using the hybrid radix sort function
+			radixSort( list, options );
+
+		}
+
+	</script>
+
+</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -140,6 +140,7 @@ const exceptionList = [
 	'webgpu_portal',
 	'webgpu_custom_fog',
 	'webgpu_instancing_morph',
+	'webgpu_mesh_batch',
 
 	// WebGPU idleTime and parseTime too low
 	'webgpu_compute_particles',