ソースを参照

Merge branch 'dev' into e2e

# Conflicts:
#	examples/screenshots/webgpu_backdrop.jpg
Mr.doob 1 年間 前
コミット
df50af8c9b
34 ファイル変更1365 行追加470 行削除
  1. 3 1
      examples/files.json
  2. 2 0
      examples/index.html
  3. 1 0
      examples/jsm/Addons.js
  4. 592 0
      examples/jsm/loaders/UltraHDRLoader.js
  5. BIN
      examples/screenshots/webgl_loader_texture_hdrjpg.jpg
  6. BIN
      examples/screenshots/webgl_loader_texture_ultrahdr.jpg
  7. BIN
      examples/screenshots/webgpu_backdrop.jpg
  8. BIN
      examples/screenshots/webgpu_mrt.jpg
  9. BIN
      examples/screenshots/webgpu_mrt_mask.jpg
  10. BIN
      examples/screenshots/webgpu_multiple_rendertargets.jpg
  11. BIN
      examples/screenshots/webgpu_multiple_rendertargets_readback.jpg
  12. 1 1
      examples/tags.json
  13. BIN
      examples/textures/equirectangular/spruit_sunrise_2k.hdr.jpg
  14. 0 289
      examples/webgl_loader_texture_hdrjpg.html
  15. 152 0
      examples/webgl_loader_texture_ultrahdr.html
  16. 1 1
      examples/webgpu_backdrop.html
  17. 152 0
      examples/webgpu_mrt.html
  18. 170 0
      examples/webgpu_mrt_mask.html
  19. 18 91
      examples/webgpu_multiple_rendertargets.html
  20. 23 61
      examples/webgpu_multiple_rendertargets_readback.html
  21. 3 2
      src/nodes/Nodes.js
  22. 1 1
      src/nodes/accessors/NormalNode.js
  23. 82 0
      src/nodes/core/MRTNode.js
  24. 1 0
      src/nodes/core/PropertyNode.js
  25. 2 2
      src/nodes/display/AfterImageNode.js
  26. 2 2
      src/nodes/display/AnamorphicNode.js
  27. 9 2
      src/nodes/display/GaussianBlurNode.js
  28. 84 11
      src/nodes/display/PassNode.js
  29. 7 0
      src/nodes/materials/MeshBasicNodeMaterial.js
  30. 33 2
      src/nodes/materials/NodeMaterial.js
  31. 2 1
      src/renderers/common/Background.js
  32. 2 2
      src/renderers/common/PostProcessing.js
  33. 16 0
      src/renderers/common/Renderer.js
  34. 6 1
      src/renderers/webgpu/WebGPUBackend.js

+ 3 - 1
examples/files.json

@@ -118,8 +118,8 @@
 		"webgl_loader_tilt",
 		"webgl_loader_texture_dds",
 		"webgl_loader_texture_exr",
+		"webgl_loader_texture_ultrahdr",
 		"webgl_loader_texture_hdr",
-		"webgl_loader_texture_hdrjpg",
 		"webgl_loader_texture_ktx",
 		"webgl_loader_texture_ktx2",
 		"webgl_loader_texture_logluv",
@@ -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",

+ 2 - 0
examples/index.html

@@ -106,6 +106,8 @@
 
 				if ( validRedirects.has( file ) === true ) {
 
+					content.querySelector( `a[href="${ file }.html"]` ).scrollIntoView();
+
 					selectFile( file );
 					viewer.src = validRedirects.get( file );
 					viewer.style.display = 'unset';

+ 1 - 0
examples/jsm/Addons.js

@@ -117,6 +117,7 @@ export * from './loaders/PDBLoader.js';
 export * from './loaders/PLYLoader.js';
 export * from './loaders/PVRLoader.js';
 export * from './loaders/RGBELoader.js';
+export * from './loaders/UltraHDRLoader.js';
 export * from './loaders/RGBMLoader.js';
 export * from './loaders/STLLoader.js';
 export * from './loaders/SVGLoader.js';

+ 592 - 0
examples/jsm/loaders/UltraHDRLoader.js

@@ -0,0 +1,592 @@
+import {
+	ClampToEdgeWrapping,
+	DataTexture,
+	DataUtils,
+	FileLoader,
+	HalfFloatType,
+	LinearFilter,
+	LinearMipMapLinearFilter,
+	LinearSRGBColorSpace,
+	Loader,
+	RGBAFormat,
+	UVMapping,
+} from 'three';
+
+// UltraHDR Image Format - https://developer.android.com/media/platform/hdr-image-format
+// Originally proposed as gainmap-js using a WASM dependency - https://github.com/MONOGRID/gainmap-js
+
+/**
+ *
+ * Short format brief:
+ *
+ *  [JPEG headers]
+ *  [XMP metadata describing the MPF container and *both* SDR and gainmap images]
+ *  [Optional metadata] [EXIF] [ICC Profile]
+ *  [SDR image]
+ *  [XMP metadata describing only the gainmap image]
+ *  [Gainmap image]
+ *
+ * Each section is separated by a 0xFFXX byte followed by a descriptor byte (0xFFE0, 0xFFE1, 0xFFE2.)
+ * Binary image storages are prefixed with a unique 0xFFD8 16-bit descriptor.
+ */
+
+/**
+ * Current feature set:
+ * - JPEG headers (required)
+ * - XMP metadata (required)
+ *  - XMP validation (not implemented)
+ * - EXIF profile (not implemented)
+ * - ICC profile (not implemented)
+ * - Binary storage for SDR & HDR images (required)
+ * - Gainmap metadata (required)
+ * - Non-JPEG image formats (not implemented)
+ * - Primary image as an HDR image (not implemented)
+ */
+
+/* Calculating this SRGB powers is extremely slow for 4K images and can be sufficiently precalculated for a 3-4x speed boost */
+const SRGB_TO_LINEAR = Array( 1024 )
+	.fill( 0 )
+	.map( ( _, value ) =>
+		Math.pow( ( value / 255 ) * 0.9478672986 + 0.0521327014, 2.4 )
+	);
+
+class UltraHDRLoader extends Loader {
+
+	constructor( manager ) {
+
+		super( manager );
+
+		this.type = HalfFloatType;
+
+	}
+
+	setDataType( value ) {
+
+		this.type = value;
+
+		return this;
+
+	}
+
+	parse( buffer, onLoad ) {
+
+		const xmpMetadata = {
+			version: null,
+			baseRenditionIsHDR: null,
+			gainMapMin: null,
+			gainMapMax: null,
+			gamma: null,
+			offsetSDR: null,
+			offsetHDR: null,
+			hdrCapacityMin: null,
+			hdrCapacityMax: null,
+		};
+		const textDecoder = new TextDecoder();
+
+		const data = new DataView( buffer );
+		const dataAsString = textDecoder.decode( data );
+
+		/* Minimal sufficient validation - https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format */
+		if ( ! dataAsString.includes( 'hdrgm:Version="1.0"' ) ) {
+
+			throw new Error( 'THREE.UltraHDRLoader: Not a valid UltraHDR image' );
+
+		}
+
+		let byteOffset = 0;
+		const sections = [];
+
+		while ( byteOffset < data.byteLength ) {
+
+			const byte = data.getUint8( byteOffset );
+
+			if ( byte === 0xff ) {
+
+				const leadingByte = data.getUint8( byteOffset + 1 );
+
+				if (
+					[
+						/* Valid section headers */
+						0xd8, // SOI
+						0xe0, // APP0
+						0xe1, // APP1
+						0xe2, // APP2
+					].includes( leadingByte )
+				) {
+
+					sections.push( {
+						sectionType: leadingByte,
+						section: [ byte, leadingByte ],
+						sectionOffset: byteOffset + 2,
+					} );
+
+					byteOffset += 2;
+
+				} else {
+
+					sections[ sections.length - 1 ].section.push( byte, leadingByte );
+
+					byteOffset += 2;
+
+				}
+
+			} else {
+
+				sections[ sections.length - 1 ].section.push( byte );
+
+				byteOffset ++;
+
+			}
+
+		}
+
+		let primaryImage, gainmapImage;
+
+		for ( let i = 0; i < sections.length; i ++ ) {
+
+			const { sectionType, section, sectionOffset } = sections[ i ];
+
+			if ( sectionType === 0xe0 ) {
+
+				/* JPEG Header - no useful information */
+
+			} else if ( sectionType === 0xe1 ) {
+
+				/* XMP Metadata */
+
+				this._parseXMPMetadata(
+					textDecoder.decode( new Uint8Array( section ) ),
+					xmpMetadata
+				);
+
+			} else if ( sectionType === 0xe2 ) {
+
+				/* Data Sections - MPF / EXIF / ICC Profile */
+
+				const sectionData = new DataView(
+					new Uint8Array( section.slice( 2 ) ).buffer
+				);
+				const sectionHeader = sectionData.getUint32( 2, false );
+
+				if ( sectionHeader === 0x4d504600 ) {
+
+					/* MPF Section */
+
+					/* Section contains a list of static bytes and ends with offsets indicating location of SDR and gainmap images */
+					/* First bytes after header indicate little / big endian ordering (0x49492A00 - LE / 0x4D4D002A - BE) */
+					/*
+            ... 60 bytes indicating tags, versions, etc. ...
+
+            bytes | bits | description
+
+            4       32     primary image size
+            4       32     primary image offset
+            2       16     0x0000
+            2       16     0x0000
+
+            4       32     0x00000000
+            4       32     gainmap image size
+            4       32     gainmap image offset
+            2       16     0x0000
+            2       16     0x0000
+          */
+
+					const mpfLittleEndian = sectionData.getUint32( 6 ) === 0x49492a00;
+					const mpfBytesOffset = 60;
+
+					/* SDR size includes the metadata length, SDR offset is always 0 */
+
+					const primaryImageSize = sectionData.getUint32(
+						mpfBytesOffset,
+						mpfLittleEndian
+					);
+					const primaryImageOffset = sectionData.getUint32(
+						mpfBytesOffset + 4,
+						mpfLittleEndian
+					);
+
+					/* Gainmap size is an absolute value starting from its offset, gainmap offset needs 6 bytes padding to take into account 0x00 bytes at the end of XMP */
+					const gainmapImageSize = sectionData.getUint32(
+						mpfBytesOffset + 16,
+						mpfLittleEndian
+					);
+					const gainmapImageOffset =
+						sectionData.getUint32( mpfBytesOffset + 20, mpfLittleEndian ) +
+						sectionOffset +
+						6;
+
+					primaryImage = new Uint8Array(
+						data.buffer,
+						primaryImageOffset,
+						primaryImageSize
+					);
+
+					gainmapImage = new Uint8Array(
+						data.buffer,
+						gainmapImageOffset,
+						gainmapImageSize
+					);
+
+				}
+
+			}
+
+		}
+
+		if ( primaryImage && gainmapImage ) {
+
+			this._applyGainmapToSDR(
+				xmpMetadata,
+				primaryImage,
+				gainmapImage,
+				( hdrBuffer, width, height ) => {
+
+					onLoad( {
+						width,
+						height,
+						data: hdrBuffer,
+						format: RGBAFormat,
+						type: this.type,
+					} );
+
+				},
+				( error ) => {
+
+					throw new Error( error );
+
+				}
+			);
+
+		} else {
+
+			throw new Error( 'THREE.UltraHDRLoader: Could not parse UltraHDR images' );
+
+		}
+
+	}
+
+	load( url, onLoad, onError ) {
+
+		const texture = new DataTexture(
+			this.type === HalfFloatType ? new Uint16Array() : new Float32Array(),
+			0,
+			0,
+			RGBAFormat,
+			this.type,
+			UVMapping,
+			ClampToEdgeWrapping,
+			ClampToEdgeWrapping,
+			LinearFilter,
+			LinearMipMapLinearFilter,
+			1,
+			LinearSRGBColorSpace
+		);
+		texture.generateMipmaps = true;
+		texture.flipY = true;
+
+		const loader = new FileLoader( this.manager );
+		loader.setResponseType( 'arraybuffer' );
+		loader.setRequestHeader( this.requestHeader );
+		loader.setPath( this.path );
+		loader.setWithCredentials( this.withCredentials );
+		loader.load( url, ( buffer ) => {
+
+			try {
+
+				this.parse(
+					buffer,
+					( texData ) => {
+
+						texture.image = {
+							data: texData.data,
+							width: texData.width,
+							height: texData.height,
+						};
+						texture.needsUpdate = true;
+
+						if ( onLoad ) onLoad( texture, texData );
+
+					},
+					onError
+				);
+
+			} catch ( error ) {
+
+				if ( onError ) onError( error );
+
+				console.error( error );
+
+			}
+
+		} );
+
+		return texture;
+
+	}
+
+	_parseXMPMetadata( xmpDataString, xmpMetadata ) {
+
+		const domParser = new DOMParser();
+
+		const xmpXml = domParser.parseFromString(
+			xmpDataString.substring(
+				xmpDataString.indexOf( '<' ),
+				xmpDataString.lastIndexOf( '>' ) + 1
+			),
+			'text/xml'
+		);
+
+		/* Determine if given XMP metadata is the primary GContainer descriptor or a gainmap descriptor */
+		const [ hasHDRContainerDescriptor ] = xmpXml.getElementsByTagName(
+			'Container:Directory'
+		);
+
+		if ( hasHDRContainerDescriptor ) {
+
+			/* There's not much useful information in the container descriptor besides memory-validation */
+
+		} else {
+
+			/* Gainmap descriptor - defaults from https://developer.android.com/media/platform/hdr-image-format#HDR_gain_map_metadata */
+
+			const [ gainmapNode ] = xmpXml.getElementsByTagName( 'rdf:Description' );
+
+			xmpMetadata.version = gainmapNode.getAttribute( 'hdrgm:Version' ) || '1.0';
+			xmpMetadata.baseRenditionIsHDR =
+				gainmapNode.getAttribute( 'hdrgm:BaseRenditionIsHDR' ) === 'True';
+			xmpMetadata.gainMapMin = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:GainMapMin' ) || 0.0
+			);
+			xmpMetadata.gainMapMax = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:GainMapMax' ) || 1.0
+			);
+			xmpMetadata.gamma = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:Gamma' ) || 1.0
+			);
+			xmpMetadata.offsetSDR = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:OffsetSDR' ) / ( 1 / 64 )
+			);
+			xmpMetadata.offsetHDR = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:OffsetHDR' ) / ( 1 / 64 )
+			);
+			xmpMetadata.hdrCapacityMin = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:HDRCapacityMin' ) || 0.0
+			);
+			xmpMetadata.hdrCapacityMax = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:HDRCapacityMax' ) || 1.0
+			);
+
+		}
+
+	}
+
+	_srgbToLinear( value ) {
+
+		if ( value / 255 < 0.04045 ) {
+
+			return ( value / 255 ) * 0.0773993808;
+
+		}
+
+		if ( value < 1024 ) {
+
+			return SRGB_TO_LINEAR[ ~ ~ value ];
+
+		}
+
+		return Math.pow( ( value / 255 ) * 0.9478672986 + 0.0521327014, 2.4 );
+
+	}
+
+	_applyGainmapToSDR(
+		xmpMetadata,
+		sdrBuffer,
+		gainmapBuffer,
+		onSuccess,
+		onError
+	) {
+
+		const getImageDataFromBuffer = ( buffer ) =>
+			new Promise( ( resolve, reject ) => {
+
+				const imageLoader = document.createElement( 'img' );
+
+				imageLoader.onload = () => {
+
+					const image = {
+						width: imageLoader.naturalWidth,
+						height: imageLoader.naturalHeight,
+						source: imageLoader,
+					};
+
+					URL.revokeObjectURL( imageLoader.src );
+
+					resolve( image );
+
+				};
+
+				imageLoader.onerror = () => {
+
+					URL.revokeObjectURL( imageLoader.src );
+
+					reject();
+
+				};
+
+				imageLoader.src = URL.createObjectURL(
+					new Blob( [ buffer ], { type: 'image/jpeg' } )
+				);
+
+			} );
+
+		Promise.all( [
+			getImageDataFromBuffer( sdrBuffer ),
+			getImageDataFromBuffer( gainmapBuffer ),
+		] )
+			.then( ( [ sdrImage, gainmapImage ] ) => {
+
+				const sdrImageAspect = sdrImage.width / sdrImage.height;
+				const gainmapImageAspect = gainmapImage.width / gainmapImage.height;
+
+				if ( sdrImageAspect !== gainmapImageAspect ) {
+
+					onError(
+						'THREE.UltraHDRLoader Error: Aspect ratio mismatch between SDR and Gainmap images'
+					);
+
+					return;
+
+				}
+
+				const canvas = document.createElement( 'canvas' );
+				const ctx = canvas.getContext( '2d', {
+					willReadFrequently: true,
+					colorSpace: 'srgb',
+				} );
+
+				canvas.width = sdrImage.width;
+				canvas.height = sdrImage.height;
+
+				/* Use out-of-the-box interpolation of Canvas API to scale gainmap to fit the SDR resolution */
+				ctx.drawImage(
+					gainmapImage.source,
+					0,
+					0,
+					gainmapImage.width,
+					gainmapImage.height,
+					0,
+					0,
+					sdrImage.width,
+					sdrImage.height
+				);
+				const gainmapImageData = ctx.getImageData(
+					0,
+					0,
+					sdrImage.width,
+					sdrImage.height,
+					{ colorSpace: 'srgb' }
+				);
+
+				ctx.drawImage( sdrImage.source, 0, 0 );
+				const sdrImageData = ctx.getImageData(
+					0,
+					0,
+					sdrImage.width,
+					sdrImage.height,
+					{ colorSpace: 'srgb' }
+				);
+
+				/* HDR Recovery formula - https://developer.android.com/media/platform/hdr-image-format#use_the_gain_map_to_create_adapted_HDR_rendition */
+				let hdrBuffer;
+
+				if ( this.type === HalfFloatType ) {
+
+					hdrBuffer = new Uint16Array( sdrImageData.data.length ).fill( 23544 );
+
+				} else {
+
+					hdrBuffer = new Float32Array( sdrImageData.data.length ).fill( 255 );
+
+				}
+
+				const maxDisplayBoost = Math.sqrt(
+					Math.pow(
+						/* 1.8 instead of 2 near-perfectly rectifies approximations introduced by precalculated SRGB_TO_LINEAR values */
+						1.8,
+						xmpMetadata.hdrCapacityMax
+					)
+				);
+				const unclampedWeightFactor =
+					( Math.log2( maxDisplayBoost ) - xmpMetadata.hdrCapacityMin ) /
+					( xmpMetadata.hdrCapacityMax - xmpMetadata.hdrCapacityMin );
+				const weightFactor = Math.min(
+					Math.max( unclampedWeightFactor, 0.0 ),
+					1.0
+				);
+				const useGammaOne = xmpMetadata.gamma === 1.0;
+
+				for (
+					let pixelIndex = 0;
+					pixelIndex < sdrImageData.data.length;
+					pixelIndex += 4
+				) {
+
+					const x = ( pixelIndex / 4 ) % sdrImage.width;
+					const y = Math.floor( pixelIndex / 4 / sdrImage.width );
+
+					for ( let index = pixelIndex; index < pixelIndex + 3; index ++ ) {
+
+						const sdrValue = sdrImageData.data[ index ];
+
+						const gainmapIndex = ( y * sdrImage.width + x ) * 4;
+						const gainmapValue = gainmapImageData.data[ gainmapIndex ] / 255.0;
+
+						/* Gamma is 1.0 by default */
+						const logRecovery = useGammaOne
+							? gainmapValue
+							: Math.pow( gainmapValue, 1.0 / xmpMetadata.gamma );
+
+						const logBoost =
+							xmpMetadata.gainMapMin * ( 1.0 - logRecovery ) +
+							xmpMetadata.gainMapMax * logRecovery;
+
+						const hdrValue =
+						( sdrValue + xmpMetadata.offsetSDR ) *
+						( logBoost * weightFactor === 0.0
+							? 1.0
+							: Math.pow( 2, logBoost * weightFactor ) ) -
+					xmpMetadata.offsetHDR;
+
+						const linearHDRValue = Math.min(
+							Math.max(
+								this._srgbToLinear( hdrValue ),
+								0
+							),
+							65504
+						);
+
+						hdrBuffer[ index ] =
+							this.type === HalfFloatType
+								? DataUtils.toHalfFloat( linearHDRValue )
+								: linearHDRValue;
+
+					}
+
+				}
+
+				onSuccess( hdrBuffer, sdrImage.width, sdrImage.height );
+
+			} )
+			.catch( () => {
+
+				throw new Error(
+					'THREE.UltraHDRLoader Error: Could not parse UltraHDR images'
+				);
+
+			} );
+
+	}
+
+}
+
+export { UltraHDRLoader };

BIN
examples/screenshots/webgl_loader_texture_hdrjpg.jpg


BIN
examples/screenshots/webgl_loader_texture_ultrahdr.jpg


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/tags.json

@@ -44,7 +44,7 @@
 	"webgl_loader_ifc": [ "external" ],
 	"webgl_loader_ldraw": [ "lego" ],
 	"webgl_loader_pdb": [ "molecules", "css2d" ],
-	"webgl_loader_texture_hdrjpg": [ "external", "hdr", "jpg", "ultrahdr", "gainmap" ],
+	"webgl_loader_texture_ultrahdr": [ "hdr", "jpg", "ultrahdr" ],
 	"webgl_loader_ttf": [ "text", "font" ],
 	"webgl_lod": [ "level", "details" ],
 	"webgl_materials_blending": [ "alpha" ],

BIN
examples/textures/equirectangular/spruit_sunrise_2k.hdr.jpg


+ 0 - 289
examples/webgl_loader_texture_hdrjpg.html

@@ -1,289 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-	<head>
-		<title>three.js webgl - hdr jpg</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>
-			.lbl {
-				color: #fff;
-				font-size: 16px;
-				font-weight: bold;
-				position: absolute;
-				bottom: 0px;
-				z-index: 100;
-				text-shadow: #000 1px 1px 1px;
-				background-color: rgba(0,0,0,0.85);
-				padding: 1em;
-			}
-
-			#lbl_left {
-				text-align:left;
-				left:0px;
-			}
-		</style>
-	</head>
-
-	<body>
-		<div id="info">
-			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - hdr jpg loader <br/>
-			Converted from hdr with <a href="https://gainmap-creator.mono-grid.com/" target="_blank" rel="noopener">converter</a>. <br />
-			See external <a href="https://github.com/MONOGRID/gainmap-js" target="_blank" rel="noopener">gainmap-js</a> for more information.
-		</div>
-
-		<div id="lbl_left" class="lbl"></div>
-
-		<script type="importmap">
-			{
-				"imports": {
-					"three": "../build/three.module.js",
-					"three/addons/": "./jsm/",
-					"@monogrid/gainmap-js": "https://cdn.jsdelivr.net/npm/@monogrid/[email protected]/dist/decode.js"
-				}
-			}
-		</script>
-
-		<script type="module">
-
-			import * as THREE from 'three';
-
-			import Stats from 'three/addons/libs/stats.module.js';
-
-			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
-			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
-			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
-
-			import { HDRJPGLoader } from '@monogrid/gainmap-js';
-
-			const params = {
-				envMap: 'HDR JPG',
-				roughness: 0.0,
-				metalness: 1.0,
-				exposure: 1.0,
-				debug: false
-			};
-
-			let container, stats;
-			let camera, scene, renderer, controls;
-			let torusMesh, planeMesh;
-			let hdrJpg, hdrJpgPMREMRenderTarget, hdrJpgEquirectangularMap;
-			let hdrPMREMRenderTarget, hdrEquirectangularMap;
-
-
-			const fileSizes = {};
-			const resolutions = {};
-
-			init();
-
-			function init() {
-
-				const lbl = document.getElementById( 'lbl_left' );
-
-				container = document.createElement( 'div' );
-				document.body.appendChild( container );
-
-				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 500 );
-				camera.position.set( 0, 0, - 120 );
-
-				scene = new THREE.Scene();
-
-				renderer = new THREE.WebGLRenderer();
-				renderer.toneMapping = THREE.ACESFilmicToneMapping;
-
-				//
-
-				let geometry = new THREE.TorusKnotGeometry( 18, 8, 200, 40, 1, 3 );
-				let material = new THREE.MeshStandardMaterial( {
-					color: 0xffffff,
-					metalness: params.metalness,
-					roughness: params.roughness
-				} );
-
-				torusMesh = new THREE.Mesh( geometry, material );
-				scene.add( torusMesh );
-
-
-				geometry = new THREE.PlaneGeometry( 200, 200 );
-				material = new THREE.MeshBasicMaterial();
-
-				planeMesh = new THREE.Mesh( geometry, material );
-				planeMesh.position.y = - 50;
-				planeMesh.rotation.x = - Math.PI * 0.5;
-				scene.add( planeMesh );
-
-
-				const pmremGenerator = new THREE.PMREMGenerator( renderer );
-				pmremGenerator.compileEquirectangularShader();
-
-				THREE.DefaultLoadingManager.onLoad = function ( ) {
-
-					pmremGenerator.dispose();
-
-				};
-
-
-
-				hdrJpg = new HDRJPGLoader( renderer )
-					.load( 'textures/equirectangular/spruit_sunrise_4k.hdr.jpg', function ( ) {
-
-						resolutions[ 'HDR JPG' ] = hdrJpg.width + 'x' + hdrJpg.height;
-
-						displayStats( 'HDR JPG' );
-
-						hdrJpgEquirectangularMap = hdrJpg.renderTarget.texture;
-						hdrJpgPMREMRenderTarget = pmremGenerator.fromEquirectangular( hdrJpgEquirectangularMap );
-
-						hdrJpgEquirectangularMap.mapping = THREE.EquirectangularReflectionMapping;
-						hdrJpgEquirectangularMap.needsUpdate = true;
-
-						hdrJpg.dispose();
-
-					}, function ( progress ) {
-
-						fileSizes[ 'HDR JPG' ] = humanFileSize( progress.total );
-
-					} );
-
-				hdrEquirectangularMap = new RGBELoader()
-					.load( 'textures/equirectangular/spruit_sunrise_1k.hdr', function ( ) {
-
-						resolutions[ 'HDR' ] = hdrEquirectangularMap.image.width + 'x' + hdrEquirectangularMap.image.height;
-
-						hdrPMREMRenderTarget = pmremGenerator.fromEquirectangular( hdrEquirectangularMap );
-
-						hdrEquirectangularMap.mapping = THREE.EquirectangularReflectionMapping;
-						hdrEquirectangularMap.minFilter = THREE.LinearFilter;
-						hdrEquirectangularMap.magFilter = THREE.LinearFilter;
-						hdrEquirectangularMap.needsUpdate = true;
-
-					}, function ( progress ) {
-
-						fileSizes[ 'HDR' ] = humanFileSize( progress.total );
-
-					} );
-
-				renderer.setPixelRatio( window.devicePixelRatio );
-				renderer.setSize( window.innerWidth, window.innerHeight );
-				renderer.setAnimationLoop( animate );
-				container.appendChild( renderer.domElement );
-
-				stats = new Stats();
-				container.appendChild( stats.dom );
-
-				controls = new OrbitControls( camera, renderer.domElement );
-				controls.minDistance = 50;
-				controls.maxDistance = 300;
-
-				window.addEventListener( 'resize', onWindowResize );
-
-				const gui = new GUI();
-
-				gui.add( params, 'envMap', [ 'HDR JPG', 'HDR' ] ).onChange( displayStats );
-				gui.add( params, 'roughness', 0, 1, 0.01 );
-				gui.add( params, 'metalness', 0, 1, 0.01 );
-				gui.add( params, 'exposure', 0, 2, 0.01 );
-				gui.add( params, 'debug' );
-				gui.open();
-
-				function displayStats( value ) {
-
-					lbl.innerHTML = value + ' size : ' + fileSizes[ value ] + ', Resolution: ' + resolutions[ value ];
-
-				}
-
-			}
-
-
-			function humanFileSize( bytes, si = true, dp = 1 ) {
-
-				const thresh = si ? 1000 : 1024;
-
-				if ( Math.abs( bytes ) < thresh ) {
-
-					return bytes + ' B';
-
-				}
-
-				const units = si
-					? [ 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]
-					: [ 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB' ];
-				let u = - 1;
-				const r = 10 ** dp;
-
-				do {
-
-					bytes /= thresh;
-					++ u;
-
-				} while ( Math.round( Math.abs( bytes ) * r ) / r >= thresh && u < units.length - 1 );
-
-				return bytes.toFixed( dp ) + ' ' + units[ u ];
-
-			}
-
-
-			function onWindowResize() {
-
-				const width = window.innerWidth;
-				const height = window.innerHeight;
-
-				camera.aspect = width / height;
-				camera.updateProjectionMatrix();
-
-				renderer.setSize( width, height );
-
-			}
-
-			function animate() {
-			
-				stats.begin();
-				render();
-				stats.end();
-
-			}
-
-			function render() {
-
-				torusMesh.material.roughness = params.roughness;
-				torusMesh.material.metalness = params.metalness;
-
-				let pmremRenderTarget, equirectangularMap;
-
-				switch ( params.envMap ) {
-
-					case 'HDR JPG':
-						pmremRenderTarget = hdrJpgPMREMRenderTarget;
-						equirectangularMap = hdrJpgEquirectangularMap;
-						break;
-					case 'HDR':
-						pmremRenderTarget = hdrPMREMRenderTarget;
-						equirectangularMap = hdrEquirectangularMap;
-						break;
-
-				}
-
-				const newEnvMap = pmremRenderTarget ? pmremRenderTarget.texture : null;
-
-				if ( newEnvMap && newEnvMap !== torusMesh.material.envMap ) {
-
-					planeMesh.material.map = newEnvMap;
-					planeMesh.material.needsUpdate = true;
-
-				}
-
-				torusMesh.rotation.y += 0.005;
-				planeMesh.visible = params.debug;
-
-				scene.environment = equirectangularMap;
-				scene.background = equirectangularMap;
-				renderer.toneMappingExposure = params.exposure;
-
-				renderer.render( scene, camera );
-
-			}
-
-		</script>
-
-	</body>
-</html>

+ 152 - 0
examples/webgl_loader_texture_ultrahdr.html

@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - materials - UltraHDR texture loader</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> - UltraHDR texture loader <br/>
+			Converted from hdr with <a href="https://gainmap-creator.mono-grid.com/" target="_blank" rel="noopener">converter</a>. <br />
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			const params = {
+				autoRotate: true,
+				metalness: 1.0,
+				roughness: 0.0,
+				exposure: 1.0,
+				resolution: '2k',
+				type: 'HalfFloatType'
+			};
+
+			let renderer, scene, camera, controls, torusMesh, loader;
+
+			init();
+
+			function init() {
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				renderer.toneMappingExposure = params.exposure;
+
+				renderer.setAnimationLoop( render );
+
+				scene = new THREE.Scene();
+
+				torusMesh = new THREE.Mesh(
+					new THREE.TorusKnotGeometry( 1, 0.4, 128, 128, 1, 3 ),
+					new THREE.MeshStandardMaterial( { roughness: params.roughness, metalness: params.metalness } )
+				 );
+				scene.add( torusMesh );
+
+				camera = new THREE.PerspectiveCamera(
+					50,
+					window.innerWidth / window.innerHeight,
+					1,
+					500
+				);
+				camera.position.set( 0.0, 0.0, - 6.0 );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+			
+				loader = new UltraHDRLoader();
+				loader.setDataType( THREE.FloatType );
+
+				const loadEnvironment = function ( resolution = '2k', type = 'HalfFloatType' ) {
+
+					loader.setDataType( THREE[ type ] );
+
+					loader.load( `textures/equirectangular/spruit_sunrise_${resolution}.hdr.jpg`, function ( texture ) {
+
+						texture.mapping = THREE.EquirectangularReflectionMapping;
+						texture.needsUpdate = true;
+
+						scene.background = texture;
+						scene.environment = texture;
+
+					} );
+
+				};
+
+				loadEnvironment( params.resolution, params.type );
+
+				const gui = new GUI();
+
+				gui.add( params, 'autoRotate' );
+				gui.add( params, 'metalness', 0, 1, 0.01 );
+				gui.add( params, 'roughness', 0, 1, 0.01 );
+				gui.add( params, 'exposure', 0, 4, 0.01 );
+				gui.add( params, 'resolution', [ '2k', '4k' ] ).onChange( ( value ) => {
+
+					loadEnvironment( value, params.type );
+			
+				} );
+				gui.add( params, 'type', [ 'HalfFloatType', 'FloatType' ] ).onChange( ( value ) => {
+
+					loadEnvironment( params.resolution, value );
+			
+				} );
+
+				gui.open();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function render() {
+
+				torusMesh.material.roughness = params.roughness;
+				torusMesh.material.metalness = params.metalness;
+
+				if ( params.autoRotate ) {
+
+					torusMesh.rotation.y += 0.005;
+
+				}
+
+				renderer.toneMappingExposure = params.exposure;
+
+				controls.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 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 );

+ 7 - 0
src/nodes/materials/MeshBasicNodeMaterial.js

@@ -4,6 +4,7 @@ import { MeshBasicMaterial } from '../../materials/MeshBasicMaterial.js';
 import BasicEnvironmentNode from '../lighting/BasicEnvironmentNode.js';
 import BasicLightMapNode from '../lighting/BasicLightMapNode.js';
 import BasicLightingModel from '../functions/BasicLightingModel.js';
+import { transformedNormalView, normalView } from '../accessors/NormalNode.js';
 
 const defaultValues = new MeshBasicMaterial();
 
@@ -24,6 +25,12 @@ class MeshBasicNodeMaterial extends NodeMaterial {
 
 	}
 
+	setupNormal() {
+
+		transformedNormalView.assign( normalView ); // see #28839
+
+	}
+
 	setupEnvironment( builder ) {
 
 		const envNode = super.setupEnvironment( builder );

+ 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;