Browse Source

WebGPURenderer: PostProcessing + GaussianBlurNode + QuadMesh (#27369)

* WebGPURenderer: PostProcessing + GaussianBlurNode

* update example

* revision

* revision

* Fix RTT & Framebuffer flipY

* Fix multi-scene backgroundNode

* fixes

* new webgpu_portal example

* fix title

* cleanup

* cleanup

* adjustments

* Added QuadMesh

* PostProcessing just for WebGPUBackend for now

* portal update

* error message

* cleanup

* using quad texture

* Fix flip RTT & DepthNode after QuadMesh

* update to QuadMesh

* Update webgpu_depth_texture.jpg

* update `webgpu_instance_uniform` example
sunag 1 year ago
parent
commit
3d65226309

+ 1 - 0
examples/files.json

@@ -349,6 +349,7 @@
 		"webgpu_morphtargets_face",
 		"webgpu_morphtargets_face",
 		"webgpu_occlusion",
 		"webgpu_occlusion",
 		"webgpu_particles",
 		"webgpu_particles",
+		"webgpu_portal",
 		"webgpu_rtt",
 		"webgpu_rtt",
 		"webgpu_sandbox",
 		"webgpu_sandbox",
 		"webgpu_shadertoy",
 		"webgpu_shadertoy",

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

@@ -110,6 +110,8 @@ export { default as ViewportTextureNode, viewportTexture, viewportMipTexture } f
 export { default as ViewportSharedTextureNode, viewportSharedTexture } from './display/ViewportSharedTextureNode.js';
 export { default as ViewportSharedTextureNode, viewportSharedTexture } from './display/ViewportSharedTextureNode.js';
 export { default as ViewportDepthTextureNode, viewportDepthTexture } from './display/ViewportDepthTextureNode.js';
 export { default as ViewportDepthTextureNode, viewportDepthTexture } from './display/ViewportDepthTextureNode.js';
 export { default as ViewportDepthNode, viewZToOrthographicDepth, orthographicDepthToViewZ, viewZToPerspectiveDepth, perspectiveDepthToViewZ, depth, depthTexture, depthPixel } from './display/ViewportDepthNode.js';
 export { default as ViewportDepthNode, viewZToOrthographicDepth, orthographicDepthToViewZ, viewZToPerspectiveDepth, perspectiveDepthToViewZ, depth, depthTexture, depthPixel } from './display/ViewportDepthNode.js';
+export { default as GaussianBlurNode, gaussianBlur } from './display/GaussianBlurNode.js';
+export { default as PassNode, pass, depthPass } from './display/PassNode.js';
 
 
 // code
 // code
 export { default as ExpressionNode, expression } from './code/ExpressionNode.js';
 export { default as ExpressionNode, expression } from './code/ExpressionNode.js';

+ 7 - 2
examples/jsm/nodes/accessors/CubeTextureNode.js

@@ -27,9 +27,14 @@ class CubeTextureNode extends TextureNode {
 
 
 	setUpdateMatrix( /*updateMatrix*/ ) { } // Ignore .updateMatrix for CubeTextureNode
 	setUpdateMatrix( /*updateMatrix*/ ) { } // Ignore .updateMatrix for CubeTextureNode
 
 
-	generateUV( builder, uvNode ) {
+	setupUV( builder, uvNode ) {
+
+		return vec3( uvNode.x.negate(), uvNode.yz );
+
+	}
+
+	generateUV( builder, cubeUV ) {
 
 
-		const cubeUV = vec3( uvNode.x.negate(), uvNode.yz );
 		return cubeUV.build( builder, 'vec3' );
 		return cubeUV.build( builder, 'vec3' );
 
 
 	}
 	}

+ 22 - 15
examples/jsm/nodes/accessors/TextureNode.js

@@ -78,6 +78,20 @@ class TextureNode extends UniformNode {
 
 
 	}
 	}
 
 
+	setupUV( builder, uvNode ) {
+
+		const texture = this.value;
+
+		if ( builder.isFlipY() && ( texture.isRenderTargetTexture === true || texture.isFramebufferTexture === true || texture.isDepthTexture === true ) ) {
+
+			uvNode = uvNode.setY( uvNode.y.oneMinus() );
+
+		}
+
+		return uvNode;
+
+	}
+
 	setup( builder ) {
 	setup( builder ) {
 
 
 		const properties = builder.getNodeProperties( this );
 		const properties = builder.getNodeProperties( this );
@@ -100,6 +114,8 @@ class TextureNode extends UniformNode {
 
 
 		}
 		}
 
 
+		uvNode = this.setupUV( builder, uvNode );
+
 		//
 		//
 
 
 		let levelNode = this.levelNode;
 		let levelNode = this.levelNode;
@@ -125,6 +141,12 @@ class TextureNode extends UniformNode {
 
 
 	}
 	}
 
 
+	generateUV( builder, uvNode ) {
+
+		return uvNode.build( builder, this.sampler === true ? 'vec2' : 'ivec2' );
+
+	}
+
 	generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, depthSnippet, compareSnippet ) {
 	generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, depthSnippet, compareSnippet ) {
 
 
 		const texture = this.value;
 		const texture = this.value;
@@ -153,21 +175,6 @@ class TextureNode extends UniformNode {
 
 
 	}
 	}
 
 
-	generateUV( builder, uvNode ) {
-
-		const texture = this.value;
-
-		if ( ( builder.isFlipY() && ( texture.isFramebufferTexture === true || texture.isDepthTexture === true ) ) ||
-			( builder.isFlipY() === false && texture.isRenderTargetTexture === true ) ) {
-
-			uvNode = uvNode.setY( uvNode.y.fract().oneMinus() );
-
-		}
-
-		return uvNode.build( builder, this.sampler === true ? 'vec2' : 'ivec2' );
-
-	}
-
 	generate( builder, output ) {
 	generate( builder, output ) {
 
 
 		const properties = builder.getNodeProperties( this );
 		const properties = builder.getNodeProperties( this );

+ 6 - 0
examples/jsm/nodes/core/AttributeNode.js

@@ -12,6 +12,12 @@ class AttributeNode extends Node {
 
 
 	}
 	}
 
 
+	isGlobal() {
+
+		return true;
+
+	}
+
 	getHash( builder ) {
 	getHash( builder ) {
 
 
 		return this.getAttributeName( builder );
 		return this.getAttributeName( builder );

+ 4 - 1
examples/jsm/nodes/core/CacheNode.js

@@ -24,8 +24,9 @@ class CacheNode extends Node {
 	build( builder, ...params ) {
 	build( builder, ...params ) {
 
 
 		const previousCache = builder.getCache();
 		const previousCache = builder.getCache();
+		const cache = this.cache || builder.globalCache;
 
 
-		builder.setCache( this.cache );
+		builder.setCache( cache );
 
 
 		const data = this.node.build( builder, ...params );
 		const data = this.node.build( builder, ...params );
 
 
@@ -40,7 +41,9 @@ class CacheNode extends Node {
 export default CacheNode;
 export default CacheNode;
 
 
 export const cache = nodeProxy( CacheNode );
 export const cache = nodeProxy( CacheNode );
+export const globalCache = ( node ) => cache( node, null );
 
 
 addNodeElement( 'cache', cache );
 addNodeElement( 'cache', cache );
+addNodeElement( 'globalCache', globalCache );
 
 
 addNodeClass( 'CacheNode', CacheNode );
 addNodeClass( 'CacheNode', CacheNode );

+ 3 - 3
examples/jsm/nodes/core/NodeBuilder.js

@@ -626,9 +626,9 @@ class NodeBuilder {
 
 
 	}
 	}
 
 
-	getDataFromNode( node, shaderStage = this.shaderStage ) {
+	getDataFromNode( node, shaderStage = this.shaderStage, cache = null ) {
 
 
-		const cache = node.isGlobal( this ) ? this.globalCache : this.cache;
+		cache = cache === null ? ( node.isGlobal( this ) ? this.globalCache : this.cache ) : cache;
 
 
 		let nodeData = cache.getNodeData( node );
 		let nodeData = cache.getNodeData( node );
 
 
@@ -697,7 +697,7 @@ class NodeBuilder {
 
 
 	getUniformFromNode( node, type, shaderStage = this.shaderStage, name = null ) {
 	getUniformFromNode( node, type, shaderStage = this.shaderStage, name = null ) {
 
 
-		const nodeData = this.getDataFromNode( node, shaderStage );
+		const nodeData = this.getDataFromNode( node, shaderStage, this.globalCache );
 
 
 		let nodeUniform = nodeData.uniform;
 		let nodeUniform = nodeData.uniform;
 
 

+ 2 - 2
examples/jsm/nodes/display/ColorAdjustmentNode.js

@@ -6,7 +6,7 @@ import { addNodeElement, tslFn, nodeProxy, float, vec3 } from '../shadernode/Sha
 
 
 const saturationNode = tslFn( ( { color, adjustment } ) => {
 const saturationNode = tslFn( ( { color, adjustment } ) => {
 
 
-	return adjustment.mix( luminance( color ), color );
+	return adjustment.mix( luminance( color.rgb ), color.rgb );
 
 
 } );
 } );
 
 
@@ -17,7 +17,7 @@ const vibranceNode = tslFn( ( { color, adjustment } ) => {
 	const mx = color.r.max( color.g.max( color.b ) );
 	const mx = color.r.max( color.g.max( color.b ) );
 	const amt = mx.sub( average ).mul( adjustment ).mul( - 3.0 );
 	const amt = mx.sub( average ).mul( adjustment ).mul( - 3.0 );
 
 
-	return mix( color, mx, amt );
+	return mix( color.rgb, mx, amt );
 
 
 } );
 } );
 
 

+ 165 - 0
examples/jsm/nodes/display/GaussianBlurNode.js

@@ -0,0 +1,165 @@
+import TempNode from '../core/TempNode.js';
+import { nodeObject, addNodeElement, tslFn, float, vec2, vec3, vec4 } from '../shadernode/ShaderNode.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { mul } from '../math/OperatorNode.js';
+import { uv } from '../accessors/UVNode.js';
+import { texture } from '../accessors/TextureNode.js';
+import { uniform } from '../core/UniformNode.js';
+import { Vector2, RenderTarget } from 'three';
+import QuadMesh from '../../objects/QuadMesh.js';
+
+const quadMesh = new QuadMesh();
+
+class GaussianBlurNode extends TempNode {
+
+	constructor( textureNode, sigma = 2 ) {
+
+		super( textureNode );
+
+		this.textureNode = textureNode;
+		this.sigma = sigma;
+
+		this.directionNode = vec2( 1 );
+
+		this._invSize = uniform( new Vector2() );
+		this._passDirection = uniform( new Vector2() );
+
+		this._horizontalRT = new RenderTarget();
+		this._verticalRT = new RenderTarget();
+
+		this.updateBeforeType = NodeUpdateType.RENDER;
+
+	}
+
+	setSize( width, height ) {
+
+		this._invSize.value.set( 1 / width, 1 / height );
+		this._horizontalRT.setSize( width, height );
+		this._verticalRT.setSize( width, height );
+
+	}
+
+	updateBefore( frame ) {
+
+		const { renderer } = frame;
+
+		const textureNode = this.textureNode;
+		const map = textureNode.value;
+
+		const currentRenderTarget = renderer.getRenderTarget();
+		const currentTexture = textureNode.value;
+
+		quadMesh.material = this._material;
+
+		this.setSize( map.image.width, map.image.height );
+
+		// horizontal
+
+		renderer.setRenderTarget( this._horizontalRT );
+
+		this._passDirection.value.set( 1, 0 );
+
+		quadMesh.render( renderer );
+
+		// vertical
+
+		textureNode.value = this._horizontalRT.texture;
+		renderer.setRenderTarget( this._verticalRT );
+
+		this._passDirection.value.set( 0, 1 );
+
+		quadMesh.render( renderer );
+
+		// restore
+
+		renderer.setRenderTarget( currentRenderTarget );
+		textureNode.value = currentTexture;
+
+	}
+
+	setup( builder ) {
+
+		const textureNode = this.textureNode;
+
+		if ( textureNode.isTextureNode !== true ) {
+
+			console.error( 'GaussianBlurNode requires a TextureNode.' );
+
+			return vec4();
+
+		}
+
+		//
+
+		const uvNode = textureNode.uvNode || uv();
+
+		const sampleTexture = ( uv ) => textureNode.cache().context( { getUV: () => uv, forceUVContext: true } );
+
+		const blur = tslFn( () => {
+
+			const kernelSize = 3 + ( 2 * this.sigma );
+			const gaussianCoefficients = this._getCoefficients( kernelSize );
+
+			const invSize = this._invSize;
+			const direction = vec2( this.directionNode ).mul( this._passDirection );
+
+			const weightSum = float( gaussianCoefficients[ 0 ] ).toVar();
+			const diffuseSum = vec3( sampleTexture( uvNode ).mul( weightSum ) ).toVar();
+
+			for ( let i = 1; i < kernelSize; i ++ ) {
+
+				const x = float( i );
+				const w = float( gaussianCoefficients[ i ] );
+
+				const uvOffset = vec2( direction.mul( invSize.mul( x ) ) ).toVar();
+
+				const sample1 = vec3( sampleTexture( uvNode.add( uvOffset ) ) );
+				const sample2 = vec3( sampleTexture( uvNode.sub( uvOffset ) ) );
+
+				diffuseSum.addAssign( sample1.add( sample2 ).mul( w ) );
+				weightSum.addAssign( mul( 2.0, w ) );
+
+			}
+
+			return vec4( diffuseSum.div( weightSum ), 1.0 );
+
+		} );
+
+		//
+
+		const material = this._material || ( this._material = builder.createNodeMaterial( 'MeshBasicNodeMaterial' ) );
+		material.fragmentNode = blur();
+
+		//
+
+		const properties = builder.getNodeProperties( this );
+		properties.textureNode = textureNode;
+
+		//
+
+		return texture( this._verticalRT.texture );
+
+	}
+
+	_getCoefficients( kernelRadius ) {
+
+		const coefficients = [];
+
+		for ( let i = 0; i < kernelRadius; i ++ ) {
+
+			coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );
+
+		}
+
+		return coefficients;
+
+	}
+
+}
+
+export const gaussianBlur = ( node, sigma ) => nodeObject( new GaussianBlurNode( nodeObject( node ), sigma ) );
+
+addNodeElement( 'gaussianBlur', gaussianBlur );
+
+export default GaussianBlurNode;
+

+ 182 - 0
examples/jsm/nodes/display/PassNode.js

@@ -0,0 +1,182 @@
+import { addNodeClass } from '../core/Node.js';
+import TempNode from '../core/TempNode.js';
+import TextureNode from '../accessors/TextureNode.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { nodeObject } from '../shadernode/ShaderNode.js';
+import { uniform } from '../core/UniformNode.js';
+import { viewZToOrthographicDepth, perspectiveDepthToViewZ } from './ViewportDepthNode.js';
+import { RenderTarget, Vector2, HalfFloatType, DepthTexture, FloatType, NoToneMapping } from 'three';
+
+class PassTextureNode extends TextureNode {
+
+	constructor( passNode, texture ) {
+
+		super( texture );
+
+		this.passNode = passNode;
+
+		this.setUpdateMatrix( false );
+
+	}
+
+	setup( builder ) {
+
+		this.passNode.build( builder );
+
+		return super.setup( builder );
+
+	}
+
+	clone() {
+
+		return new this.constructor( this.passNode, this.value );
+
+	}
+
+}
+
+class PassNode extends TempNode {
+
+	constructor( scope, scene, camera ) {
+
+		super( 'vec4' );
+
+		this.scope = scope;
+		this.scene = scene;
+		this.camera = camera;
+
+		this._pixelRatio = 1;
+		this._width = 1;
+		this._height = 1;
+
+		const depthTexture = new DepthTexture();
+		depthTexture.isRenderTargetTexture = true;
+		depthTexture.type = FloatType;
+		depthTexture.name = 'PostProcessingDepth';
+
+		const renderTarget = new RenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );
+		renderTarget.texture.name = 'PostProcessing';
+		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._depthNode = null;
+		this._cameraNear = uniform( 0 );
+		this._cameraFar = uniform( 0 );
+
+		this.isPassNode = true;
+
+	}
+
+	isGlobal() {
+
+		return true;
+
+	}
+
+	getTextureNode() {
+
+		return this._textureNode;
+
+	}
+
+	getTextureDepthNode() {
+
+		return this._depthTextureNode;
+
+	}
+
+	getDepthNode() {
+
+		if ( this._depthNode === null ) {
+
+			const cameraNear = this._cameraNear;
+			const cameraFar = this._cameraFar;
+
+			this._depthNode = viewZToOrthographicDepth( perspectiveDepthToViewZ( this._depthTextureNode, cameraNear, cameraFar ), cameraNear, cameraFar );
+
+		}
+
+		return this._depthNode;
+
+	}
+
+	setup() {
+
+		return this.scope === PassNode.COLOR ? this.getTextureNode() : this.getDepthNode();
+
+	}
+
+	updateBefore( frame ) {
+
+		const { renderer } = frame;
+		const { scene, camera } = this;
+
+		this._pixelRatio = renderer.getPixelRatio();
+
+		const size = renderer.getSize( new Vector2() );
+
+		this.setSize( size.width, size.height );
+
+		const currentToneMapping = renderer.toneMapping;
+		const currentToneMappingNode = renderer.toneMappingNode;
+		const currentRenderTarget = renderer.getRenderTarget();
+
+		this._cameraNear.value = camera.near;
+		this._cameraFar.value = camera.far;
+
+		renderer.toneMapping = NoToneMapping;
+		renderer.toneMappingNode = null;
+		renderer.setRenderTarget( this.renderTarget );
+
+		renderer.render( scene, camera );
+
+		renderer.toneMapping = currentToneMapping;
+		renderer.toneMappingNode = currentToneMappingNode;
+		renderer.setRenderTarget( currentRenderTarget );
+
+	}
+
+	setSize( width, height ) {
+
+		this._width = width;
+		this._height = height;
+
+		const effectiveWidth = this._width * this._pixelRatio;
+		const effectiveHeight = this._height * this._pixelRatio;
+
+		this.renderTarget.setSize( effectiveWidth, effectiveHeight );
+
+	}
+
+	setPixelRatio( pixelRatio ) {
+
+		this._pixelRatio = pixelRatio;
+
+		this.setSize( this._width, this._height );
+
+	}
+
+	dispose() {
+
+		this.renderTarget.dispose();
+
+	}
+
+
+}
+
+PassNode.COLOR = 'color';
+PassNode.DEPTH = 'depth';
+
+export default PassNode;
+
+export const pass = ( scene, camera ) => nodeObject( new PassNode( PassNode.COLOR, scene, camera ) );
+export const depthPass = ( scene, camera ) => nodeObject( new PassNode( PassNode.DEPTH, scene, camera ) );
+
+addNodeClass( 'PassNode', PassNode );

+ 2 - 2
examples/jsm/nodes/utils/RemapNode.js

@@ -1,9 +1,9 @@
 import Node, { addNodeClass } from '../core/Node.js';
 import Node, { addNodeClass } from '../core/Node.js';
-import { addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';
+import { float, addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';
 
 
 class RemapNode extends Node {
 class RemapNode extends Node {
 
 
-	constructor( node, inLowNode, inHighNode, outLowNode, outHighNode ) {
+	constructor( node, inLowNode, inHighNode, outLowNode = float( 0 ), outHighNode = float( 1 ) ) {
 
 
 		super();
 		super();
 
 

+ 60 - 0
examples/jsm/objects/QuadMesh.js

@@ -0,0 +1,60 @@
+import { BufferGeometry, Float32BufferAttribute, Mesh, OrthographicCamera } from 'three';
+
+// Helper for passes that need to fill the viewport with a single quad.
+
+const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+
+// https://github.com/mrdoob/three.js/pull/21358
+
+class QuadGeometry extends BufferGeometry {
+
+	constructor( flipY = false ) {
+
+		super();
+
+		const uv = flipY === false ? [ 0, - 1, 0, 1, 2, 1 ] : [ 0, 2, 0, 0, 2, 0 ];
+
+		this.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
+		this.setAttribute( 'uv', new Float32BufferAttribute( uv, 2 ) );
+
+	}
+
+}
+
+const _geometry = new QuadGeometry();
+
+class QuadMesh {
+
+	constructor( material = null ) {
+
+		this._mesh = new Mesh( _geometry, material );
+
+	}
+
+	dispose() {
+
+		this._mesh.geometry.dispose();
+
+	}
+
+	render( renderer ) {
+
+		renderer.render( this._mesh, _camera );
+
+	}
+
+	get material() {
+
+		return this._mesh.material;
+
+	}
+
+	set material( value ) {
+
+		this._mesh.material = value;
+
+	}
+
+}
+
+export default QuadMesh;

+ 8 - 10
examples/jsm/renderers/common/Background.js

@@ -1,7 +1,7 @@
 import DataMap from './DataMap.js';
 import DataMap from './DataMap.js';
 import Color4 from './Color4.js';
 import Color4 from './Color4.js';
 import { Mesh, SphereGeometry, BackSide } from 'three';
 import { Mesh, SphereGeometry, BackSide } from 'three';
-import { context, normalWorld, backgroundBlurriness, backgroundIntensity, NodeMaterial, modelViewProjection } from '../../nodes/Nodes.js';
+import { vec4, context, normalWorld, backgroundBlurriness, backgroundIntensity, NodeMaterial, modelViewProjection } from '../../nodes/Nodes.js';
 
 
 const _clearColor = new Color4();
 const _clearColor = new Color4();
 
 
@@ -14,9 +14,6 @@ class Background extends DataMap {
 		this.renderer = renderer;
 		this.renderer = renderer;
 		this.nodes = nodes;
 		this.nodes = nodes;
 
 
-		this.backgroundMesh = null;
-		this.backgroundMeshNode = null;
-
 	}
 	}
 
 
 	update( scene, renderList, renderContext ) {
 	update( scene, renderList, renderContext ) {
@@ -49,11 +46,11 @@ class Background extends DataMap {
 
 
 			_clearColor.copy( renderer._clearColor );
 			_clearColor.copy( renderer._clearColor );
 
 
-			let backgroundMesh = this.backgroundMesh;
+			let backgroundMesh = sceneData.backgroundMesh;
 
 
-			if ( backgroundMesh === null ) {
+			if ( backgroundMesh === undefined ) {
 
 
-				this.backgroundMeshNode = context( backgroundNode, {
+				const backgroundMeshNode = context( vec4( backgroundNode ), {
 					// @TODO: Add Texture2D support using node context
 					// @TODO: Add Texture2D support using node context
 					getUV: () => normalWorld,
 					getUV: () => normalWorld,
 					getTextureLevel: () => backgroundBlurriness
 					getTextureLevel: () => backgroundBlurriness
@@ -68,9 +65,10 @@ class Background extends DataMap {
 				nodeMaterial.depthWrite = false;
 				nodeMaterial.depthWrite = false;
 				nodeMaterial.fog = false;
 				nodeMaterial.fog = false;
 				nodeMaterial.vertexNode = viewProj;
 				nodeMaterial.vertexNode = viewProj;
-				nodeMaterial.fragmentNode = this.backgroundMeshNode;
+				nodeMaterial.fragmentNode = backgroundMeshNode;
 
 
-				this.backgroundMesh = backgroundMesh = new Mesh( new SphereGeometry( 1, 32, 32 ), nodeMaterial );
+				sceneData.backgroundMeshNode = backgroundMeshNode;
+				sceneData.backgroundMesh = backgroundMesh = new Mesh( new SphereGeometry( 1, 32, 32 ), nodeMaterial );
 				backgroundMesh.frustumCulled = false;
 				backgroundMesh.frustumCulled = false;
 
 
 				backgroundMesh.onBeforeRender = function ( renderer, scene, camera ) {
 				backgroundMesh.onBeforeRender = function ( renderer, scene, camera ) {
@@ -85,7 +83,7 @@ class Background extends DataMap {
 
 
 			if ( sceneData.backgroundCacheKey !== backgroundCacheKey ) {
 			if ( sceneData.backgroundCacheKey !== backgroundCacheKey ) {
 
 
-				this.backgroundMeshNode.node = backgroundNode;
+				sceneData.backgroundMeshNode.node = vec4( backgroundNode );
 
 
 				backgroundMesh.material.needsUpdate = true;
 				backgroundMesh.material.needsUpdate = true;
 
 

+ 25 - 0
examples/jsm/renderers/common/PostProcessing.js

@@ -0,0 +1,25 @@
+import { vec4, MeshBasicNodeMaterial } from '../../nodes/Nodes.js';
+import QuadMesh from '../../objects/QuadMesh.js';
+
+const quadMesh = new QuadMesh( new MeshBasicNodeMaterial() );
+
+class PostProcessing {
+
+	constructor( renderer, outputNode = vec4( 0, 0, 1, 1 ) ) {
+
+		this.renderer = renderer;
+		this.outputNode = outputNode;
+
+	}
+
+	render() {
+
+		quadMesh.material.fragmentNode = this.outputNode;
+
+		quadMesh.render( this.renderer );
+
+	}
+
+}
+
+export default PostProcessing;

+ 9 - 8
examples/jsm/renderers/webgl/WebGLBackend.js

@@ -459,12 +459,6 @@ class WebGLBackend extends Backend {
 
 
 	}
 	}
 
 
-	destroySampler( /*texture*/ ) {
-
-		console.warn( 'Abstract class.' );
-
-	}
-
 	createDefaultTexture( texture ) {
 	createDefaultTexture( texture ) {
 
 
 		const { gl, textureUtils, defaultTextures } = this;
 		const { gl, textureUtils, defaultTextures } = this;
@@ -607,12 +601,19 @@ class WebGLBackend extends Backend {
 
 
 	}
 	}
 
 
-	destroyTexture( /*texture*/ ) {
+	destroyTexture( texture ) {
 
 
-		console.warn( 'Abstract class.' );
+		const { gl } = this;
+		const { textureGPU } = this.get( texture );
+
+		gl.deleteTexture( textureGPU );
+
+		this.delete( texture );
 
 
 	}
 	}
 
 
+	destroySampler() {}
+
 	copyTextureToBuffer( texture, x, y, width, height ) {
 	copyTextureToBuffer( texture, x, y, width, height ) {
 
 
 		return this.textureUtils.copyTextureToBuffer( texture, x, y, width, height );
 		return this.textureUtils.copyTextureToBuffer( texture, x, y, width, height );

+ 1 - 1
examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js

@@ -583,7 +583,7 @@ void main() {
 	getUniformFromNode( node, type, shaderStage, name = null ) {
 	getUniformFromNode( node, type, shaderStage, name = null ) {
 
 
 		const uniformNode = super.getUniformFromNode( node, type, shaderStage, name );
 		const uniformNode = super.getUniformFromNode( node, type, shaderStage, name );
-		const nodeData = this.getDataFromNode( node, shaderStage );
+		const nodeData = this.getDataFromNode( node, shaderStage, this.globalCache );
 
 
 		let uniformGPU = nodeData.uniformGPU;
 		let uniformGPU = nodeData.uniformGPU;
 
 

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

@@ -342,7 +342,7 @@ class WGSLNodeBuilder extends NodeBuilder {
 	getUniformFromNode( node, type, shaderStage, name = null ) {
 	getUniformFromNode( node, type, shaderStage, name = null ) {
 
 
 		const uniformNode = super.getUniformFromNode( node, type, shaderStage, name );
 		const uniformNode = super.getUniformFromNode( node, type, shaderStage, name );
-		const nodeData = this.getDataFromNode( node, shaderStage );
+		const nodeData = this.getDataFromNode( node, shaderStage, this.globalCache );
 
 
 		if ( nodeData.uniformGPU === undefined ) {
 		if ( nodeData.uniformGPU === undefined ) {
 
 

BIN
examples/screenshots/webgpu_depth_texture.jpg


BIN
examples/screenshots/webgpu_instance_uniform.jpg


BIN
examples/screenshots/webgpu_portal.jpg


BIN
examples/screenshots/webgpu_skinning_instancing.jpg


+ 7 - 4
examples/webgpu_compute_particles_rain.html

@@ -46,7 +46,7 @@
 			let clock;
 			let clock;
 
 
 			let collisionBox, collisionCamera, collisionPosRT, collisionPosMaterial;
 			let collisionBox, collisionCamera, collisionPosRT, collisionPosMaterial;
-			let collisionBoxPos;
+			let collisionBoxPos, collisionBoxPosUI;
 
 
 			init();
 			init();
 
 
@@ -138,7 +138,7 @@
 
 
 				const computeUpdate = tslFn( () => {
 				const computeUpdate = tslFn( () => {
 
 
-					const getCoord = ( pos ) => pos.add( 50 ).div( 100 ).mul( vec2( 1, - 1 ) );
+					const getCoord = ( pos ) => pos.add( 50 ).div( 100 );
 
 
 					const position = positionBuffer.element( instanceIndex );
 					const position = positionBuffer.element( instanceIndex );
 					const velocity = velocityBuffer.element( instanceIndex );
 					const velocity = velocityBuffer.element( instanceIndex );
@@ -346,9 +346,10 @@
 				const gui = new GUI();
 				const gui = new GUI();
 
 
 				// use lerp to smooth the movement
 				// use lerp to smooth the movement
-				collisionBoxPos = new THREE.Vector3().copy( collisionBox.position );
+				collisionBoxPosUI = new THREE.Vector3().copy( collisionBox.position );
+				collisionBoxPos = new THREE.Vector3();
 
 
-				gui.add( collisionBoxPos, 'z', - 50, 50, .001 ).name( 'position' );
+				gui.add( collisionBoxPosUI, 'z', - 50, 50, .001 ).name( 'position' );
 				gui.add( collisionBox.scale, 'x', .1, 3.5, 0.01 ).name( 'scale' );
 				gui.add( collisionBox.scale, 'x', .1, 3.5, 0.01 ).name( 'scale' );
 				gui.add( rainParticles, 'count', 200, maxParticleCount, 1 ).name( 'drop count' ).onChange( ( v ) => rippleParticles.count = v );
 				gui.add( rainParticles, 'count', 200, maxParticleCount, 1 ).name( 'drop count' ).onChange( ( v ) => rippleParticles.count = v );
 
 
@@ -377,6 +378,8 @@
 
 
 				}
 				}
 
 
+				collisionBoxPos.set( collisionBoxPosUI.x, collisionBoxPosUI.y, - collisionBoxPosUI.z );
+
 				collisionBox.position.lerp( collisionBoxPos, 10 * delta );
 				collisionBox.position.lerp( collisionBoxPos, 10 * delta );
 
 
 				// position
 				// position

+ 5 - 11
examples/webgpu_depth_texture.html

@@ -31,11 +31,13 @@
 
 
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
 
 
+			import QuadMesh from 'three/addons/objects/QuadMesh.js';
+
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 			let camera, scene, controls, renderer;
 			let camera, scene, controls, renderer;
 
 
-			let cameraFX, sceneFX, renderTarget;
+			let quad, renderTarget;
 
 
 			const dpr = window.devicePixelRatio;
 			const dpr = window.devicePixelRatio;
 
 
@@ -100,18 +102,10 @@
 
 
 				// FX
 				// FX
 
 
-				cameraFX = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-				sceneFX = new THREE.Scene();
-
-				const geometryFX = new THREE.PlaneGeometry( 2, 2 );
-
-				//
-
 				const materialFX = new MeshBasicNodeMaterial();
 				const materialFX = new MeshBasicNodeMaterial();
 				materialFX.colorNode = texture( depthTexture );
 				materialFX.colorNode = texture( depthTexture );
 
 
-				const quad = new THREE.Mesh( geometryFX, materialFX );
-				sceneFX.add( quad );
+				quad = new QuadMesh( materialFX );
 
 
 				//
 				//
 
 
@@ -136,7 +130,7 @@
 				renderer.render( scene, camera );
 				renderer.render( scene, camera );
 
 
 				renderer.setRenderTarget( null );
 				renderer.setRenderTarget( null );
-				renderer.render( sceneFX, cameraFX );
+				quad.render( renderer );
 
 
 			}
 			}
 
 

+ 0 - 1
examples/webgpu_instance_uniform.html

@@ -98,7 +98,6 @@
 				// Grid
 				// Grid
 
 
 				const helper = new THREE.GridHelper( 1000, 40, 0x303030, 0x303030 );
 				const helper = new THREE.GridHelper( 1000, 40, 0x303030, 0x303030 );
-				helper.material.colorNode = attribute( 'color' );
 				helper.position.y = - 75;
 				helper.position.y = - 75;
 				scene.add( helper );
 				scene.add( helper );
 
 

+ 5 - 10
examples/webgpu_multiple_rendertargets.html

@@ -34,9 +34,10 @@
 
 
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
 
 
+			import QuadMesh from 'three/addons/objects/QuadMesh.js';
+
 			let camera, scene, renderer, torus;
 			let camera, scene, renderer, torus;
-			let renderTarget;
-			let postScene, postCamera;
+			let quadMesh, renderTarget;
 
 
 			/*
 			/*
 
 
@@ -156,13 +157,7 @@
 
 
 				// PostProcessing setup
 				// PostProcessing setup
 
 
-				postScene = new THREE.Scene();
-				postCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-
-				postScene.add( new THREE.Mesh(
-					new THREE.PlaneGeometry( 2, 2 ),
-					new ReadGBufferMaterial( renderTarget.texture[ 0 ], renderTarget.texture[ 1 ] )
-				) );
+				quadMesh = new QuadMesh( new ReadGBufferMaterial( renderTarget.texture[ 0 ], renderTarget.texture[ 1 ] ) );
 
 
 				// Controls
 				// Controls
 
 
@@ -212,7 +207,7 @@
 
 
 				// render post FX
 				// render post FX
 				renderer.setRenderTarget( null );
 				renderer.setRenderTarget( null );
-				renderer.render( postScene, postCamera );
+				quadMesh.render( renderer );
 
 
 			}
 			}
 
 

+ 196 - 0
examples/webgpu_portal.html

@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js - WebGPU - Portal</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 - Portal
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { pass, color, mx_worley_noise_float, timerLocal, viewportTopLeft, vec2, uv, normalWorld, mx_fractal_noise_vec3, toneMapping, MeshBasicNodeMaterial } from 'three/nodes';
+
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			let camera, sceneMain, scenePortal, renderer;
+			let clock;
+
+			const mixers = [];
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				//
+
+				sceneMain = new THREE.Scene();
+				sceneMain.background = new THREE.Color( 0x222222 );
+				sceneMain.backgroundNode = normalWorld.y.mix( color( 0x0066ff ), color( 0xff0066 ) );
+
+				scenePortal = new THREE.Scene();
+				scenePortal.backgroundNode = mx_worley_noise_float( normalWorld.mul( 20 ).add( vec2( 0, timerLocal().oneMinus() ) ) ).mul( color( 0x0066ff ) );
+
+				//
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.01, 30 );
+				camera.position.set( 2.5, 1, 3 );
+				camera.position.multiplyScalar( .8 );
+				camera.lookAt( 0, 1, 0 );
+
+				clock = new THREE.Clock();
+
+				// lights
+
+				const light = new THREE.PointLight( 0xffffff, 1 );
+				light.position.set( 0, 1, 5 );
+				light.power = 17000;
+
+				sceneMain.add( new THREE.HemisphereLight( 0xff0066, 0x0066ff, 7 ) );
+				sceneMain.add( light );
+				scenePortal.add( light.clone() );
+
+				// models
+
+				const loader = new GLTFLoader();
+				loader.load( 'models/gltf/Xbot.glb', function ( gltf ) {
+
+					const createModel = ( colorNode = null ) => {
+
+						let object;
+			
+						if ( mixers.length === 0 ) {
+
+							object = gltf.scene;
+
+						} else {
+
+							object = gltf.scene.clone();
+
+							const children = object.children[ 0 ].children;
+
+							const applyFX = ( index ) => {
+
+								children[ index ].material = children[ index ].material.clone();
+								children[ index ].material.colorNode = colorNode;
+								children[ index ].material.wireframe = true;
+
+							};
+
+							applyFX( 0 );
+							applyFX( 1 );
+
+						}
+
+						const mixer = new THREE.AnimationMixer( object );
+
+						const action = mixer.clipAction( gltf.animations[ 6 ] );
+						action.play();
+
+						mixers.push( mixer );
+
+						return object;
+
+					};
+
+					const colorNode = mx_fractal_noise_vec3( uv().mul( 20 ).add( timerLocal() ) );
+
+					const modelMain = createModel();
+					const modelPortal = createModel( colorNode );
+
+					//
+
+					sceneMain.add( modelMain );
+					scenePortal.add( modelPortal );
+
+				} );
+
+				//
+
+				const geometry = new THREE.PlaneGeometry( 1.7, 2 );
+				const material = new MeshBasicNodeMaterial();
+				material.colorNode = pass( scenePortal, camera ).context( { getUV: () => viewportTopLeft } );
+				material.opacityNode = uv().distance( .5 ).remapClamp( .3, .5 ).oneMinus();
+				material.side = THREE.DoubleSide;
+				material.transparent = true;
+
+				const plane = new THREE.Mesh( geometry, material );
+				plane.position.set( 0, 1, .8 );
+				sceneMain.add( plane );
+
+				// renderer
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.toneMappingNode = toneMapping( THREE.LinearToneMapping, .15 );
+				document.body.appendChild( renderer.domElement );
+
+				//
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 1, 0 );
+				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();
+
+				for ( const mixer of mixers ) {
+
+					mixer.update( delta );
+
+				}
+
+				renderer.render( sceneMain, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 5 - 9
examples/webgpu_rtt.html

@@ -31,10 +31,12 @@
 
 
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
 
 
+			import QuadMesh from 'three/addons/objects/QuadMesh.js';
+
 			let camera, scene, renderer;
 			let camera, scene, renderer;
 			const mouse = new THREE.Vector2();
 			const mouse = new THREE.Vector2();
 
 
-			let cameraFX, sceneFX, renderTarget;
+			let quadMesh, renderTarget;
 
 
 			let box;
 			let box;
 
 
@@ -87,11 +89,6 @@
 
 
 				// FX
 				// FX
 
 
-				cameraFX = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-				sceneFX = new THREE.Scene();
-
-				const geometryFX = new THREE.PlaneGeometry( 2, 2 );
-
 				// modulate the final color based on the mouse position
 				// modulate the final color based on the mouse position
 
 
 				const screenFXNode = uniform( mouse );
 				const screenFXNode = uniform( mouse );
@@ -99,8 +96,7 @@
 				const materialFX = new MeshBasicNodeMaterial();
 				const materialFX = new MeshBasicNodeMaterial();
 				materialFX.colorNode = texture( renderTarget.texture ).rgb.saturation( screenFXNode.x.oneMinus() ).hue( screenFXNode.y );
 				materialFX.colorNode = texture( renderTarget.texture ).rgb.saturation( screenFXNode.x.oneMinus() ).hue( screenFXNode.y );
 
 
-				const quad = new THREE.Mesh( geometryFX, materialFX );
-				sceneFX.add( quad );
+				quadMesh = new QuadMesh( materialFX );
 
 
 			}
 			}
 
 
@@ -130,7 +126,7 @@
 				renderer.render( scene, camera );
 				renderer.render( scene, camera );
 
 
 				renderer.setRenderTarget( null );
 				renderer.setRenderTarget( null );
-				renderer.render( sceneFX, cameraFX );
+				quadMesh.render( renderer );
 
 
 			}
 			}
 
 

+ 41 - 8
examples/webgpu_skinning_instancing.html

@@ -25,7 +25,7 @@
 		<script type="module">
 		<script type="module">
 
 
 			import * as THREE from 'three';
 			import * as THREE from 'three';
-			import { mix, range, color, oscSine, timerLocal, toneMapping, MeshStandardNodeMaterial } from 'three/nodes';
+			import { pass, mix, range, color, oscSine, timerLocal, MeshStandardNodeMaterial } from 'three/nodes';
 
 
 			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
@@ -33,8 +33,10 @@
 			import WebGL from 'three/addons/capabilities/WebGL.js';
 			import WebGL from 'three/addons/capabilities/WebGL.js';
 
 
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+			import PostProcessing from 'three/addons/renderers/common/PostProcessing.js';
 
 
 			let camera, scene, renderer;
 			let camera, scene, renderer;
+			let postProcessing;
 
 
 			let mixer, clock;
 			let mixer, clock;
 
 
@@ -50,7 +52,7 @@
 
 
 				}
 				}
 
 
-				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.01, 100 );
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.01, 40 );
 				camera.position.set( 1, 2, 3 );
 				camera.position.set( 1, 2, 3 );
 
 
 				scene = new THREE.Scene();
 				scene = new THREE.Scene();
@@ -58,19 +60,25 @@
 
 
 				clock = new THREE.Clock();
 				clock = new THREE.Clock();
 
 
-				//lights
+				// lights
 
 
 				const centerLight = new THREE.PointLight( 0xff9900, 1, 100 );
 				const centerLight = new THREE.PointLight( 0xff9900, 1, 100 );
 				centerLight.position.y = 4.5;
 				centerLight.position.y = 4.5;
 				centerLight.position.z = - 2;
 				centerLight.position.z = - 2;
-				centerLight.power = 1700;
+				centerLight.power = 400;
 				scene.add( centerLight );
 				scene.add( centerLight );
 
 
 				const cameraLight = new THREE.PointLight( 0x0099ff, 1, 100 );
 				const cameraLight = new THREE.PointLight( 0x0099ff, 1, 100 );
-				cameraLight.power = 1700;
+				cameraLight.power = 400;
 				camera.add( cameraLight );
 				camera.add( cameraLight );
 				scene.add( camera );
 				scene.add( camera );
 
 
+				const geometry = new THREE.PlaneGeometry( 1000, 1000 );
+				geometry.rotateX( - Math.PI / 2 );
+
+				const plane = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: 0x000000, visible: true } ) );
+				scene.add( plane );
+
 				const loader = new GLTFLoader();
 				const loader = new GLTFLoader();
 				loader.load( 'models/gltf/Michelle.glb', function ( gltf ) {
 				loader.load( 'models/gltf/Michelle.glb', function ( gltf ) {
 
 
@@ -124,15 +132,32 @@
 
 
 				} );
 				} );
 
 
-				//renderer
+				// renderer
 
 
 				renderer = new WebGPURenderer( { antialias: true } );
 				renderer = new WebGPURenderer( { antialias: true } );
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animate );
 				renderer.setAnimationLoop( animate );
-				renderer.toneMappingNode = toneMapping( THREE.LinearToneMapping, .17 );
 				document.body.appendChild( renderer.domElement );
 				document.body.appendChild( renderer.domElement );
 
 
+				// post processing ( just for WebGPUBackend for now )
+
+				if ( renderer.backend.isWebGPUBackend ) {
+
+					const scenePass = pass( scene, camera );
+					const scenePassColor = scenePass.getTextureNode();
+					const scenePassDepth = scenePass.getDepthNode().remapClamp( .15, .3 );
+
+					const scenePassColorBlurred = scenePassColor.gaussianBlur();
+					scenePassColorBlurred.directionNode = scenePassDepth;
+
+					postProcessing = new PostProcessing( renderer );
+					postProcessing.outputNode = scenePassColorBlurred;
+
+				}
+
+				// events
+
 				window.addEventListener( 'resize', onWindowResize );
 				window.addEventListener( 'resize', onWindowResize );
 
 
 			}
 			}
@@ -152,7 +177,15 @@
 
 
 				if ( mixer ) mixer.update( delta );
 				if ( mixer ) mixer.update( delta );
 
 
-				renderer.render( scene, camera );
+				if ( renderer.backend.isWebGPUBackend ) {
+
+					postProcessing.render();
+
+				} else {
+
+					renderer.render( scene, camera );
+
+				}
 
 
 			}
 			}
 
 

+ 2 - 1
test/e2e/puppeteer.js

@@ -108,7 +108,7 @@ const exceptionList = [
 	// could it fix some examples from above?
 	// could it fix some examples from above?
 	'physics_rapier_instancing',
 	'physics_rapier_instancing',
 
 
-	// Awaiting for WebGPU support
+	// Awaiting for WebGL backend support
 	'webgpu_clearcoat',
 	'webgpu_clearcoat',
 	'webgpu_compute_audio',
 	'webgpu_compute_audio',
 	'webgpu_compute_particles',
 	'webgpu_compute_particles',
@@ -122,6 +122,7 @@ const exceptionList = [
 	'webgpu_loader_gltf_iridescence',
 	'webgpu_loader_gltf_iridescence',
 	'webgpu_loader_gltf_sheen',
 	'webgpu_loader_gltf_sheen',
 	'webgpu_materials',
 	'webgpu_materials',
+	'webgpu_portal',
 	'webgpu_sandbox',
 	'webgpu_sandbox',
 	'webgpu_sprites',
 	'webgpu_sprites',
 	'webgpu_video_panorama',
 	'webgpu_video_panorama',