Bläddra i källkod

WebGPURenderer: Occlusion queries with `renderer.isOccluded( object )` (#26335)

* add support for meshPhongNodeMaterial

* WIP occlusion query WebGPU

* bass query count to backend

* add  new example

* minor cleanup/changes

* cleanup draw conditions - more readable

* add dummy screenshot and remove import

* drop references to objects

* fix issue with multi pass renders.

* address @sunag's recommendation

* add puppeteer exception

* Renderer.isOccluded()

---------

Co-authored-by: aardgoose <[email protected]>
Co-authored-by: sunag <[email protected]>
aardgoose 1 år sedan
förälder
incheckning
a0cb76952d

+ 1 - 0
examples/files.json

@@ -330,6 +330,7 @@
 		"webgpu_materials",
 		"webgpu_materials_video",
 		"webgpu_morphtargets",
+		"webgpu_occlusion",
 		"webgpu_particles",
 		"webgpu_rtt",
 		"webgpu_sandbox",

+ 6 - 0
examples/jsm/renderers/common/RenderList.js

@@ -61,6 +61,8 @@ class RenderList {
 		this.lightsNode = new LightsNode( [] );
 		this.lightsArray = [];
 
+		this.occlusionQueryCount = 0;
+
 	}
 
 	begin() {
@@ -71,6 +73,8 @@ class RenderList {
 		this.transparent.length = 0;
 		this.lightsArray.length = 0;
 
+		this.occlusionQueryCount = 0;
+
 		return this;
 
 	}
@@ -117,6 +121,8 @@ class RenderList {
 
 		const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group );
 
+		if ( object.occlusionTest === true ) this.occlusionQueryCount ++;
+
 		( material.transparent === true ? this.transparent : this.opaque ).push( renderItem );
 
 	}

+ 9 - 0
examples/jsm/renderers/common/Renderer.js

@@ -289,6 +289,7 @@ class Renderer {
 		}
 
 		renderContext.activeCubeFace = activeCubeFace;
+		renderContext.occlusionQueryCount = renderList.occlusionQueryCount;
 
 		//
 
@@ -550,6 +551,14 @@ class Renderer {
 
 	}
 
+	isOccluded( object ) {
+
+		const renderContext = this._currentRenderContext || this._lastRenderContext;
+
+		return renderContext ? this.backend.isOccluded( renderContext, object ) : false;
+
+	}
+
 	clear( color = true, depth = true, stencil = true ) {
 
 		const renderContext = this._currentRenderContext || this._lastRenderContext;

+ 152 - 1
examples/jsm/renderers/webgpu/WebGPUBackend.js

@@ -62,6 +62,7 @@ class WebGPUBackend extends Backend {
 		this.bindingUtils = new WebGPUBindingUtils( this );
 		this.pipelineUtils = new WebGPUPipelineUtils( this );
 		this.textureUtils = new WebGPUTextureUtils( this );
+		this.occludedResolveCache = new Map();
 
 	}
 
@@ -135,6 +136,32 @@ class WebGPUBackend extends Backend {
 		const renderContextData = this.get( renderContext );
 
 		const device = this.device;
+		const occlusionQueryCount = renderContext.occlusionQueryCount;
+
+		let occlusionQuerySet;
+
+		if ( occlusionQueryCount > 0 ) {
+
+			if ( renderContextData.currentOcclusionQuerySet ) renderContextData.currentOcclusionQuerySet.destroy();
+			if ( renderContextData.currentOcclusionQueryBuffer ) renderContextData.currentOcclusionQueryBuffer.destroy();
+
+			// Get a reference to the array of objects with queries. The renderContextData property
+			// can be changed by another render pass before the buffer.mapAsyc() completes.
+			renderContextData.currentOcclusionQuerySet = renderContextData.occlusionQuerySet;
+			renderContextData.currentOcclusionQueryBuffer = renderContextData.occlusionQueryBuffer;
+			renderContextData.currentOcclusionQueryObjects = renderContextData.occlusionQueryObjects;
+
+			//
+
+			occlusionQuerySet = device.createQuerySet( { type: 'occlusion', count: occlusionQueryCount } );
+
+			renderContextData.occlusionQuerySet = occlusionQuerySet;
+			renderContextData.occlusionQueryIndex = 0;
+			renderContextData.occlusionQueryObjects = new Array( occlusionQueryCount );
+
+			renderContextData.lastOcclusionObject = null;
+
+		}
 
 		const descriptor = {
 			colorAttachments: [ {
@@ -142,7 +169,8 @@ class WebGPUBackend extends Backend {
 			} ],
 			depthStencilAttachment: {
 				view: null
-			}
+			},
+			occlusionQuerySet
 		};
 
 		const colorAttachment = descriptor.colorAttachments[ 0 ];
@@ -282,9 +310,58 @@ class WebGPUBackend extends Backend {
 	finishRender( renderContext ) {
 
 		const renderContextData = this.get( renderContext );
+		const occlusionQueryCount = renderContext.occlusionQueryCount;
+
+		if ( occlusionQueryCount > renderContextData.occlusionQueryIndex ) {
+
+			renderContextData.currentPass.endOcclusionQuery();
+
+		}
 
 		renderContextData.currentPass.end();
 
+		if ( occlusionQueryCount > 0 ) {
+
+			const bufferSize = occlusionQueryCount * 8; // 8 byte entries for query results
+
+			//
+
+			let queryResolveBuffer = this.occludedResolveCache.get( bufferSize );
+
+			if ( queryResolveBuffer === undefined ) {
+
+				queryResolveBuffer = this.device.createBuffer(
+					{
+						size: bufferSize,
+						usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
+					}
+				);
+
+				this.occludedResolveCache.set( bufferSize, queryResolveBuffer );
+
+			}
+
+			//
+
+			const readBuffer = this.device.createBuffer(
+				{
+					size: bufferSize,
+					usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
+				}
+			);
+
+			// two buffers required here - WebGPU doesn't allow usage of QUERY_RESOLVE & MAP_READ to be combined
+			renderContextData.encoder.resolveQuerySet( renderContextData.occlusionQuerySet, 0, occlusionQueryCount, queryResolveBuffer, 0 );
+			renderContextData.encoder.copyBufferToBuffer( queryResolveBuffer, 0, readBuffer, 0, bufferSize );
+
+			renderContextData.occlusionQueryBuffer = readBuffer;
+
+			//
+
+			this.resolveOccludedAsync( renderContext );
+
+		}
+
 		this.device.queue.submit( [ renderContextData.encoder.finish() ] );
 
 		//
@@ -297,6 +374,52 @@ class WebGPUBackend extends Backend {
 
 	}
 
+	isOccluded( renderContext, object ) {
+
+		const renderContextData = this.get( renderContext );
+
+		return renderContextData.occluded && renderContextData.occluded.has( object );
+
+	}
+
+	async resolveOccludedAsync( renderContext ) {
+
+		const renderContextData = this.get( renderContext );
+
+		// handle occlusion query results
+
+		const { currentOcclusionQueryBuffer, currentOcclusionQueryObjects } = renderContextData;
+
+		if ( currentOcclusionQueryBuffer && currentOcclusionQueryObjects ) {
+
+			const occluded = new WeakSet();
+
+			renderContextData.currentOcclusionQueryObjects = null;
+			renderContextData.currentOcclusionQueryBuffer = null;
+
+			await currentOcclusionQueryBuffer.mapAsync( GPUMapMode.READ );
+
+			const buffer = currentOcclusionQueryBuffer.getMappedRange();
+			const results = new BigUint64Array( buffer );
+
+			for ( let i = 0; i < currentOcclusionQueryObjects.length; i++ ) {
+
+				if ( results[ i ] !== 0n ) {
+
+					occluded.add( currentOcclusionQueryObjects[ i ], true );
+
+				}
+
+			}
+
+			currentOcclusionQueryBuffer.destroy();
+
+			renderContextData.occluded = occluded;
+
+		}
+
+	}
+
 	updateViewport( renderContext ) {
 
 		const { currentPass } = this.get( renderContext );
@@ -486,6 +609,34 @@ class WebGPUBackend extends Backend {
 
 		}
 
+		// occlusion queries - handle multiple consecutive draw calls for an object
+
+		if ( contextData.occlusionQuerySet !== undefined  ) {
+
+			const lastObject = contextData.lastOcclusionObject;
+
+			if ( lastObject !== object ) {
+
+				if ( lastObject !== null && lastObject.occlusionTest === true ) {
+
+					passEncoderGPU.endOcclusionQuery();
+					contextData.occlusionQueryIndex ++;
+
+				}
+
+				if ( object.occlusionTest === true ) {
+
+					passEncoderGPU.beginOcclusionQuery( contextData.occlusionQueryIndex );
+					contextData.occlusionQueryObjects[ contextData.occlusionQueryIndex ] = object;
+
+				}
+
+				contextData.lastOcclusionObject = object;
+
+			}
+
+		}
+
 		// draw
 
 		const drawRange = geometry.drawRange;

BIN
examples/screenshots/webgpu_occlusion.jpg


+ 153 - 0
examples/webgpu_occlusion.html

@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js - WebGPU - Occlusion</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="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - WebGPU - Occlusion<br />
+			The plane changes color if the sphere behind it is rendered
+		</div>
+
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { nodeObject, uniform, Node, NodeUpdateType, MeshPhongNodeMaterial } from 'three/nodes';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			let camera, scene, renderer, controls;
+
+			class OcclusionNode extends Node {
+
+				constructor( testObject, normalColor, occludedColor ) {
+
+					super( 'vec3' );
+
+					this.updateType = NodeUpdateType.OBJECT;
+
+					this.uniformNode = uniform( new THREE.Color() );
+
+					this.testObject = testObject;
+					this.normalColor = normalColor;
+					this.occludedColor = occludedColor;
+
+				}
+
+				async update( frame ) {
+
+					const isOccluded = frame.renderer.isOccluded( this.testObject );
+
+					this.uniformNode.value.copy( isOccluded ? this.occludedColor : this.normalColor );
+
+				}
+
+				construct( /* builder */ ) {
+
+					return this.uniformNode;
+
+				}
+
+			}
+
+			init();
+			render();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.01, 100 );
+				camera.position.z = 7;
+
+				scene = new THREE.Scene();
+
+				// lights
+
+				const ambientLight = new THREE.AmbientLight( 0xb0b0b0 );
+
+				const light = new THREE.DirectionalLight( 0xFFFFFF, 1.0 );
+				light.position.set( 0.32, 0.39, 0.7 );
+
+				scene.add( ambientLight );
+				scene.add( light );
+
+				// models
+
+				const planeGeometry = new THREE.PlaneGeometry( 2, 2 );
+				const sphereGeometry = new THREE.SphereGeometry( 0.5 );
+
+				const plane = new THREE.Mesh( planeGeometry, new MeshPhongNodeMaterial( { color: 0x00ff00 } ) );
+				const sphere = new THREE.Mesh( sphereGeometry, new MeshPhongNodeMaterial( { color: 0xffff00 } ) );
+
+				const instanceUniform = nodeObject( new OcclusionNode( sphere, new THREE.Color( 0x00ff00 ), new THREE.Color( 0x0000ff ) ) );
+
+				plane.material.colorNode = instanceUniform;
+
+				sphere.position.z = - 1;
+				sphere.occlusionTest = true;
+
+				scene.add( plane );
+				scene.add( sphere );
+
+				// renderer
+
+				renderer = new WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				// controls
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 3;
+				controls.maxDistance = 25;
+				controls.addEventListener( 'change', render );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -129,6 +129,7 @@ const exceptionList = [
 	'webgpu_materials',
 	'webgpu_materials_video',
 	'webgpu_morphtargets',
+	'webgpu_occlusion',
 	'webgpu_particles',
 	'webgpu_rtt',
 	'webgpu_sandbox',