浏览代码

WebGPURenderer: Support for multiple render targets (#26409)

* experimental multi attachment shader

add support for webglmultiplerendertargets wip

* fix logic

* rework to use common path

* cleanup

* added antialias

---------

Co-authored-by: aardgoose <[email protected]>
aardgoose 1 年之前
父节点
当前提交
1f43ebe1b7

+ 1 - 0
examples/files.json

@@ -331,6 +331,7 @@
 		"webgpu_loader_gltf_sheen",
 		"webgpu_loader_gltf_sheen",
 		"webgpu_materials",
 		"webgpu_materials",
 		"webgpu_materials_video",
 		"webgpu_materials_video",
+		"webgpu_multiple_rendertargets",
 		"webgpu_morphtargets",
 		"webgpu_morphtargets",
 		"webgpu_occlusion",
 		"webgpu_occlusion",
 		"webgpu_particles",
 		"webgpu_particles",

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

@@ -30,6 +30,7 @@ export { default as TempNode } from './core/TempNode.js';
 export { default as UniformNode, uniform } from './core/UniformNode.js';
 export { default as UniformNode, uniform } from './core/UniformNode.js';
 export { default as VarNode, temp } from './core/VarNode.js';
 export { default as VarNode, temp } from './core/VarNode.js';
 export { default as VaryingNode, varying } from './core/VaryingNode.js';
 export { default as VaryingNode, varying } from './core/VaryingNode.js';
+export { default as OutputStructNode, outputStruct } from './core/OutputStructNode.js';
 
 
 import * as NodeUtils from './core/NodeUtils.js';
 import * as NodeUtils from './core/NodeUtils.js';
 export { NodeUtils };
 export { NodeUtils };

+ 22 - 0
examples/jsm/nodes/core/NodeBuilder.js

@@ -76,6 +76,7 @@ class NodeBuilder {
 		this.flowNodes = { vertex: [], fragment: [], compute: [] };
 		this.flowNodes = { vertex: [], fragment: [], compute: [] };
 		this.flowCode = { vertex: '', fragment: '', compute: [] };
 		this.flowCode = { vertex: '', fragment: '', compute: [] };
 		this.uniforms = { vertex: [], fragment: [], compute: [], index: 0 };
 		this.uniforms = { vertex: [], fragment: [], compute: [], index: 0 };
+		this.structs = { vertex: [], fragment: [], compute: [], index: 0 };
 		this.codes = { vertex: [], fragment: [], compute: [] };
 		this.codes = { vertex: [], fragment: [], compute: [] };
 		this.bindings = { vertex: [], fragment: [], compute: [] };
 		this.bindings = { vertex: [], fragment: [], compute: [] };
 		this.bindingsOffset = { vertex: 0, fragment: 0, compute: 0 };
 		this.bindingsOffset = { vertex: 0, fragment: 0, compute: 0 };
@@ -646,6 +647,27 @@ class NodeBuilder {
 
 
 	}
 	}
 
 
+	getStructTypeFromNode( node, shaderStage = this.shaderStage, name = null ) {
+
+		const nodeData = this.getDataFromNode( node, shaderStage );
+
+		let nodeStruct = nodeData.structType;
+
+		if ( nodeStruct === undefined ) {
+
+			const index = this.structs.index ++;
+
+			node.name = `StructType${index}`;
+			this.structs[ shaderStage ].push( node );
+
+			nodeData.structType = node;
+
+		}
+
+		return node;
+
+	}
+
 	getUniformFromNode( node, type, shaderStage = this.shaderStage, name = null ) {
 	getUniformFromNode( node, type, shaderStage = this.shaderStage, name = null ) {
 
 
 		const nodeData = this.getDataFromNode( node, shaderStage );
 		const nodeData = this.getDataFromNode( node, shaderStage );

+ 58 - 0
examples/jsm/nodes/core/OutputStructNode.js

@@ -0,0 +1,58 @@
+import Node, { addNodeClass } from './Node.js';
+import StructTypeNode from './StructTypeNode.js';
+import { nodeProxy } from '../shadernode/ShaderNode.js';
+
+class OutputStructNode extends Node {
+
+	constructor( ...members ) {
+
+		super();
+
+		this.isOutputStructNode = true;
+		this.members = members;
+
+	}
+
+	construct( builder ) {
+
+		super.construct( builder );
+
+		const members = this.members;
+		const types = [];
+
+		for ( let i = 0; i < members.length; i++ ) {
+
+			types.push( members[ i ].getNodeType( builder ) );
+
+		}
+
+		this.nodeType = builder.getStructTypeFromNode( new StructTypeNode( types ) ).name;
+
+	}
+
+	generate( builder, output ) {
+
+		const nodeVar = builder.getVarFromNode( this, this.nodeType );
+		const propertyName = builder.getPropertyName( nodeVar );
+
+		const members = this.members;
+
+		for ( let i = 0; i < members.length; i++ ) {
+
+			const snippet = members[ i ].build( builder, output );
+
+			builder.addLineFlowCode( `${propertyName}.m${i} = ${snippet}` );
+
+		}
+
+		return propertyName;
+
+	}
+
+}
+
+export default OutputStructNode;
+
+export const outputStruct = nodeProxy( OutputStructNode );
+
+addNodeClass( OutputStructNode );

+ 24 - 0
examples/jsm/nodes/core/StructTypeNode.js

@@ -0,0 +1,24 @@
+import Node, { addNodeClass } from './Node.js';
+
+class StructTypeNode extends Node {
+
+	constructor( types ) {
+
+		super();
+
+        this.types = types;
+		this.isStructTypeNode = true;
+
+	}
+
+    getMemberTypes() {
+
+        return this.types;
+
+    }
+
+}
+
+export default StructTypeNode;
+
+addNodeClass( StructTypeNode );

+ 9 - 1
examples/jsm/nodes/materials/NodeMaterial.js

@@ -345,7 +345,15 @@ class NodeMaterial extends ShaderMaterial {
 
 
 			if ( renderTarget !== null ) {
 			if ( renderTarget !== null ) {
 
 
-				outputColorSpace = renderTarget.texture.colorSpace;
+				if ( Array.isArray( renderTarget.texture ) ) {
+
+					outputColorSpace = renderTarget.texture[ 0 ].colorSpace;
+
+				} else {
+
+					outputColorSpace = renderTarget.texture.colorSpace;
+
+				}
 
 
 			} else {
 			} else {
 
 

+ 26 - 1
examples/jsm/renderers/common/RenderContexts.js

@@ -12,7 +12,32 @@ class RenderContexts {
 	get( scene, camera, renderTarget = null ) {
 	get( scene, camera, renderTarget = null ) {
 
 
 		const chainKey = [ scene, camera ];
 		const chainKey = [ scene, camera ];
-		const attachmentState = renderTarget === null ? 'default' : `${renderTarget.texture.format}:${renderTarget.samples}:${renderTarget.depthBuffer}:${renderTarget.stencilBuffer}`;
+
+		let attachmentState;
+
+		if ( renderTarget === null ) {
+
+			attachmentState = 'default';
+
+		} else {
+
+			let format, count;
+
+			if ( renderTarget.isWebGLMultipleRenderTargets ) {
+
+				format = renderTarget.texture[ 0 ].format;
+				count = renderTarget.texture.length;
+
+			} else {
+
+				format = renderTarget.texture.format;
+				count = 1;
+
+			}
+
+			attachmentState = `${count}:${format}:${renderTarget.samples}:${renderTarget.depthBuffer}:${renderTarget.stencilBuffer}`;
+
+		}
 
 
 		const chainMap = this.getChainMap( attachmentState );
 		const chainMap = this.getChainMap( attachmentState );
 
 

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

@@ -278,12 +278,12 @@ class Renderer {
 
 
 			const renderTargetData = this._textures.get( renderTarget );
 			const renderTargetData = this._textures.get( renderTarget );
 
 
-			renderContext.texture = renderTargetData.texture;
+			renderContext.textures = renderTargetData.textures;
 			renderContext.depthTexture = renderTargetData.depthTexture;
 			renderContext.depthTexture = renderTargetData.depthTexture;
 
 
 		} else {
 		} else {
 
 
-			renderContext.texture = null;
+			renderContext.textures = null;
 			renderContext.depthTexture = null;
 			renderContext.depthTexture = null;
 
 
 		}
 		}

+ 43 - 6
examples/jsm/renderers/common/Textures.js

@@ -20,10 +20,24 @@ class Textures extends DataMap {
 		const renderTargetData = this.get( renderTarget );
 		const renderTargetData = this.get( renderTarget );
 		const sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples;
 		const sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples;
 
 
-		const texture = renderTarget.texture;
+		let texture, textures;
+
+		if ( renderTarget.isWebGLMultipleRenderTargets ) {
+
+			textures = renderTarget.texture;
+			texture = renderTarget.texture[ 0 ];
+
+		} else {
+
+			textures = [ renderTarget.texture ];
+			texture = renderTarget.texture;
+
+		}
+
 		const size = this.getSize( texture );
 		const size = this.getSize( texture );
 
 
 		let depthTexture = renderTarget.depthTexture || renderTargetData.depthTexture;
 		let depthTexture = renderTarget.depthTexture || renderTargetData.depthTexture;
+		let textureNeedsUpdate = false;
 
 
 		if ( depthTexture === undefined ) {
 		if ( depthTexture === undefined ) {
 
 
@@ -37,7 +51,7 @@ class Textures extends DataMap {
 
 
 		if ( renderTargetData.width !== size.width || size.height !== renderTargetData.height ) {
 		if ( renderTargetData.width !== size.width || size.height !== renderTargetData.height ) {
 
 
-			texture.needsUpdate = true;
+			textureNeedsUpdate = true;
 			depthTexture.needsUpdate = true;
 			depthTexture.needsUpdate = true;
 
 
 			depthTexture.image.width = size.width;
 			depthTexture.image.width = size.width;
@@ -47,12 +61,12 @@ class Textures extends DataMap {
 
 
 		renderTargetData.width = size.width;
 		renderTargetData.width = size.width;
 		renderTargetData.height = size.height;
 		renderTargetData.height = size.height;
-		renderTargetData.texture = texture;
+		renderTargetData.textures = textures;
 		renderTargetData.depthTexture = depthTexture;
 		renderTargetData.depthTexture = depthTexture;
 
 
 		if ( renderTargetData.sampleCount !== sampleCount ) {
 		if ( renderTargetData.sampleCount !== sampleCount ) {
 
 
-			texture.needsUpdate = true;
+			textureNeedsUpdate = true;
 			depthTexture.needsUpdate = true;
 			depthTexture.needsUpdate = true;
 
 
 			renderTargetData.sampleCount = sampleCount;
 			renderTargetData.sampleCount = sampleCount;
@@ -61,7 +75,17 @@ class Textures extends DataMap {
 
 
 		const options = { sampleCount };
 		const options = { sampleCount };
 
 
-		this.updateTexture( texture, options );
+
+		for ( let i = 0; i < textures.length; i ++ ) {
+
+			const texture = textures[ i ];
+
+			if ( textureNeedsUpdate ) texture.needsUpdate = true;
+
+			this.updateTexture( texture, options );
+
+		}
+
 		this.updateTexture( depthTexture, options );
 		this.updateTexture( depthTexture, options );
 
 
 		// dispose handler
 		// dispose handler
@@ -76,7 +100,20 @@ class Textures extends DataMap {
 
 
 				renderTarget.removeEventListener( 'dispose', onDispose );
 				renderTarget.removeEventListener( 'dispose', onDispose );
 
 
-				this._destroyTexture( texture );
+				if ( textures !== undefined ) {
+
+					for ( let i = 0; i < textures.length; i ++ ) {
+
+						this._destroyTexture( textures[ i ] );
+
+					}
+
+				} else {
+
+					this._destroyTexture( texture );
+
+				}
+
 				this._destroyTexture( depthTexture );
 				this._destroyTexture( depthTexture );
 
 
 			};
 			};

+ 84 - 23
examples/jsm/renderers/webgpu/WebGPUBackend.js

@@ -178,30 +178,51 @@ class WebGPUBackend extends Backend {
 
 
 		const antialias = this.parameters.antialias;
 		const antialias = this.parameters.antialias;
 
 
-		if ( renderContext.texture !== null ) {
+		if ( renderContext.textures !== null ) {
 
 
-			const textureData = this.get( renderContext.texture );
-			const depthTextureData = this.get( renderContext.depthTexture );
+			const textures = renderContext.textures;
 
 
-			const view = textureData.texture.createView( {
-				baseMipLevel: 0,
-				mipLevelCount: 1,
-				baseArrayLayer: renderContext.activeCubeFace,
-				dimension: GPUTextureViewDimension.TwoD
-			} );
+			descriptor.colorAttachments = [];
 
 
-			if ( textureData.msaaTexture !== undefined ) {
+			const colorAttachments = descriptor.colorAttachments;
 
 
-				colorAttachment.view = textureData.msaaTexture.createView();
-				colorAttachment.resolveTarget = view;
+			for ( let i = 0; i < textures.length; i ++ ) {
 
 
-			} else {
+				const textureData = this.get( textures[ i ] );
 
 
-				colorAttachment.view = view;
-				colorAttachment.resolveTarget = undefined;
+				const textureView = textureData.texture.createView( {
+					baseMipLevel: 0,
+					mipLevelCount: 1,
+					baseArrayLayer: renderContext.activeCubeFace,
+					dimension: GPUTextureViewDimension.TwoD
+				} );
+
+				let view, resolveTarget;
+
+				if ( textureData.msaaTexture !== undefined ) {
+
+					view = textureData.msaaTexture.createView();
+					resolveTarget = textureView;
+
+				} else {
+
+					view = textureView;
+					resolveTarget = undefined;
+
+				}
+
+				colorAttachments.push( {
+					view,
+					resolveTarget,
+					loadOp: GPULoadOp.Load,
+					storeOp: GPUStoreOp.Store
+
+				} );
 
 
 			}
 			}
 
 
+			const depthTextureData = this.get( renderContext.depthTexture );
+
 			depthStencilAttachment.view = depthTextureData.texture.createView();
 			depthStencilAttachment.view = depthTextureData.texture.createView();
 
 
 			if ( renderContext.stencil && renderContext.depthTexture.format === DepthFormat ) {
 			if ( renderContext.stencil && renderContext.depthTexture.format === DepthFormat ) {
@@ -228,16 +249,44 @@ class WebGPUBackend extends Backend {
 
 
 		}
 		}
 
 
-		if ( renderContext.clearColor ) {
+		if ( renderContext.textures !== null ) {
+
+			const colorAttachments = descriptor.colorAttachments;
+
+			for ( let i = 0; i < colorAttachments.length; i ++ ) {
+
+				const colorAttachment = colorAttachments[ i ];
+
+				if ( renderContext.clearColor ) {
+
+					colorAttachment.clearValue = renderContext.clearColorValue;
+					colorAttachment.loadOp = GPULoadOp.Clear;
+					colorAttachment.storeOp = GPUStoreOp.Store;
+
+				} else {
+
+					colorAttachment.loadOp = GPULoadOp.Load;
+					colorAttachment.storeOp = GPUStoreOp.Store;
+
+				}
+
+			}
 
 
-			colorAttachment.clearValue = renderContext.clearColorValue;
-			colorAttachment.loadOp = GPULoadOp.Clear;
-			colorAttachment.storeOp = GPUStoreOp.Store;
 
 
 		} else {
 		} else {
 
 
-			colorAttachment.loadOp = GPULoadOp.Load;
-			colorAttachment.storeOp = GPUStoreOp.Store;
+			if ( renderContext.clearColor ) {
+
+				colorAttachment.clearValue = renderContext.clearColorValue;
+				colorAttachment.loadOp = GPULoadOp.Clear;
+				colorAttachment.storeOp = GPUStoreOp.Store;
+
+			} else {
+
+				colorAttachment.loadOp = GPULoadOp.Load;
+				colorAttachment.storeOp = GPUStoreOp.Store;
+
+			}
 
 
 		}
 		}
 
 
@@ -366,9 +415,21 @@ class WebGPUBackend extends Backend {
 
 
 		//
 		//
 
 
-		if ( renderContext.texture !== null && renderContext.texture.generateMipmaps === true ) {
+		if ( renderContext.textures !== null ) {
+
+			const textures = renderContext.textures;
+
+			for ( let i = 0; i < textures.length; i ++ ) {
+
+				const texture = textures[ i ];
 
 
-			this.textureUtils.generateMipmaps( renderContext.texture );
+				if ( texture.generateMipmaps === true ) {
+
+					this.textureUtils.generateMipmaps( texture );
+
+				}
+
+			}
 
 
 		}
 		}
 
 

+ 46 - 1
examples/jsm/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -500,6 +500,44 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 
 	}
 	}
 
 
+	getStructMembers( struct ) {
+
+		const snippets = [];
+		const members = struct.getMemberTypes();
+
+		for ( let i = 0; i < members.length; i ++ ) {
+
+			const member = members[ i ];
+			snippets.push( `\t@location( ${i} ) m${i} : ${ member }<f32>` );
+
+		}
+
+		return snippets.join( ',\n' );
+
+	}
+
+	getStructs( shaderStage ) {
+
+		const snippets = [];
+		const structs = this.structs[ shaderStage ];
+
+		for ( let index = 0, length = structs.length; index < length; index ++ ) {
+
+			const struct = structs[ index ];
+			const name = struct.name;
+
+			let snippet = `\struct ${ name } {\n`;
+			snippet += this.getStructMembers( struct );
+			snippet += '\n}';
+
+			snippets.push( snippet );
+
+		}
+
+		return snippets.join( '\n\n' );
+
+	}
+
 	getVar( type, name ) {
 	getVar( type, name ) {
 
 
 		return `var ${ name } : ${ this.getType( type ) }`;
 		return `var ${ name } : ${ this.getType( type ) }`;
@@ -548,6 +586,7 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 
 						attributesSnippet += ' @interpolate( flat )';
 						attributesSnippet += ' @interpolate( flat )';
 
 
+
 					}
 					}
 
 
 					snippets.push( `${ attributesSnippet } ${ varying.name } : ${ this.getType( varying.type ) }` );
 					snippets.push( `${ attributesSnippet } ${ varying.name } : ${ this.getType( varying.type ) }` );
@@ -723,13 +762,16 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 
 			}
 			}
 
 
+			const outputNode = mainNode.outputNode;
 			const stageData = shadersData[ shaderStage ];
 			const stageData = shadersData[ shaderStage ];
 
 
 			stageData.uniforms = this.getUniforms( shaderStage );
 			stageData.uniforms = this.getUniforms( shaderStage );
 			stageData.attributes = this.getAttributes( shaderStage );
 			stageData.attributes = this.getAttributes( shaderStage );
 			stageData.varyings = this.getVaryings( shaderStage );
 			stageData.varyings = this.getVaryings( shaderStage );
+			stageData.structs = this.getStructs( shaderStage );
 			stageData.vars = this.getVars( shaderStage );
 			stageData.vars = this.getVars( shaderStage );
 			stageData.codes = this.getCodes( shaderStage );
 			stageData.codes = this.getCodes( shaderStage );
+			stageData.returnType = ( outputNode !== undefined && outputNode.isOutputStructNode === true ) ? outputNode.nodeType : '@location( 0 ) vec4<f32>';
 			stageData.flow = flow;
 			stageData.flow = flow;
 
 
 		}
 		}
@@ -816,11 +858,14 @@ fn main( ${shaderData.attributes} ) -> NodeVaryingsStruct {
 // uniforms
 // uniforms
 ${shaderData.uniforms}
 ${shaderData.uniforms}
 
 
+// structs
+${shaderData.structs}
+
 // codes
 // codes
 ${shaderData.codes}
 ${shaderData.codes}
 
 
 @fragment
 @fragment
-fn main( ${shaderData.varyings} ) -> @location( 0 ) vec4<f32> {
+fn main( ${shaderData.varyings} ) -> ${shaderData.returnType} {
 
 
 	// vars
 	// vars
 	${shaderData.vars}
 	${shaderData.vars}

+ 32 - 8
examples/jsm/renderers/webgpu/utils/WebGPUPipelineUtils.js

@@ -64,25 +64,49 @@ class WebGPUPipelineUtils {
 
 
 		}
 		}
 
 
-		//
+		const colorWriteMask = this._getColorWriteMask( material );
+
+		const targets = [];
+
+		if ( renderObject.context.textures !== null ) {
+
+			const textures = renderObject.context.textures;
+
+			for ( let i = 0; i < textures.length; i ++ ) {
+
+				const colorFormat = utils.getTextureFormatGPU( textures[ i ] );
+
+				targets.push( {
+					format: colorFormat,
+					blend: blending,
+					writeMask: colorWriteMask
+				} );
+
+			}
+
+		} else {
+
+			const colorFormat = utils.getCurrentColorFormat( renderObject.context );
+
+			targets.push( {
+				format: colorFormat,
+				blend: blending,
+				writeMask: colorWriteMask
+			} );
+
+		}
 
 
 		const vertexModule = backend.get( vertexProgram ).module;
 		const vertexModule = backend.get( vertexProgram ).module;
 		const fragmentModule = backend.get( fragmentProgram ).module;
 		const fragmentModule = backend.get( fragmentProgram ).module;
 
 
 		const primitiveState = this._getPrimitiveState( object, geometry, material );
 		const primitiveState = this._getPrimitiveState( object, geometry, material );
-		const colorWriteMask = this._getColorWriteMask( material );
 		const depthCompare = this._getDepthCompare( material );
 		const depthCompare = this._getDepthCompare( material );
-		const colorFormat = utils.getCurrentColorFormat( renderObject.context );
 		const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context );
 		const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context );
 		const sampleCount = utils.getSampleCount( renderObject.context );
 		const sampleCount = utils.getSampleCount( renderObject.context );
 
 
 		pipelineData.pipeline = device.createRenderPipeline( {
 		pipelineData.pipeline = device.createRenderPipeline( {
 			vertex: Object.assign( {}, vertexModule, { buffers: vertexBuffers } ),
 			vertex: Object.assign( {}, vertexModule, { buffers: vertexBuffers } ),
-			fragment: Object.assign( {}, fragmentModule, { targets: [ {
-				format: colorFormat,
-				blend: blending,
-				writeMask: colorWriteMask
-			} ] } ),
+			fragment: Object.assign( {}, fragmentModule, { targets } ),
 			primitive: primitiveState,
 			primitive: primitiveState,
 			depthStencil: {
 			depthStencil: {
 				format: depthStencilFormat,
 				format: depthStencilFormat,

+ 6 - 5
examples/jsm/renderers/webgpu/utils/WebGPUUtils.js

@@ -40,9 +40,10 @@ class WebGPUUtils {
 
 
 		let format;
 		let format;
 
 
-		if ( renderContext.texture !== null ) {
+		if ( renderContext.textures !== null ) {
+
+			format = this.getTextureFormatGPU( renderContext.textures[ 0 ] );
 
 
-			format = this.getTextureFormatGPU( renderContext.texture );
 
 
 		} else {
 		} else {
 
 
@@ -56,9 +57,9 @@ class WebGPUUtils {
 
 
 	getCurrentColorSpace( renderContext ) {
 	getCurrentColorSpace( renderContext ) {
 
 
-		if ( renderContext.texture !== null ) {
+		if ( renderContext.textures !== null ) {
 
 
-			return renderContext.texture.colorSpace;
+			return renderContext.textures[ 0 ].colorSpace;
 
 
 		}
 		}
 
 
@@ -77,7 +78,7 @@ class WebGPUUtils {
 
 
 	getSampleCount( renderContext ) {
 	getSampleCount( renderContext ) {
 
 
-		if ( renderContext.texture !== null ) {
+		if ( renderContext.textures !== null ) {
 
 
 			return renderContext.sampleCount;
 			return renderContext.sampleCount;
 
 

二进制
examples/screenshots/webgpu_multiple_rendertargets.jpg


+ 221 - 0
examples/webgpu_multiple_rendertargets.html

@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - Multiple Render Targets</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">threejs</a> webgpu - Multiple RenderTargets
+		</div>
+
+		<!-- Import maps polyfill -->
+		<!-- Remove this when import maps will be widely supported -->
+		<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 { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			//import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { NodeMaterial, MeshBasicNodeMaterial, mix, modelNormalMatrix, normalGeometry, normalize, outputStruct, step, texture, uniform, uv, varying, vec2, vec4 } from 'three/nodes';
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			let camera, scene, renderer, torus;
+			let renderTarget;
+			let postScene, postCamera;
+
+			/*
+
+			const parameters = {
+				samples: 4,
+				wireframe: false
+			};
+
+			const gui = new GUI();
+			gui.add( parameters, 'samples', 0, 4 ).step( 1 );
+			gui.add( parameters, 'wireframe' );
+			gui.onChange( render );
+
+			*/
+
+			class WriteGBufferMaterial extends MeshBasicNodeMaterial {
+
+				constructor( diffuseTexture ) {
+
+					super();
+
+					this.lights = false;
+					this.diffuseTexture = diffuseTexture;
+
+					const vUv = varying( uv() );
+
+					const transformedNormal = modelNormalMatrix.mul( normalGeometry );
+					const vNormal = varying( normalize( transformedNormal ) );
+
+					const repeat = uniform( vec2( 5, 0.5 ) );
+
+					const gColor = texture( this.diffuseTexture, vUv.mul( repeat ) );
+					const gNormal = vec4( normalize( vNormal ), 1.0 );
+
+					this.outputNode = outputStruct( gColor, gNormal );
+
+				}
+
+			}
+
+			class ReadGBufferMaterial extends NodeMaterial {
+
+				constructor( tDiffuse, tNormal ) {
+
+					super();
+
+					const vUv = varying( uv() );
+
+					const diffuse = texture( tDiffuse, vUv );
+					const normal = texture( tNormal, vUv );
+
+					this.colorNode = mix( diffuse, normal, step( 0.5, vUv.x ) );
+					this.lights = false;
+
+				}
+
+			}
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( render );
+				renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
+				document.body.appendChild( renderer.domElement );
+
+				// Create a multi render target with Float buffers
+
+				renderTarget = new THREE.WebGLMultipleRenderTargets(
+					window.innerWidth * window.devicePixelRatio,
+					window.innerHeight * window.devicePixelRatio,
+					2,
+					{ minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, colorSpace: THREE.LinearSRGBColorSpace }
+				);
+
+				// Name our G-Buffer attachments for debugging
+
+				renderTarget.texture[ 0 ].name = 'diffuse';
+				renderTarget.texture[ 1 ].name = 'normal';
+
+				// Scene setup
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x222222 );
+
+				camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 50 );
+				camera.position.z = 4;
+
+				const loader = new THREE.TextureLoader();
+
+				const diffuse = loader.load( 'textures/hardwood2_diffuse.jpg', render );
+				diffuse.wrapS = THREE.RepeatWrapping;
+				diffuse.wrapT = THREE.RepeatWrapping;
+
+				torus = new THREE.Mesh(
+					new THREE.TorusKnotGeometry( 1, 0.3, 128, 32 ),
+					new WriteGBufferMaterial( diffuse )
+				);
+
+				scene.add( torus );
+
+				// PostProcessing setup
+
+				postScene = new THREE.Scene();
+				postCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+
+				postScene.add( new THREE.Mesh(
+					new THREE.PlaneGeometry( 2, 2 ),
+					new ReadGBufferMaterial( renderTarget.texture[ 0 ], renderTarget.texture[ 1 ] )
+				) );
+
+				// Controls
+
+				new OrbitControls( camera, renderer.domElement );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				const dpr = renderer.getPixelRatio();
+				renderTarget.setSize( window.innerWidth * dpr, window.innerHeight * dpr );
+
+				render();
+
+			}
+
+			function render( time ) {
+
+				/*
+
+				// Feature not yet working
+
+				renderTarget.samples = parameters.samples;
+
+				scene.traverse( function ( child ) {
+
+					if ( child.material !== undefined ) {
+
+						child.material.wireframe = parameters.wireframe;
+
+					}
+
+				} );
+
+				*/
+
+				torus.rotation.y = ( time / 1000 ) * .4;
+
+				// render scene into target
+				renderer.setRenderTarget( renderTarget );
+				renderer.render( scene, camera );
+
+				// render post FX
+				renderer.setRenderTarget( null );
+				renderer.render( postScene, postCamera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -131,6 +131,7 @@ const exceptionList = [
 	'webgpu_materials',
 	'webgpu_materials',
 	'webgpu_materials_video',
 	'webgpu_materials_video',
 	'webgpu_morphtargets',
 	'webgpu_morphtargets',
+	"webgpu_multiple_rendertargets",
 	'webgpu_occlusion',
 	'webgpu_occlusion',
 	'webgpu_particles',
 	'webgpu_particles',
 	'webgpu_rtt',
 	'webgpu_rtt',