Browse Source

WebGPURenderer: Auto-MRT (#28833)

* added mrt

* update using mrt

* cleanup

* cleanup

* cleanup

* update

* build

* fix normalWorld name

* fix background if used MRT

* fix copyFramebufferToTexture() if used MRT

* TSL: added emissive

* rename texturePass to passTexture

* added mrt support

* add mrt example

* cleanup

* cleanup

* description

* remove setMRT from PP

* update examples

* fix depth texture node

* improve material.mrtNode support

* update backdrop tone mapping

* add mrt mask example

* optimize

* optimize textures
sunag 1 year ago
parent
commit
0edacaa645

+ 2 - 0
examples/files.json

@@ -357,6 +357,8 @@
 		"webgpu_mirror",
 		"webgpu_morphtargets",
 		"webgpu_morphtargets_face",
+		"webgpu_mrt",
+		"webgpu_mrt_mask",
 		"webgpu_multiple_rendertargets",
 		"webgpu_multiple_rendertargets_readback",
 		"webgpu_multisampled_renderbuffers",

BIN
examples/screenshots/webgpu_backdrop.jpg


BIN
examples/screenshots/webgpu_mrt.jpg


BIN
examples/screenshots/webgpu_mrt_mask.jpg


BIN
examples/screenshots/webgpu_multiple_rendertargets.jpg


BIN
examples/screenshots/webgpu_multiple_rendertargets_readback.jpg


+ 1 - 1
examples/webgpu_backdrop.html

@@ -121,7 +121,7 @@
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animate );
-				renderer.toneMapping = THREE.LinearToneMapping;
+				renderer.toneMapping = THREE.NeutralToneMapping;
 				renderer.toneMappingExposure = 0.3;
 				document.body.appendChild( renderer.domElement );
 

+ 152 - 0
examples/webgpu_mrt.html

@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - mrt</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 - mrt<br />
+			Final / Beauty / Normal / Emissive / Diffuse
+		</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 { output, transformedNormalWorld, pass, step, diffuseColor, emissive, viewportTopLeft, mix, mrt, tslFn } from 'three/tsl';
+
+			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			let camera, scene, renderer;
+			let postProcessing;
+
+			init();
+
+			function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				// scene
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
+				camera.position.set( - 1.8, 0.6, 2.7 );
+
+				scene = new THREE.Scene();
+
+				new RGBELoader()
+					.setPath( 'textures/equirectangular/' )
+					.load( 'royal_esplanade_1k.hdr', function ( texture ) {
+
+						texture.mapping = THREE.EquirectangularReflectionMapping;
+
+						scene.background = texture;
+						scene.environment = texture;
+
+						// model
+
+						const loader = new GLTFLoader().setPath( 'models/gltf/DamagedHelmet/glTF/' );
+						loader.load( 'DamagedHelmet.gltf', function ( gltf ) {
+
+							scene.add( gltf.scene );
+
+						} );
+
+					} );
+
+				// renderer
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( render );
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				container.appendChild( renderer.domElement );
+
+				// post processing
+
+				const scenePass = pass( scene, camera, { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter } );
+				scenePass.setMRT( mrt( {
+					output: output,
+					normal: transformedNormalWorld.directionToColor(),
+					diffuse: diffuseColor,
+					emissive: emissive
+				} ) );
+
+				// optimize textures
+
+				const normalTexture = scenePass.getTexture( 'normal' );
+				const diffuseTexture = scenePass.getTexture( 'diffuse' );
+				const emissiveTexture = scenePass.getTexture( 'emissive' );
+
+				normalTexture.type = diffuseTexture.type = emissiveTexture.type = THREE.UnsignedByteType;
+
+				// post processing - mrt
+
+				postProcessing = new THREE.PostProcessing( renderer );
+				postProcessing.outputColorTransform = false;
+				postProcessing.outputNode = tslFn( () => {
+
+					const output = scenePass.getTextureNode( 'output' ); // output name is optional here
+					const normal = scenePass.getTextureNode( 'normal' );
+					const diffuse = scenePass.getTextureNode( 'diffuse' );
+					const emissive = scenePass.getTextureNode( 'emissive' );
+
+					const out = mix( output.renderOutput(), output, step( 0.2, viewportTopLeft.x ) );
+					const nor = mix( out, normal, step( 0.4, viewportTopLeft.x ) );
+					const emi = mix( nor, emissive, step( 0.6, viewportTopLeft.x ) );
+					const dif = mix( emi, diffuse, step( 0.8, viewportTopLeft.x ) );
+
+					return dif;
+
+				} )();
+
+				// controls
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 2;
+				controls.maxDistance = 10;
+				controls.target.set( 0, 0, - 0.2 );
+				controls.update();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function render() {
+
+				postProcessing.render();
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 170 - 0
examples/webgpu_mrt_mask.html

@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - mrt mask</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 - mrt mask
+			<br>The mask is applied followed by a gaussian blur only on some selected materials.
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../src/Three.WebGPU.js",
+					"three/tsl": "../src/Three.WebGPU.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { color, viewportTopLeft, mrt, output, pass, vec4 } from 'three/tsl';
+
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			let camera, scene, renderer;
+			let postProcessing;
+			let spheres, rotate = true;
+			let mixer, clock;
+
+			init();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.01, 100 );
+				camera.position.set( 1, 2, 3 );
+
+				scene = new THREE.Scene();
+				scene.backgroundNode = viewportTopLeft.y.mix( color( 0x66bbff ), color( 0x4466ff ) ).mul( .05 );
+				camera.lookAt( 0, 1, 0 );
+
+				clock = new THREE.Clock();
+
+				// lights
+
+				const light = new THREE.SpotLight( 0xffffff, 1 );
+				light.power = 2000;
+				camera.add( light );
+				scene.add( camera );
+
+				const loader = new GLTFLoader();
+				loader.load( 'models/gltf/Michelle.glb', function ( gltf ) {
+
+					const object = gltf.scene;
+					mixer = new THREE.AnimationMixer( object );
+
+					const material = object.children[ 0 ].children[ 0 ].material;
+
+					// add glow effect
+					material.mrtNode = mrt( { mask: output.add( 1 ) } );
+
+					const action = mixer.clipAction( gltf.animations[ 0 ] );
+					action.play();
+
+					scene.add( object );
+
+				} );
+
+				// spheres
+
+				const geometry = new THREE.SphereGeometry( .3, 32, 16 );
+
+				spheres = new THREE.Group();
+				scene.add( spheres );
+
+				function addSphere( color, mrtNode = null ) {
+
+					const distance = 1;
+					const id = spheres.children.length;
+					const rotation = THREE.MathUtils.degToRad( id * 90 );
+
+					const material = new THREE.MeshStandardNodeMaterial( { color } );
+					material.mrtNode = mrtNode;
+
+					const mesh = new THREE.Mesh( geometry, material );
+					mesh.position.set(
+						Math.cos( rotation ) * distance,
+						1,
+						Math.sin( rotation ) * distance
+					);
+
+					spheres.add( mesh );
+
+				}
+
+				addSphere( 0x0000ff, mrt( { mask: output } ) );
+				addSphere( 0x00ff00 );
+				addSphere( 0xff0000 );
+				addSphere( 0x00ffff );
+
+				// renderer
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.toneMappingExposure = 0.4;
+				document.body.appendChild( renderer.domElement );
+
+				// post processing
+
+				const scenePass = pass( scene, camera );
+				scenePass.setMRT( mrt( {
+					output: output.renderOutput(),
+					mask: vec4( 0 ) // empty as default, custom materials can set this
+				} ) );
+
+				const colorPass = scenePass.getTextureNode();
+				const maskPass = scenePass.getTextureNode( 'mask' );
+
+				postProcessing = new THREE.PostProcessing( renderer );
+				postProcessing.outputColorTransform = false;
+				postProcessing.outputNode = colorPass.add( maskPass.gaussianBlur( 1, 10 ).mul( .3 ) ).renderOutput();
+
+				// controls
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 1, 0 );
+				controls.addEventListener( 'start', () => rotate = false );
+				controls.addEventListener( 'end', () => rotate = true );
+				controls.update();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				const delta = clock.getDelta();
+
+				if ( mixer ) mixer.update( delta );
+
+				if ( rotate ) spheres.rotation.y += delta * 0.5;
+
+				postProcessing.render();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 18 - 91
examples/webgpu_multiple_rendertargets.html

@@ -25,74 +25,12 @@
 		<script type="module">
 
 			import * as THREE from 'three';
-			import { NodeMaterial, mix, modelNormalMatrix, normalGeometry, normalize, outputStruct, step, texture, uniform, uv, varying, vec2, vec4 } from 'three/tsl';
+			import { mix, vec2, step, texture, uv, viewportTopLeft, normalWorld, output, mrt } from 'three/tsl';
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
-			//import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 			let camera, scene, renderer, torus;
-			let quadMesh, renderTarget;
-
-			/*
-
-			const parameters = {
-				samples: 4,
-				wireframe: false
-			};
-
-			const gui = new GUI();
-			gui.add( parameters, 'samples', 0, 4 ).step( 1 );
-			gui.add( parameters, 'wireframe' );
-
-			*/
-
-			class WriteGBufferMaterial extends NodeMaterial {
-
-				constructor( diffuseTexture ) {
-
-					super();
-
-					this.lights = false;
-					this.fog = false;
-					this.colorSpaced = 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.fragmentNode = outputStruct( gColor, gNormal );
-
-				}
-
-			}
-
-			class ReadGBufferMaterial extends NodeMaterial {
-
-				constructor( tDiffuse, tNormal ) {
-
-					super();
-
-					this.lights = false;
-					this.fog = false;
-
-					const vUv = varying( uv() );
-
-					const diffuse = texture( tDiffuse, vUv );
-					const normal = texture( tNormal, vUv );
-
-					this.fragmentNode = mix( diffuse, normal, step( 0.5, vUv.x ) );
-
-				}
-
-			}
+			let postProcessing, renderTarget;
 
 			init();
 
@@ -114,10 +52,10 @@
 
 				// Name our G-Buffer attachments for debugging
 
-				renderTarget.textures[ 0 ].name = 'diffuse';
+				renderTarget.textures[ 0 ].name = 'output';
 				renderTarget.textures[ 1 ].name = 'normal';
 
-				// Scene setup
+				// Scene
 
 				scene = new THREE.Scene();
 				scene.background = new THREE.Color( 0x222222 );
@@ -132,16 +70,23 @@
 				diffuse.wrapS = THREE.RepeatWrapping;
 				diffuse.wrapT = THREE.RepeatWrapping;
 
-				torus = new THREE.Mesh(
-					new THREE.TorusKnotGeometry( 1, 0.3, 128, 32 ),
-					new WriteGBufferMaterial( diffuse )
-				);
+				const torusMaterial = new THREE.NodeMaterial();
+				torusMaterial.colorNode = texture( diffuse, uv().mul( vec2( 10, 4 ) ) );
 
+				torus = new THREE.Mesh( new THREE.TorusKnotGeometry( 1, 0.3, 128, 32 ), torusMaterial );
 				scene.add( torus );
 
-				// PostProcessing setup
+				// MRT
+
+				renderer.setMRT( mrt( {
+					'output': output,
+					'normal': normalWorld
+				} ) );
 
-				quadMesh = new THREE.QuadMesh( new ReadGBufferMaterial( renderTarget.textures[ 0 ], renderTarget.textures[ 1 ] ) );
+				// Post Processing
+
+				postProcessing = new THREE.PostProcessing( renderer );
+				postProcessing.outputNode = mix( texture( renderTarget.textures[ 0 ] ), texture( renderTarget.textures[ 1 ] ), step( 0.5, viewportTopLeft.x ) );
 
 				// Controls
 
@@ -165,24 +110,6 @@
 
 			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
@@ -191,7 +118,7 @@
 
 				// render post FX
 				renderer.setRenderTarget( null );
-				quadMesh.render( renderer );
+				postProcessing.render();
 
 			}
 

+ 23 - 61
examples/webgpu_multiple_rendertargets_readback.html

@@ -25,14 +25,14 @@
 		<script type="module">
 
 			import * as THREE from 'three';
-			import { mix, modelNormalMatrix, normalGeometry, normalize, outputStruct, step, texture, uniform, uv, varying, vec2, vec4 } from 'three/tsl';
+			import { mix, step, texture, viewportTopLeft, mrt, output, transformedNormalWorld, uv, vec2 } from 'three/tsl';
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 			let camera, scene, renderer, torus;
-			let quadMesh, renderTarget, readbackTarget, material, readbackMaterial, pixelBuffer, pixelBufferTexture;
+			let quadMesh, sceneMRT, renderTarget, readbackTarget, material, readbackMaterial, pixelBuffer, pixelBufferTexture;
 
 			const gui = new GUI();
 
@@ -42,54 +42,6 @@
 
 			gui.add( options, 'selection', [ 'mrt', 'diffuse', 'normal' ] );
 
-			class WriteGBufferMaterial extends THREE.NodeMaterial {
-
-				constructor( diffuseTexture ) {
-
-					super();
-
-					this.lights = false;
-					this.fog = false;
-					this.colorSpaced = 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.fragmentNode = outputStruct( gColor, gNormal );
-
-				}
-
-			}
-
-			class ReadGBufferMaterial extends THREE.NodeMaterial {
-
-				constructor( tDiffuse, tNormal ) {
-
-					super();
-
-					this.lights = false;
-					this.fog = false;
-
-					const vUv = varying( uv() );
-
-					const diffuse = texture( tDiffuse, vUv );
-					const normal = texture( tNormal, vUv );
-
-					this.fragmentNode = mix( diffuse, normal, step( 0.5, vUv.x ) );
-
-				}
-
-			}
-
 			init();
 
 			function init() {
@@ -110,10 +62,9 @@
 
 				// Name our G-Buffer attachments for debugging
 
-				renderTarget.textures[ 0 ].name = 'diffuse';
+				renderTarget.textures[ 0 ].name = 'output';
 				renderTarget.textures[ 1 ].name = 'normal';
 
-
 				// Init readback render target, readback data texture, readback material
 				// Be careful with the size! 512 is already big. Reading data back from the GPU is computationally intensive
 
@@ -129,8 +80,14 @@
 				readbackMaterial = new THREE.MeshBasicNodeMaterial();
 				readbackMaterial.colorNode = texture( pixelBufferTexture );
 
+				// MRT
 
-				// Scene setup
+				sceneMRT = mrt( {
+					'output': output,
+					'normal': transformedNormalWorld
+				} );
+
+				// Scene
 
 				scene = new THREE.Scene();
 				scene.background = new THREE.Color( 0x222222 );
@@ -138,7 +95,6 @@
 				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' );
@@ -146,15 +102,17 @@
 				diffuse.wrapS = THREE.RepeatWrapping;
 				diffuse.wrapT = THREE.RepeatWrapping;
 
-				torus = new THREE.Mesh(
-					new THREE.TorusKnotGeometry( 1, 0.3, 128, 32 ),
-					new WriteGBufferMaterial( diffuse )
-				);
+				const torusMaterial = new THREE.NodeMaterial();
+				torusMaterial.colorNode = texture( diffuse, uv().mul( vec2( 10, 4 ) ) );
 
+				torus = new THREE.Mesh( new THREE.TorusKnotGeometry( 1, 0.3, 128, 32 ), torusMaterial );
 				scene.add( torus );
 
+				// Output
+
+				material = new THREE.NodeMaterial();
+				material.colorNode = mix( texture( renderTarget.textures[ 0 ] ), texture( renderTarget.textures[ 1 ] ), step( 0.5, viewportTopLeft.x ) );
 
-				material = new ReadGBufferMaterial( renderTarget.textures[ 0 ], renderTarget.textures[ 1 ] );
 				quadMesh = new THREE.QuadMesh( material );
 
 				// Controls
@@ -183,14 +141,18 @@
 
 				torus.rotation.y = ( time / 1000 ) * .4;
 
+				const isMRT = selection === 'mrt';
+
 				// render scene into target
-				renderer.setRenderTarget( selection === 'mrt' ? renderTarget : readbackTarget );
+				renderer.setMRT( isMRT ? sceneMRT : null );
+				renderer.setRenderTarget( isMRT ? renderTarget : readbackTarget );
 				renderer.render( scene, camera );
 
 				// render post FX
+				renderer.setMRT( null );
 				renderer.setRenderTarget( null );
 
-				if ( selection === 'mrt' ) {
+				if ( isMRT ) {
 
 					quadMesh.material = material;
 

+ 3 - 2
src/nodes/Nodes.js

@@ -26,13 +26,14 @@ export { default as NodeUniform } from './core/NodeUniform.js';
 export { default as NodeVar } from './core/NodeVar.js';
 export { default as NodeVarying } from './core/NodeVarying.js';
 export { default as ParameterNode, parameter } from './core/ParameterNode.js';
-export { default as PropertyNode, property, varyingProperty, output, diffuseColor, roughness, metalness, clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, specularColor, shininess, dashSize, gapSize, pointWidth, alphaT, anisotropy, anisotropyB, anisotropyT } from './core/PropertyNode.js';
+export { default as PropertyNode, property, varyingProperty, output, diffuseColor, emissive, roughness, metalness, clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, specularColor, shininess, dashSize, gapSize, pointWidth, alphaT, anisotropy, anisotropyB, anisotropyT } from './core/PropertyNode.js';
 export { default as StackNode, stack } from './core/StackNode.js';
 export { default as TempNode } from './core/TempNode.js';
 export { default as UniformGroupNode, uniformGroup, objectGroup, renderGroup, frameGroup } from './core/UniformGroupNode.js';
 export { default as UniformNode, uniform } from './core/UniformNode.js';
 export { default as VaryingNode, varying } from './core/VaryingNode.js';
 export { default as OutputStructNode, outputStruct } from './core/OutputStructNode.js';
+export { default as MRTNode, mrt } from './core/MRTNode.js';
 
 import * as NodeUtils from './core/NodeUtils.js';
 export { NodeUtils };
@@ -135,7 +136,7 @@ export { default as FilmNode, film } from './display/FilmNode.js';
 export { default as Lut3DNode, lut3D } from './display/Lut3DNode.js';
 export { default as RenderOutputNode, renderOutput } from './display/RenderOutputNode.js';
 
-export { default as PassNode, pass, texturePass, depthPass } from './display/PassNode.js';
+export { default as PassNode, pass, passTexture, depthPass } from './display/PassNode.js';
 
 // code
 export { default as ExpressionNode, expression } from './code/ExpressionNode.js';

+ 1 - 1
src/nodes/accessors/NormalNode.js

@@ -8,7 +8,7 @@ import { vec3 } from '../shadernode/ShaderNode.js';
 export const normalGeometry = /*#__PURE__*/ attribute( 'normal', 'vec3', vec3( 0, 1, 0 ) );
 export const normalLocal = /*#__PURE__*/ normalGeometry.toVar( 'normalLocal' );
 export const normalView = /*#__PURE__*/ varying( modelNormalMatrix.mul( normalLocal ), 'v_normalView' ).normalize().toVar( 'normalView' );
-export const normalWorld = /*#__PURE__*/ varying( normalView.transformDirection( cameraViewMatrix ), 'v_normalWorld' ).normalize().toVar( 'transformedNormalWorld' );
+export const normalWorld = /*#__PURE__*/ varying( normalView.transformDirection( cameraViewMatrix ), 'v_normalWorld' ).normalize().toVar( 'normalWorld' );
 export const transformedNormalView = /*#__PURE__*/ property( 'vec3', 'transformedNormalView' );
 export const transformedNormalWorld = /*#__PURE__*/ transformedNormalView.transformDirection( cameraViewMatrix ).normalize().toVar( 'transformedNormalWorld' );
 export const transformedClearcoatNormalView = /*#__PURE__*/ property( 'vec3', 'transformedClearcoatNormalView' );

+ 82 - 0
src/nodes/core/MRTNode.js

@@ -0,0 +1,82 @@
+import { addNodeClass } from './Node.js';
+import OutputStructNode from './OutputStructNode.js';
+import { nodeProxy, vec4 } from '../shadernode/ShaderNode.js';
+
+function getTextureIndex( textures, name ) {
+
+	for ( let i = 0; i < textures.length; i ++ ) {
+
+		if ( textures[ i ].name === name ) {
+
+			return i;
+
+		}
+
+	}
+
+	return - 1;
+
+}
+
+class MRTNode extends OutputStructNode {
+
+	constructor( outputNodes ) {
+
+		super();
+
+		this.outputNodes = outputNodes;
+
+		this.isMRTNode = true;
+
+	}
+
+	merge( mrtNode ) {
+
+		const outputs = { ...this.outputNodes, ...mrtNode.outputNodes };
+
+		return mrt( outputs );
+
+	}
+
+	setup( builder ) {
+
+		const outputNodes = this.outputNodes;
+		const mrt = builder.renderer.getRenderTarget();
+
+		const members = [];
+
+		if ( Array.isArray( outputNodes ) ) {
+
+			for ( let i = 0; i < outputNodes.length; i ++ ) {
+
+				members.push( vec4( outputNodes[ i ] ) );
+
+			}
+
+		} else {
+
+			const textures = mrt.textures;
+
+			for ( const name in outputNodes ) {
+
+				const index = getTextureIndex( textures, name );
+
+				members[ index ] = vec4( outputNodes[ name ] );
+
+			}
+
+		}
+
+		this.members = members;
+
+		return super.setup( builder );
+
+	}
+
+}
+
+export default MRTNode;
+
+export const mrt = nodeProxy( MRTNode );
+
+addNodeClass( 'MRTNode', MRTNode );

+ 1 - 0
src/nodes/core/PropertyNode.js

@@ -53,6 +53,7 @@ export const property = ( type, name ) => nodeObject( new PropertyNode( type, na
 export const varyingProperty = ( type, name ) => nodeObject( new PropertyNode( type, name, true ) );
 
 export const diffuseColor = nodeImmutable( PropertyNode, 'vec4', 'DiffuseColor' );
+export const emissive = nodeImmutable( PropertyNode, 'vec3', 'EmissiveColor' );
 export const roughness = nodeImmutable( PropertyNode, 'float', 'Roughness' );
 export const metalness = nodeImmutable( PropertyNode, 'float', 'Metalness' );
 export const clearcoat = nodeImmutable( PropertyNode, 'float', 'Clearcoat' );

+ 2 - 2
src/nodes/display/AfterImageNode.js

@@ -3,7 +3,7 @@ import { nodeObject, addNodeElement, tslFn, float, vec4 } from '../shadernode/Sh
 import { NodeUpdateType } from '../core/constants.js';
 import { uv } from '../accessors/UVNode.js';
 import { texture } from '../accessors/TextureNode.js';
-import { texturePass } from './PassNode.js';
+import { passTexture } from './PassNode.js';
 import { uniform } from '../core/UniformNode.js';
 import { sign, max } from '../math/MathNode.js';
 import QuadMesh from '../../renderers/common/QuadMesh.js';
@@ -31,7 +31,7 @@ class AfterImageNode extends TempNode {
 		this._oldRT = new RenderTarget();
 		this._oldRT.texture.name = 'AfterImageNode.old';
 
-		this._textureNode = texturePass( this, this._compRT.texture );
+		this._textureNode = passTexture( this, this._compRT.texture );
 
 		this.updateBeforeType = NodeUpdateType.RENDER;
 

+ 2 - 2
src/nodes/display/AnamorphicNode.js

@@ -5,7 +5,7 @@ import { uniform } from '../core/UniformNode.js';
 import { NodeUpdateType } from '../core/constants.js';
 import { threshold } from './ColorAdjustmentNode.js';
 import { uv } from '../accessors/UVNode.js';
-import { texturePass } from './PassNode.js';
+import { passTexture } from './PassNode.js';
 import QuadMesh from '../../renderers/common/QuadMesh.js';
 
 import { Vector2 } from '../../math/Vector2.js';
@@ -31,7 +31,7 @@ class AnamorphicNode extends TempNode {
 
 		this._invSize = uniform( new Vector2() );
 
-		this._textureNode = texturePass( this, this._renderTarget.texture );
+		this._textureNode = passTexture( this, this._renderTarget.texture );
 
 		this.updateBeforeType = NodeUpdateType.RENDER;
 

+ 9 - 2
src/nodes/display/GaussianBlurNode.js

@@ -3,7 +3,7 @@ import { nodeObject, addNodeElement, tslFn, float, vec2, vec4 } from '../shadern
 import { NodeUpdateType } from '../core/constants.js';
 import { mul } from '../math/OperatorNode.js';
 import { uv } from '../accessors/UVNode.js';
-import { texturePass } from './PassNode.js';
+import { passTexture } from './PassNode.js';
 import { uniform } from '../core/UniformNode.js';
 import QuadMesh from '../../renderers/common/QuadMesh.js';
 
@@ -34,7 +34,7 @@ class GaussianBlurNode extends TempNode {
 		this._verticalRT = new RenderTarget();
 		this._verticalRT.texture.name = 'GaussianBlurNode.vertical';
 
-		this._textureNode = texturePass( this, this._verticalRT.texture );
+		this._textureNode = passTexture( this, this._verticalRT.texture );
 
 		this.updateBeforeType = NodeUpdateType.RENDER;
 
@@ -61,6 +61,8 @@ class GaussianBlurNode extends TempNode {
 		const map = textureNode.value;
 
 		const currentRenderTarget = renderer.getRenderTarget();
+		const currentMRT = renderer.getMRT();
+
 		const currentTexture = textureNode.value;
 
 		quadMesh1.material = this._material;
@@ -73,6 +75,10 @@ class GaussianBlurNode extends TempNode {
 		this._horizontalRT.texture.type = textureType;
 		this._verticalRT.texture.type = textureType;
 
+		// clear
+
+		renderer.setMRT( null );
+
 		// horizontal
 
 		renderer.setRenderTarget( this._horizontalRT );
@@ -93,6 +99,7 @@ class GaussianBlurNode extends TempNode {
 		// restore
 
 		renderer.setRenderTarget( currentRenderTarget );
+		renderer.setMRT( currentMRT );
 		textureNode.value = currentTexture;
 
 	}

+ 84 - 11
src/nodes/display/PassNode.js

@@ -41,6 +41,32 @@ class PassTextureNode extends TextureNode {
 
 }
 
+class PassMultipleTextureNode extends PassTextureNode {
+
+	constructor( passNode, textureName ) {
+
+		super( passNode, null );
+
+		this.textureName = textureName;
+
+	}
+
+	setup( builder ) {
+
+		this.value = this.passNode.getTexture( this.textureName );
+
+		return super.setup( builder );
+
+	}
+
+	clone() {
+
+		return new this.constructor( this.passNode, this.textureName );
+
+	}
+
+}
+
 class PassNode extends TempNode {
 
 	constructor( scope, scene, camera, options = {} ) {
@@ -59,43 +85,87 @@ class PassNode extends TempNode {
 		const depthTexture = new DepthTexture();
 		depthTexture.isRenderTargetTexture = true;
 		//depthTexture.type = FloatType;
-		depthTexture.name = 'PostProcessingDepth';
+		depthTexture.name = 'depth';
 
-		const renderTarget = new RenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType, ...options } );
-		renderTarget.texture.name = 'PostProcessing';
+		const renderTarget = new RenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType, ...options, } );
+		renderTarget.texture.name = 'output';
 		renderTarget.depthTexture = depthTexture;
 
 		this.renderTarget = renderTarget;
 
 		this.updateBeforeType = NodeUpdateType.FRAME;
 
-		this._textureNode = nodeObject( new PassTextureNode( this, renderTarget.texture ) );
-		this._depthTextureNode = nodeObject( new PassTextureNode( this, depthTexture ) );
+		this._textures = {
+			output: renderTarget.texture,
+			depth: depthTexture
+		};
+
+		this._nodes = {};
 
 		this._linearDepthNode = null;
 		this._viewZNode = null;
 		this._cameraNear = uniform( 0 );
 		this._cameraFar = uniform( 0 );
 
+		this._mrt = null;
+
 		this.isPassNode = true;
 
 	}
 
+	setMRT( mrt ) {
+
+		this._mrt = mrt;
+
+		return this;
+
+	}
+
+	getMRT() {
+
+		return this._mrt;
+
+	}
+
 	isGlobal() {
 
 		return true;
 
 	}
 
-	getTextureNode() {
+	getTexture( name ) {
+
+		let texture = this._textures[ name ];
+
+		if ( texture === undefined ) {
 
-		return this._textureNode;
+			const refTexture = this.renderTarget.texture;
+
+			texture = refTexture.clone();
+			texture.isRenderTargetTexture = true;
+			texture.name = name;
+
+			this._textures[ name ] = texture;
+
+			this.renderTarget.textures.push( texture );
+
+		}
+
+		return texture;
 
 	}
 
-	getTextureDepthNode() {
+	getTextureNode( name = 'output' ) {
+
+		let textureNode = this._nodes[ name ];
+
+		if ( textureNode === undefined ) {
+
+			this._nodes[ name ] = textureNode = nodeObject( new PassMultipleTextureNode( this, name ) );
+
+		}
 
-		return this._depthTextureNode;
+		return textureNode;
 
 	}
 
@@ -106,7 +176,7 @@ class PassNode extends TempNode {
 			const cameraNear = this._cameraNear;
 			const cameraFar = this._cameraFar;
 
-			this._viewZNode = perspectiveDepthToViewZ( this._depthTextureNode, cameraNear, cameraFar );
+			this._viewZNode = perspectiveDepthToViewZ( this.getTextureNode( 'depth' ), cameraNear, cameraFar );
 
 		}
 
@@ -160,15 +230,18 @@ class PassNode extends TempNode {
 		this.setSize( size.width, size.height );
 
 		const currentRenderTarget = renderer.getRenderTarget();
+		const currentMRT = renderer.getMRT();
 
 		this._cameraNear.value = camera.near;
 		this._cameraFar.value = camera.far;
 
 		renderer.setRenderTarget( this.renderTarget );
+		renderer.setMRT( this._mrt );
 
 		renderer.render( scene, camera );
 
 		renderer.setRenderTarget( currentRenderTarget );
+		renderer.setMRT( currentMRT );
 
 	}
 
@@ -207,7 +280,7 @@ PassNode.DEPTH = 'depth';
 export default PassNode;
 
 export const pass = ( scene, camera, options ) => nodeObject( new PassNode( PassNode.COLOR, scene, camera, options ) );
-export const texturePass = ( pass, texture ) => nodeObject( new PassTextureNode( pass, texture ) );
+export const passTexture = ( pass, texture ) => nodeObject( new PassTextureNode( pass, texture ) );
 export const depthPass = ( scene, camera ) => nodeObject( new PassNode( PassNode.DEPTH, scene, camera ) );
 
 addNodeClass( 'PassNode', PassNode );

+ 33 - 2
src/nodes/materials/NodeMaterial.js

@@ -3,7 +3,7 @@ import { NormalBlending } from '../../constants.js';
 
 import { getNodeChildren, getCacheKey } from '../core/NodeUtils.js';
 import { attribute } from '../core/AttributeNode.js';
-import { output, diffuseColor, varyingProperty } from '../core/PropertyNode.js';
+import { output, diffuseColor, emissive, varyingProperty } from '../core/PropertyNode.js';
 import { materialAlphaTest, materialColor, materialOpacity, materialEmissive, materialNormal, materialLightMap, materialAOMap } from '../accessors/MaterialNode.js';
 import { modelViewProjection } from '../accessors/ModelViewProjectionNode.js';
 import { transformedNormalView, normalLocal } from '../accessors/NormalNode.js';
@@ -62,6 +62,7 @@ class NodeMaterial extends Material {
 		this.shadowPositionNode = null;
 
 		this.outputNode = null;
+		this.mrtNode = null;
 
 		this.fragmentNode = null;
 		this.vertexNode = null;
@@ -125,6 +126,33 @@ class NodeMaterial extends Material {
 
 			if ( this.outputNode !== null ) resultNode = this.outputNode;
 
+			// MRT
+
+			const renderTarget = builder.renderer.getRenderTarget();
+
+			if ( renderTarget !== null ) {
+
+				const mrt = builder.renderer.getMRT();
+				const materialMRT = this.mrtNode;
+
+				if ( mrt !== null ) {
+
+					resultNode = mrt;
+
+					if ( materialMRT !== null ) {
+
+						resultNode = mrt.merge( materialMRT );
+
+					}
+
+				} else if ( materialMRT !== null ) {
+
+					resultNode = materialMRT;
+
+				}
+
+			}
+
 		} else {
 
 			let fragmentNode = this.fragmentNode;
@@ -442,7 +470,9 @@ class NodeMaterial extends Material {
 
 		if ( ( emissiveNode && emissiveNode.isNode === true ) || ( material.emissive && material.emissive.isColor === true ) ) {
 
-			outgoingLightNode = outgoingLightNode.add( vec3( emissiveNode ? emissiveNode : materialEmissive ) );
+			emissive.assign( vec3( emissiveNode ? emissiveNode : materialEmissive ) );
+
+			outgoingLightNode = outgoingLightNode.add( emissive );
 
 		}
 
@@ -578,6 +608,7 @@ class NodeMaterial extends Material {
 		this.shadowPositionNode = source.shadowPositionNode;
 
 		this.outputNode = source.outputNode;
+		this.mrtNode = source.mrtNode;
 
 		this.fragmentNode = source.fragmentNode;
 		this.vertexNode = source.vertexNode;

+ 2 - 1
src/renderers/common/Background.js

@@ -67,8 +67,9 @@ class Background extends DataMap {
 				nodeMaterial.depthTest = false;
 				nodeMaterial.depthWrite = false;
 				nodeMaterial.fog = false;
+				nodeMaterial.lights = false;
 				nodeMaterial.vertexNode = viewProj;
-				nodeMaterial.fragmentNode = backgroundMeshNode;
+				nodeMaterial.colorNode = backgroundMeshNode;
 
 				sceneData.backgroundMeshNode = backgroundMeshNode;
 				sceneData.backgroundMesh = backgroundMesh = new Mesh( new SphereGeometry( 1, 32, 32 ), nodeMaterial );

+ 2 - 2
src/renderers/common/PostProcessing.js

@@ -31,7 +31,7 @@ class PostProcessing {
 
 		//
 
-		quadMesh.render( this.renderer );
+		quadMesh.render( renderer );
 
 		//
 
@@ -72,7 +72,7 @@ class PostProcessing {
 
 		//
 
-		await quadMesh.renderAsync( this.renderer );
+		await quadMesh.renderAsync( renderer );
 
 		//
 

+ 16 - 0
src/renderers/common/Renderer.js

@@ -120,6 +120,8 @@ class Renderer {
 		this._activeCubeFace = 0;
 		this._activeMipmapLevel = 0;
 
+		this._mrt = null;
+
 		this._renderObjectFunction = null;
 		this._currentRenderObjectFunction = null;
 		this._currentRenderBundle = null;
@@ -344,6 +346,20 @@ class Renderer {
 
 	}
 
+	setMRT( mrt ) {
+
+		this._mrt = mrt;
+
+		return this;
+
+	}
+
+	getMRT() {
+
+		return this._mrt;
+
+	}
+
 	_renderBundle( bundle, sceneRef, lightsNode ) {
 
 		const { object, camera, renderList } = bundle;

+ 6 - 1
src/renderers/webgpu/WebGPUBackend.js

@@ -1418,7 +1418,12 @@ class WebGPUBackend extends Backend {
 
 		if ( texture.generateMipmaps ) this.textureUtils.generateMipmaps( texture );
 
-		descriptor.colorAttachments[ 0 ].loadOp = GPULoadOp.Load;
+		for ( let i = 0; i < descriptor.colorAttachments.length; i ++ ) {
+
+			descriptor.colorAttachments[ i ].loadOp = GPULoadOp.Load;
+
+		}
+
 		if ( renderContext.depth ) descriptor.depthStencilAttachment.depthLoadOp = GPULoadOp.Load;
 		if ( renderContext.stencil ) descriptor.depthStencilAttachment.stencilLoadOp = GPULoadOp.Load;