Browse Source

TSL: Editor (#26270)

* update label() behavior, use temp() instead

* Nodes: Add unlit support and some revisions

* Add `webgpu_tsl` example

* update texture

* some additional instructions

* update case

* cleanup

* update title

* adjust some decorations

* fix typo

* rename webgpu_tsl -> webgpu_tsl_editor

* update title

* cleanup

* output nodes

* revisions

* cleanup

* cleanup

* clean up

* fix needsUpdate

* cleanup
sunag 2 years ago
parent
commit
b691d7008e

+ 1 - 0
examples/files.json

@@ -333,6 +333,7 @@
 		"webgpu_skinning_instancing",
 		"webgpu_skinning_points",
 		"webgpu_sprites",
+		"webgpu_tsl_editor",
 		"webgpu_video_panorama"
 	],
 	"webaudio": [

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

@@ -10,7 +10,7 @@ export { default as AttributeNode, attribute } from './core/AttributeNode.js';
 export { default as BypassNode, bypass } from './core/BypassNode.js';
 export { default as CacheNode, cache } from './core/CacheNode.js';
 export { default as ConstNode } from './core/ConstNode.js';
-export { default as ContextNode, context } from './core/ContextNode.js';
+export { default as ContextNode, context, label } from './core/ContextNode.js';
 export { default as InstanceIndexNode, instanceIndex } from './core/InstanceIndexNode.js';
 export { default as LightingModel, lightingModel } from './core/LightingModel.js';
 export { default as Node, addNodeClass, createNodeFromType } from './core/Node.js';
@@ -28,7 +28,7 @@ export { default as PropertyNode, property, diffuseColor, roughness, metalness,
 export { default as StackNode, stack } from './core/StackNode.js';
 export { default as TempNode } from './core/TempNode.js';
 export { default as UniformNode, uniform } from './core/UniformNode.js';
-export { default as VarNode, label, temp } from './core/VarNode.js';
+export { default as VarNode, temp } from './core/VarNode.js';
 export { default as VaryingNode, varying } from './core/VaryingNode.js';
 
 import * as NodeUtils from './core/NodeUtils.js';

+ 2 - 2
examples/jsm/nodes/accessors/TangentNode.js

@@ -1,6 +1,6 @@
 import Node, { addNodeClass } from '../core/Node.js';
 import { attribute } from '../core/AttributeNode.js';
-import { label } from '../core/VarNode.js';
+import { temp } from '../core/VarNode.js';
 import { varying } from '../core/VaryingNode.js';
 import { normalize } from '../math/MathNode.js';
 import { cameraViewMatrix } from './CameraNode.js';
@@ -97,7 +97,7 @@ export const tangentGeometry = nodeImmutable( TangentNode, TangentNode.GEOMETRY
 export const tangentLocal = nodeImmutable( TangentNode, TangentNode.LOCAL );
 export const tangentView = nodeImmutable( TangentNode, TangentNode.VIEW );
 export const tangentWorld = nodeImmutable( TangentNode, TangentNode.WORLD );
-export const transformedTangentView = label( tangentView, 'TransformedTangentView' );
+export const transformedTangentView = temp( tangentView, 'TransformedTangentView' );
 export const transformedTangentWorld = normalize( transformedTangentView.transformDirection( cameraViewMatrix ) );
 
 addNodeClass( TangentNode );

+ 2 - 0
examples/jsm/nodes/core/ContextNode.js

@@ -53,7 +53,9 @@ class ContextNode extends Node {
 export default ContextNode;
 
 export const context = nodeProxy( ContextNode );
+export const label = ( node, name ) => context( node, { label: name } );
 
 addNodeElement( 'context', context );
+addNodeElement( 'label', label );
 
 addNodeClass( ContextNode );

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

@@ -46,8 +46,8 @@ class NodeBuilder {
 	constructor( object, renderer, parser ) {
 
 		this.object = object;
-		this.material = object && ( object.material || null );
-		this.geometry = object && ( object.geometry || null );
+		this.material = ( object && object.material ) || null;
+		this.geometry = ( object && object.geometry ) || null;
 		this.renderer = renderer;
 		this.parser = parser;
 
@@ -594,7 +594,7 @@ class NodeBuilder {
 
 	}
 
-	getUniformFromNode( node, type, shaderStage = this.shaderStage ) {
+	getUniformFromNode( node, type, shaderStage = this.shaderStage, name = null ) {
 
 		const nodeData = this.getDataFromNode( node, shaderStage );
 
@@ -604,7 +604,7 @@ class NodeBuilder {
 
 			const index = this.uniforms.index ++;
 
-			nodeUniform = new NodeUniform( 'nodeUniform' + index, type, node );
+			nodeUniform = new NodeUniform( name || ( 'nodeUniform' + index ), type, node );
 
 			this.uniforms[ shaderStage ].push( nodeUniform );
 

+ 1 - 1
examples/jsm/nodes/core/UniformNode.js

@@ -36,7 +36,7 @@ class UniformNode extends InputNode {
 
 		const sharedNodeType = sharedNode.getInputType( builder );
 
-		const nodeUniform = builder.getUniformFromNode( sharedNode, sharedNodeType, builder.shaderStage );
+		const nodeUniform = builder.getUniformFromNode( sharedNode, sharedNodeType, builder.shaderStage, builder.context.label );
 		const propertyName = builder.getPropertyName( nodeUniform );
 
 		return builder.format( propertyName, type, output );

+ 1 - 3
examples/jsm/nodes/core/VarNode.js

@@ -80,10 +80,8 @@ class VarNode extends Node {
 
 export default VarNode;
 
-export const label = nodeProxy( VarNode );
-export const temp = label;
+export const temp = nodeProxy( VarNode );
 
-addNodeElement( 'label', label );
 addNodeElement( 'temp', temp );
 
 addNodeClass( VarNode );

+ 0 - 15
examples/jsm/nodes/materials/LineBasicNodeMaterial.js

@@ -21,21 +21,6 @@ class LineBasicNodeMaterial extends NodeMaterial {
 
 	}
 
-	copy( source ) {
-
-		this.colorNode = source.colorNode;
-		this.opacityNode = source.opacityNode;
-
-		this.alphaTestNode = source.alphaTestNode;
-
-		this.lightNode = source.lightNode;
-
-		this.positionNode = source.positionNode;
-
-		return super.copy( source );
-
-	}
-
 }
 
 export default LineBasicNodeMaterial;

+ 0 - 15
examples/jsm/nodes/materials/MeshBasicNodeMaterial.js

@@ -20,21 +20,6 @@ class MeshBasicNodeMaterial extends NodeMaterial {
 
 	}
 
-	copy( source ) {
-
-		this.colorNode = source.colorNode;
-		this.opacityNode = source.opacityNode;
-
-		this.alphaTestNode = source.alphaTestNode;
-
-		this.lightNode = source.lightNode;
-
-		this.positionNode = source.positionNode;
-
-		return super.copy( source );
-
-	}
-
 }
 
 export default MeshBasicNodeMaterial;

+ 0 - 10
examples/jsm/nodes/materials/MeshNormalNodeMaterial.js

@@ -31,16 +31,6 @@ class MeshNormalNodeMaterial extends NodeMaterial {
 
 	}
 
-	copy( source ) {
-
-		this.opacityNode = source.opacityNode;
-
-		this.positionNode = source.positionNode;
-
-		return super.copy( source );
-
-	}
-
 }
 
 export default MeshNormalNodeMaterial;

+ 0 - 9
examples/jsm/nodes/materials/MeshPhongNodeMaterial.js

@@ -51,18 +51,9 @@ class MeshPhongNodeMaterial extends NodeMaterial {
 
 	copy( source ) {
 
-		this.colorNode = source.colorNode;
-		this.opacityNode = source.opacityNode;
-
-		this.alphaTestNode = source.alphaTestNode;
-
 		this.shininessNode = source.shininessNode;
 		this.specularNode = source.specularNode;
 
-		this.lightNode = source.lightNode;
-
-		this.positionNode = source.positionNode;
-
 		return super.copy( source );
 
 	}

+ 0 - 13
examples/jsm/nodes/materials/MeshStandardNodeMaterial.js

@@ -64,24 +64,11 @@ class MeshStandardNodeMaterial extends NodeMaterial {
 
 	copy( source ) {
 
-		this.colorNode = source.colorNode;
-		this.opacityNode = source.opacityNode;
-
-		this.alphaTestNode = source.alphaTestNode;
-
-		this.normalNode = source.normalNode;
-
 		this.emissiveNode = source.emissiveNode;
 
 		this.metalnessNode = source.metalnessNode;
 		this.roughnessNode = source.roughnessNode;
 
-		this.envNode = source.envNode;
-
-		this.lightsNode = source.lightsNode;
-
-		this.positionNode = source.positionNode;
-
 		return super.copy( source );
 
 	}

+ 41 - 11
examples/jsm/nodes/materials/NodeMaterial.js

@@ -45,6 +45,7 @@ class NodeMaterial extends ShaderMaterial {
 		this.alphaTestNode = null;
 
 		this.positionNode = null;
+		this.outputNode = null;
 
 	}
 
@@ -74,14 +75,22 @@ class NodeMaterial extends ShaderMaterial {
 
 		builder.addStack();
 
-		if ( this.normals === true ) this.constructNormal( builder );
+		if ( this.isUnlit === false ) {
 
-		this.constructDiffuseColor( builder );
-		this.constructVariants( builder );
+			if ( this.normals === true ) this.constructNormal( builder );
 
-		const outgoingLightNode = this.constructLighting( builder );
+			this.constructDiffuseColor( builder );
+			this.constructVariants( builder );
 
-		builder.stack.outputNode = this.constructOutput( builder, outgoingLightNode, diffuseColor.a );
+			const outgoingLightNode = this.constructLighting( builder );
+
+			builder.stack.outputNode = this.constructOutput( builder, vec4( outgoingLightNode, diffuseColor.a ) );
+
+		} else {
+
+			builder.stack.outputNode = this.constructOutput( builder, this.outputNode || vec4( 0, 0, 0, 1 ) );
+
+		}
 
 		builder.addFlow( 'fragment', builder.removeStack() );
 
@@ -265,7 +274,7 @@ class NodeMaterial extends ShaderMaterial {
 
 	}
 
-	constructOutput( builder, outgoingLight, opacity ) {
+	constructOutput( builder, outputNode ) {
 
 		const renderer = builder.renderer;
 
@@ -275,14 +284,10 @@ class NodeMaterial extends ShaderMaterial {
 
 		if ( toneMappingNode ) {
 
-			outgoingLight = toneMappingNode.context( { color: outgoingLight } );
+			outputNode = vec4( toneMappingNode.context( { color: outputNode.rgb } ), outputNode.a );
 
 		}
 
-		// @TODO: Optimize outputNode to vec3.
-
-		let outputNode = vec4( outgoingLight, opacity );
-
 		// ENCODING
 
 		const renderTarget = renderer.getRenderTarget();
@@ -406,6 +411,31 @@ class NodeMaterial extends ShaderMaterial {
 
 	}
 
+	get isUnlit() {
+
+		return this.constructor === NodeMaterial.prototype.constructor;
+
+	}
+
+	copy( source ) {
+
+		this.lightsNode = source.lightsNode;
+		this.envNode = source.envNode;
+
+		this.colorNode = source.colorNode;
+		this.normalNode = source.normalNode;
+		this.opacityNode = source.opacityNode;
+		this.backdropNode = source.backdropNode;
+		this.backdropAlphaNode = source.backdropAlphaNode;
+		this.alphaTestNode = source.alphaTestNode;
+
+		this.positionNode = source.positionNode;
+		this.outputNode = source.outputNode;
+
+		return super.copy( source );
+
+	}
+
 	static fromMaterial( material ) {
 
 		if ( material.isNodeMaterial === true ) { // is already a node material

+ 0 - 9
examples/jsm/nodes/materials/PointsNodeMaterial.js

@@ -36,17 +36,8 @@ class PointsNodeMaterial extends NodeMaterial {
 
 	copy( source ) {
 
-		this.colorNode = source.colorNode;
-		this.opacityNode = source.opacityNode;
-
-		this.alphaTestNode = source.alphaTestNode;
-
-		this.lightNode = source.lightNode;
-
 		this.sizeNode = source.sizeNode;
 
-		this.positionNode = source.positionNode;
-
 		return super.copy( source );
 
 	}

+ 0 - 7
examples/jsm/nodes/materials/SpriteNodeMaterial.js

@@ -88,13 +88,6 @@ class SpriteNodeMaterial extends NodeMaterial {
 
 	copy( source ) {
 
-		this.colorNode = source.colorNode;
-		this.opacityNode = source.opacityNode;
-
-		this.alphaTestNode = source.alphaTestNode;
-
-		this.lightNode = source.lightNode;
-
 		this.positionNode = source.positionNode;
 		this.rotationNode = source.rotationNode;
 		this.scaleNode = source.scaleNode;

+ 21 - 1
examples/jsm/renderers/common/RenderObject.js

@@ -1,9 +1,13 @@
+import { EventDispatcher } from 'three';
+
 let id = 0;
 
-export default class RenderObject {
+export default class RenderObject extends EventDispatcher {
 
 	constructor( nodes, geometries, renderer, object, material, scene, camera, lightsNode ) {
 
+		super();
+
 		this._nodes = nodes;
 		this._geometries = geometries;
 
@@ -25,6 +29,16 @@ export default class RenderObject {
 		this._materialVersion = - 1;
 		this._materialCacheKey = '';
 
+		const onDispose = () => {
+
+			this.material.removeEventListener( 'dispose', onDispose );
+
+			this.dispose();
+
+		};
+
+		this.material.addEventListener( 'dispose', onDispose );
+
 	}
 
 	getNodeBuilder() {
@@ -92,4 +106,10 @@ export default class RenderObject {
 
 	}
 
+	dispose() {
+
+		this.dispatchEvent( { type: 'dispose' } );
+
+	}
+
 }

+ 4 - 5
examples/jsm/renderers/common/RenderObjects.js

@@ -39,10 +39,9 @@ class RenderObjects extends ChainMap {
 
 			if ( data.cacheKey !== cacheKey ) {
 
-				data.cacheKey = cacheKey;
+				renderObject.dispose();
 
-				this.pipelines.delete( renderObject );
-				this.nodes.delete( renderObject );
+				renderObject = this.get( object, material, scene, camera, lightsNode );
 
 			}
 
@@ -71,7 +70,7 @@ class RenderObjects extends ChainMap {
 
 			const onDispose = () => {
 
-				renderObject.material.removeEventListener( 'dispose', onDispose );
+				renderObject.removeEventListener( 'dispose', onDispose );
 
 				this.pipelines.delete( renderObject );
 				this.nodes.delete( renderObject );
@@ -80,7 +79,7 @@ class RenderObjects extends ChainMap {
 
 			};
 
-			renderObject.material.addEventListener( 'dispose', onDispose );
+			renderObject.addEventListener( 'dispose', onDispose );
 
 		}
 

+ 2 - 2
examples/jsm/renderers/webgpu/WebGPUBackend.js

@@ -4,7 +4,7 @@ import 'https://greggman.github.io/webgpu-avoid-redundant-state-setting/webgpu-c
 
 import { GPUFeatureName, GPUTextureFormat, GPULoadOp, GPUStoreOp, GPUIndexFormat, GPUTextureViewDimension } from './utils/WebGPUConstants.js';
 
-import WebGPUNodeBuilder from './nodes/WGSLNodeBuilder.js';
+import WGSLNodeBuilder from './nodes/WGSLNodeBuilder.js';
 import Backend from '../common/Backend.js';
 
 import { DepthTexture, DepthFormat, DepthStencilFormat, UnsignedInt248Type, UnsignedIntType, WebGPUCoordinateSystem } from 'three';
@@ -576,7 +576,7 @@ class WebGPUBackend extends Backend {
 
 	createNodeBuilder( object, renderer ) {
 
-		return new WebGPUNodeBuilder( object, renderer );
+		return new WGSLNodeBuilder( object, renderer );
 
 	}
 

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

@@ -96,7 +96,7 @@ fn threejs_repeatWrapping( uv : vec2<f32>, dimension : vec2<u32> ) -> vec2<u32>
 ` )
 };
 
-class WebGPUNodeBuilder extends NodeBuilder {
+class WGSLNodeBuilder extends NodeBuilder {
 
 	constructor( object, renderer ) {
 
@@ -258,9 +258,9 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 	}
 
-	getUniformFromNode( node, type, shaderStage ) {
+	getUniformFromNode( node, type, shaderStage, name = null ) {
 
-		const uniformNode = super.getUniformFromNode( node, type, shaderStage );
+		const uniformNode = super.getUniformFromNode( node, type, shaderStage, name );
 		const nodeData = this.getDataFromNode( node, shaderStage );
 
 		if ( nodeData.uniformGPU === undefined ) {
@@ -516,7 +516,7 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 					snippets.push( `${ attributesSnippet } ${ varying.name } : ${ this.getType( varying.type ) }` );
 
-				} else if ( vars.includes( varying ) === false ) {
+				} else if ( shaderStage === 'vertex' && vars.includes( varying ) === false ) {
 
 					vars.push( varying );
 
@@ -554,7 +554,7 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 				if ( shaderStage === 'fragment' ) {
 
-					bindingSnippets.push( `@group( 0 ) @binding( ${index ++} ) var ${uniform.name}_sampler : sampler;` );
+					bindingSnippets.push( `@binding( ${index ++} ) @group( 0 ) var ${uniform.name}_sampler : sampler;` );
 
 				}
 
@@ -580,7 +580,7 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 				}
 
-				bindingSnippets.push( `@group( 0 ) @binding( ${index ++} ) var ${uniform.name} : ${textureType};` );
+				bindingSnippets.push( `@binding( ${index ++} ) @group( 0 ) var ${uniform.name} : ${textureType};` );
 
 			} else if ( uniform.type === 'buffer' || uniform.type === 'storageBuffer' ) {
 
@@ -861,4 +861,4 @@ var<${access}> ${name} : ${structName};`;
 
 }
 
-export default WebGPUNodeBuilder;
+export default WGSLNodeBuilder;

BIN
examples/screenshots/webgpu_tsl_editor.jpg


+ 226 - 0
examples/webgpu_tsl_editor.html

@@ -0,0 +1,226 @@
+<html lang="en">
+	<head>
+		<title>three.js - webgpu - tsl editor</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>
+
+		<style>
+			#source {
+				position: absolute;
+				top: 0;
+				left: 0;
+				width: 50%;
+				height: 100%;
+			}
+			#result {
+				position: absolute;
+				top: 0;
+				right: 0;
+				width: 50%;
+				height: 100%;
+			}
+			#renderer {
+				position: absolute;
+				top: 0;
+				right: 50%;
+				width: 200px;
+				height: 200px;
+				z-index: 100;
+				pointer-events: none;
+			}
+		</style>
+
+		<div id="source"></div>
+		<div id="result"></div>
+		<div id="renderer"></div>
+
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script src="https://cdn.jsdelivr.net/npm/monaco-editor@latest/min/vs/loader.min.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import * as Nodes from 'three/nodes';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+			import WGSLNodeBuilder from 'three/addons/renderers/webgpu/nodes/WGSLNodeBuilder.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				// add the depedencies
+
+				const width = 200;
+				const height = 200;
+
+				const camera = new THREE.PerspectiveCamera( 70, width / height, 0.1, 10 );
+				camera.position.z = .72;
+				camera.lookAt( 0, 0, 0 );
+
+				const scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x222222 );
+
+				const rendererDOM = document.getElementById( 'renderer' );
+
+				const renderer = new WebGPURenderer();
+				renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( 200, 200 );
+				rendererDOM.appendChild( renderer.domElement );
+
+				const material = new Nodes.NodeMaterial();
+				material.outputNode = Nodes.vec4( 0, 0, 0, 1 );
+
+				const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1 ), material );
+				scene.add( mesh );
+
+				renderer.setAnimationLoop( () => {
+
+					renderer.render( scene, camera );
+
+				} );
+
+				// editor
+
+				window.require.config( { paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@latest/min/vs' } } );
+
+				require( [ 'vs/editor/editor.main' ], () => {
+
+					const options = { shader: 'fragment', outputColorSpace: THREE.LinearSRGBColorSpace };
+
+					let timeout = null;
+					let nodeBuilder = null;
+
+					const editorDOM = document.getElementById( 'source' );
+					const resultDOM = document.getElementById( 'result' );
+
+					const tslCode = `// Example: Desaturate texture
+
+const { texture, uniform, vec4 } = TSL;
+
+//const samplerTexture = new THREE.Texture();
+const samplerTexture = new THREE.TextureLoader().load( './textures/uv_grid_opengl.jpg' );
+
+// label is optional
+const myMap = texture( samplerTexture ).rgb.label( 'myTexture' );
+const myColor = uniform( new THREE.Color( 0x0066ff ) ).label( 'myColor' );
+const opacity = .7;
+
+const desaturatedMap = myMap.rgb.saturation( 0 ); // try add .temp( 'myVar' ) after saturation()
+
+const finalColor = desaturatedMap.add( myColor );
+
+output = vec4( finalColor, opacity );
+`;
+
+					const editor = window.monaco.editor.create( editorDOM, {
+						value: tslCode,
+						language: 'javascript',
+						theme: 'vs-dark',
+						automaticLayout: true
+					} );
+
+					const result = window.monaco.editor.create( resultDOM, {
+						value: '',
+						language: 'wgsl',
+						theme: 'vs-dark',
+						automaticLayout: true,
+						readOnly: true
+					} );
+
+					const showCode = () => {
+
+						result.setValue( nodeBuilder[ options.shader + 'Shader' ] );
+						result.revealLine( 1 );
+
+					};
+
+					const build = () => {
+
+						try {
+
+							const tslCode = `let output = null;\n${ editor.getValue() }\nreturn { output };`;
+							const nodes = new Function( 'THREE', 'TSL', tslCode )( THREE, Nodes );
+
+							mesh.material.outputNode = nodes.output;
+							mesh.material.needsUpdate = true;
+
+							nodeBuilder = new WGSLNodeBuilder( mesh, renderer );
+							nodeBuilder.build();
+
+							showCode();
+
+							// extra debug info
+
+							/*const style = 'background-color: #333; color: white; font-style: italic; border: 2px solid #777; font-size: 22px;';
+
+							console.log( '%c  [ WGSL ] Vertex Shader      ', style );
+							console.log( nodeBuilder.vertexShader );
+							console.log( '%c  [ WGSL ] Fragment Shader    ', style );
+							console.log( nodeBuilder.fragmentShader );*/
+
+						} catch ( e ) {
+
+							result.setValue( 'Error: ' + e.message );
+
+						}
+
+					};
+
+					build();
+
+					editor.getModel().onDidChangeContent( () => {
+
+						if ( timeout ) clearTimeout( timeout );
+
+						timeout = setTimeout( build, 1000 );
+
+					} );
+
+					// gui
+
+					const gui = new GUI();
+
+					gui.add( options, 'shader', [ 'vertex', 'fragment' ] ).onChange( showCode );
+
+					gui.add( options, 'outputColorSpace', [ THREE.LinearSRGBColorSpace, THREE.SRGBColorSpace ] ).onChange( ( value ) => {
+
+						renderer.outputColorSpace = value;
+
+						build();
+
+					} );
+
+				} );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -130,6 +130,7 @@ const exceptionList = [
 	'webgpu_skinning_instancing',
 	'webgpu_skinning_points',
 	'webgpu_sprites',
+	'webgpu_tsl_editor',
 	'webgpu_video_panorama'
 
 ];