Просмотр исходного кода

Inspector: Introduce TSL Graph Addons (#33165)

sunag 1 месяц назад
Родитель
Сommit
d2dd6dc6ff

+ 1 - 0
examples/files.json

@@ -479,6 +479,7 @@
 		"webgpu_tsl_earth",
 		"webgpu_tsl_editor",
 		"webgpu_tsl_galaxy",
+		"webgpu_tsl_graph",
 		"webgpu_tsl_halftone",
 		"webgpu_tsl_interoperability",
 		"webgpu_tsl_procedural_terrain",

+ 43 - 0
examples/jsm/inspector/Inspector.js

@@ -44,6 +44,7 @@ class Inspector extends RendererInspector {
 		// init profiler
 
 		const profiler = new Profiler();
+		profiler.addEventListener( 'resize', ( e ) => this.dispatchEvent( e ) );
 
 		const parameters = new Parameters( {
 			builtin: true,
@@ -111,6 +112,48 @@ class Inspector extends RendererInspector {
 
 	}
 
+	hide() {
+
+		this.profiler.hide();
+
+	}
+
+	show() {
+
+		this.profiler.show();
+
+	}
+
+	getSize() {
+
+		return this.profiler.getSize();
+
+	}
+
+	setActiveTab( tab ) {
+
+		this.profiler.setActiveTab( tab.id );
+
+		return this;
+
+	}
+
+	addTab( tab ) {
+
+		this.profiler.addTab( tab );
+
+		return this;
+
+	}
+
+	removeTab( tab ) {
+
+		this.profiler.removeTab( tab );
+
+		return this;
+
+	}
+
 	resolveConsoleOnce( type, message ) {
 
 		const key = type + message;

+ 750 - 0
examples/jsm/inspector/addons/tsl-graph/TSLGraphEditor.js

@@ -0,0 +1,750 @@
+import { error } from 'three/webgpu';
+import { Tab } from '../../ui/Tab.js';
+import { TSLGraphLoader } from './TSLGraphLoader.js';
+
+const HOST_SOURCE = 'tsl-graph-host';
+const EDITOR_SOURCE = 'tsl-graph-editor';
+
+const _resposeByCommand = {
+	'tsl:command:get-code': 'tsl:response:get-code',
+	'tsl:command:set-root-material': 'tsl:response:set-root-material',
+	'tsl:command:get-graph': 'tsl:response:get-graph',
+	'tsl:command:load': 'tsl:response:load',
+	'tsl:command:clear-graph': 'tsl:response:clear-graph'
+};
+
+const _refMaterials = new WeakMap();
+
+export class TSLGraphEditor extends Tab {
+
+	constructor( options = {} ) {
+
+		super( 'TSL Graph', options );
+
+		const editorUrl = new URL( 'https://www.tsl-graph.xyz/editor/standalone' );
+		editorUrl.searchParams.set( 'graphs', 'material' );
+		editorUrl.searchParams.set( 'targetOrigin', '*' );
+
+		// UI Setup
+		this.content.style.display = 'flex';
+		this.content.style.flexDirection = 'column';
+		this.content.style.position = 'relative';
+
+		const headerDiv = document.createElement( 'div' );
+		headerDiv.style.padding = '4px';
+		headerDiv.style.backgroundColor = 'var(--profiler-header-bg, #2a2a33aa)';
+		headerDiv.style.borderBottom = '1px solid var(--profiler-border, #4a4a5a)';
+		headerDiv.style.display = 'flex';
+		headerDiv.style.justifyContent = 'center';
+		headerDiv.style.gap = '4px';
+
+		const importBtn = document.createElement( 'button' );
+		importBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>';
+		importBtn.className = 'panel-action-btn';
+		importBtn.title = 'Import';
+		importBtn.style.padding = '5px 8px';
+		importBtn.onclick = () => this._importData();
+
+		const exportBtn = document.createElement( 'button' );
+		exportBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>';
+		exportBtn.className = 'panel-action-btn';
+		exportBtn.title = 'Export';
+		exportBtn.style.padding = '5px 8px';
+		exportBtn.onclick = () => this._exportData();
+
+		const manageBtn = document.createElement( 'button' );
+		manageBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="3" y="3" width="7" height="7" rx="1"></rect><path d="M14 4h7"></path><path d="M14 9h7"></path><path d="M14 15h7"></path><path d="M14 20h7"></path></svg>';
+		manageBtn.className = 'panel-action-btn';
+		manageBtn.title = 'Saved Materials';
+		manageBtn.style.padding = '5px 8px';
+		manageBtn.onclick = () => this._showManagerModal();
+
+		headerDiv.appendChild( importBtn );
+		headerDiv.appendChild( exportBtn );
+		headerDiv.appendChild( manageBtn );
+
+		this.content.appendChild( headerDiv );
+
+		this.iframe = document.createElement( 'iframe' );
+		this.iframe.style.width = '100%';
+		this.iframe.style.minHeight = '600px';
+		this.iframe.style.border = 'none';
+		this.iframe.style.flex = '1';
+		this.iframe.src = editorUrl.toString();
+		this.editorOrigin = new URL( this.iframe.src ).origin;
+
+		this.content.appendChild( this.iframe );
+
+		this.material = null;
+		this.uniforms = null;
+
+		this.isReady = false;
+
+		this._codeData = null;
+		this._codeSaveTimeout = null;
+
+		this._pending = new Map();
+
+		this._resolveReady = null;
+		this._editorReady = new Promise( ( resolve ) => {
+
+			this._resolveReady = resolve;
+
+		} );
+
+		window.addEventListener( 'message', this.onMessage.bind( this ) );
+
+	}
+
+	get hasGraphs() {
+
+		return TSLGraphLoader.hasGraphs;
+
+	}
+
+	apply( scene ) {
+
+		const loader = new TSLGraphLoader();
+		const applier = loader.parse( TSLGraphLoader.getCodes() );
+		applier.apply( scene );
+
+		return this;
+
+	}
+
+	restoreMaterial( material ) {
+
+		material.copy( new material.constructor() );
+		material.needsUpdate = true;
+
+	}
+
+	async setMaterial( material ) {
+
+		if ( this.material === material ) return;
+
+		await this._setMaterial( material );
+
+		this.dispatchEvent( { type: 'change', material } );
+
+	}
+
+	async loadGraph( graphData ) {
+
+		await this.command( 'load', { graphData } );
+
+	}
+
+	async command( type, payload ) {
+
+		type = 'tsl:command:' + type;
+
+		await this._editorReady;
+
+		const requestId = this._makeRequestId();
+		const expectedType = _resposeByCommand[ type ];
+
+		return new Promise( ( resolve, reject ) => {
+
+			const timer = window.setTimeout( () => {
+
+				if ( ! this._pending.has( requestId ) ) return;
+				this._pending.delete( requestId );
+				reject( new Error( `Timeout for ${type}` ) );
+
+			}, 5000 );
+
+			this._pending.set( requestId, { expectedType, resolve, reject, timer } );
+
+			const message = { source: HOST_SOURCE, type, requestId };
+			if ( payload !== undefined ) message.payload = payload;
+
+			this._post( message );
+
+		} );
+
+	}
+
+	async getCode() {
+
+		return this.command( 'get-code' );
+
+	}
+
+	async getTSLFunction() {
+
+		const graphLoader = new TSLGraphLoader();
+		const applier = graphLoader.parse( await this.getCode() );
+
+		return applier.tslGraphFns[ 'tslGraph' ];
+
+	}
+
+	async getGraph() {
+
+		return ( await this.command( 'get-graph' ) ).graphData;
+
+	}
+
+	async onResponse( /*type, payload*/ ) {
+
+
+
+	}
+
+	async onEvent( type, payload ) {
+
+		if ( type === 'ready' ) {
+
+			if ( ! this.isReady ) {
+
+				this.isReady = true;
+
+				this._resolveReady();
+
+			}
+
+		} else if ( type === 'graph-changed' ) {
+
+			if ( this.material === null ) return;
+
+			await this._updateMaterial();
+
+			const graphData = await this.getGraph();
+
+			const graphId = this.material.userData.graphId;
+
+			TSLGraphLoader.setGraph( graphId, graphData );
+
+		} else if ( type === 'uniforms-changed' ) {
+
+			this._updateUniforms( payload.uniforms );
+
+		}
+
+	}
+
+	async onMessage( event ) {
+
+		if ( event.origin !== this.editorOrigin ) return;
+		if ( ! this._isEditorMessage( event.data ) ) return;
+
+		const msg = event.data;
+
+		if ( msg.requestId && msg.type.startsWith( 'tsl:response:' ) ) {
+
+			const waiter = this._pending.get( msg.requestId );
+			if ( ! waiter ) return;
+			if ( msg.type !== waiter.expectedType ) return;
+
+			this._pending.delete( msg.requestId );
+			window.clearTimeout( waiter.timer );
+
+			if ( msg.error ) waiter.reject( new Error( msg.error ) );
+			else waiter.resolve( msg.payload );
+
+			this.onResponse( msg.type.substring( 'tsl:response:'.length ), msg.payload );
+
+		} else if ( msg.type.startsWith( 'tsl:event:' ) ) {
+
+			this.onEvent( msg.type.substring( 'tsl:event:'.length ), msg.payload );
+
+		}
+
+	}
+
+	async _setMaterial( material ) {
+
+		if ( ! material ) {
+
+			this.material = null;
+			this.materialDefault = null;
+			this.uniforms = null;
+
+			await this.command( 'clear-graph' );
+
+			return;
+
+		}
+
+		if ( material.isNodeMaterial !== true ) {
+
+			error( 'Inspector: "Material" needs be a "NodeMaterial".' );
+
+			return;
+
+		}
+
+		if ( material.userData.graphId === undefined ) {
+
+			error( 'Inspector: "NodeMaterial" has no graphId. Set a "graphId" for the material in "material.userData.graphId".' );
+
+			return;
+
+		}
+
+		let materialDefault = _refMaterials.get( material );
+
+		if ( materialDefault === undefined ) {
+
+			//materialDefault = material.clone();
+			materialDefault = new material.constructor();
+			materialDefault.userData = material.userData;
+
+			_refMaterials.set( material, materialDefault );
+
+		}
+
+		this.material = material;
+		this.materialDefault = materialDefault;
+		this.uniforms = null;
+
+		const graphData = TSLGraphLoader.getGraph( this.material.userData.graphId );
+
+		if ( graphData ) {
+
+			await this.loadGraph( graphData );
+
+		} else {
+
+			await this.command( 'clear-graph' );
+
+			await this.command( 'set-root-material', { materialType: this._getGraphType( this.material ) } );
+
+		}
+
+	}
+
+	_getGraphType( material ) {
+
+		if ( material.isMeshPhysicalNodeMaterial ) return 'material/physical';
+		if ( material.isMeshStandardNodeMaterial ) return 'material/standard';
+		if ( material.isMeshPhongNodeMaterial ) return 'material/phong';
+		if ( material.isMeshBasicNodeMaterial ) return 'material/basic';
+		if ( material.isSpriteNodeMaterial ) return 'material/sprite';
+
+		return 'material/node';
+
+	}
+
+	_showManagerModal() {
+
+		const overlay = document.createElement( 'div' );
+		overlay.style.position = 'absolute';
+		overlay.style.top = '0';
+		overlay.style.left = '0';
+		overlay.style.width = '100%';
+		overlay.style.height = '100%';
+		overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
+		overlay.style.zIndex = '100';
+		overlay.style.display = 'flex';
+		overlay.style.justifyContent = 'center';
+		overlay.style.alignItems = 'center';
+		overlay.onclick = ( e ) => {
+
+			if ( e.target === overlay ) {
+
+				this.content.removeChild( overlay );
+
+			}
+
+		};
+
+		const modal = document.createElement( 'div' );
+		modal.style.width = '80%';
+		modal.style.maxWidth = '500px';
+		modal.style.height = '400px';
+		modal.style.backgroundColor = 'var(--profiler-bg, #1e1e24f5)';
+		modal.style.border = '1px solid var(--profiler-border, #4a4a5a)';
+		modal.style.borderRadius = '8px';
+		modal.style.display = 'flex';
+		modal.style.flexDirection = 'column';
+
+		const header = document.createElement( 'div' );
+		header.style.padding = '15px';
+		header.style.borderBottom = '1px solid var(--profiler-border, #4a4a5a)';
+		header.style.display = 'flex';
+		header.style.justifyContent = 'space-between';
+		header.style.alignItems = 'center';
+		header.style.gap = '15px';
+
+		const filterInput = document.createElement( 'input' );
+		filterInput.type = 'text';
+		filterInput.className = 'console-filter-input';
+		filterInput.placeholder = 'Filter...';
+		filterInput.style.flex = '1';
+
+		const closeBtn = document.createElement( 'button' );
+		closeBtn.innerHTML = '&#x2715;';
+		closeBtn.style.background = 'transparent';
+		closeBtn.style.border = 'none';
+		closeBtn.style.color = 'var(--text-secondary, #9a9aab)';
+		closeBtn.style.cursor = 'pointer';
+		closeBtn.style.fontSize = '16px';
+		closeBtn.onmouseover = () => closeBtn.style.color = 'var(--text-primary, #e0e0e0)';
+		closeBtn.onmouseout = () => closeBtn.style.color = 'var(--text-secondary, #9a9aab)';
+		closeBtn.onclick = () => this.content.removeChild( overlay );
+
+		header.appendChild( filterInput );
+		header.appendChild( closeBtn );
+
+		const codes = this.getCodes();
+		const materialIds = Object.keys( codes.materials || {} );
+
+		if ( materialIds.length === 0 ) {
+
+			const listContainer = document.createElement( 'div' );
+			listContainer.style.padding = '10px';
+			listContainer.style.flex = '1';
+
+			const emptyMsg = document.createElement( 'div' );
+			emptyMsg.textContent = 'No saved materials found.';
+			emptyMsg.style.color = 'var(--text-secondary, #9a9aab)';
+			emptyMsg.style.padding = '10px';
+			emptyMsg.style.textAlign = 'center';
+			emptyMsg.style.fontFamily = 'var(--font-family, sans-serif)';
+			emptyMsg.style.fontSize = '12px';
+			listContainer.appendChild( emptyMsg );
+
+			modal.appendChild( header );
+			modal.appendChild( listContainer );
+
+		} else {
+
+			const listHeaderContainer = document.createElement( 'div' );
+			listHeaderContainer.style.display = 'grid';
+			listHeaderContainer.style.gridTemplateColumns = '1fr 80px';
+			listHeaderContainer.style.gap = '10px';
+			listHeaderContainer.style.padding = '10px 15px 8px 15px';
+			listHeaderContainer.style.borderBottom = '1px solid var(--profiler-border, #4a4a5a)';
+			listHeaderContainer.style.backgroundColor = 'var(--profiler-bg, #1e1e24f5)';
+			listHeaderContainer.style.fontFamily = 'var(--font-family, sans-serif)';
+			listHeaderContainer.style.fontSize = '11px';
+			listHeaderContainer.style.fontWeight = 'bold';
+			listHeaderContainer.style.textTransform = 'uppercase';
+			listHeaderContainer.style.letterSpacing = '0.5px';
+			listHeaderContainer.style.color = 'var(--text-secondary, #9a9aab)';
+			listHeaderContainer.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
+			listHeaderContainer.style.zIndex = '1';
+
+			const col1 = document.createElement( 'div' );
+			col1.textContent = 'Material Name / ID';
+			const col2 = document.createElement( 'div' );
+			col2.textContent = 'Action';
+			col2.style.textAlign = 'right';
+
+			listHeaderContainer.appendChild( col1 );
+			listHeaderContainer.appendChild( col2 );
+
+			const scrollWrapper = document.createElement( 'div' );
+			scrollWrapper.style.flex = '1';
+			scrollWrapper.style.overflowY = 'auto';
+			scrollWrapper.style.padding = '0';
+
+			const rows = [];
+
+			for ( const id of materialIds ) {
+
+				const itemRow = document.createElement( 'div' );
+				itemRow.style.display = 'grid';
+				itemRow.style.gridTemplateColumns = '1fr 80px';
+				itemRow.style.gap = '10px';
+				itemRow.style.alignItems = 'center';
+				itemRow.style.padding = '8px 15px';
+				itemRow.style.borderBottom = '1px solid rgba(74, 74, 90, 0.4)';
+				itemRow.onmouseover = () => itemRow.style.backgroundColor = 'rgba(255, 255, 255, 0.04)';
+				itemRow.onmouseout = () => itemRow.style.backgroundColor = 'transparent';
+
+				const nameSpan = document.createElement( 'span' );
+				const materialData = codes.materials[ id ];
+				const materialName = materialData.name || id;
+				nameSpan.textContent = materialName;
+				nameSpan.style.fontFamily = 'var(--font-mono, monospace)';
+				nameSpan.style.fontSize = '12px';
+				nameSpan.style.color = 'var(--text-primary, #e0e0e0)';
+				nameSpan.style.userSelect = 'all';
+				nameSpan.style.overflow = 'hidden';
+				nameSpan.style.textOverflow = 'ellipsis';
+				nameSpan.style.whiteSpace = 'nowrap';
+
+				const actionContainer = document.createElement( 'div' );
+				actionContainer.style.textAlign = 'right';
+
+				const removeBtn = document.createElement( 'button' );
+				removeBtn.textContent = 'Remove';
+				removeBtn.style.background = 'rgba(244, 67, 54, 0.1)';
+				removeBtn.style.border = '1px solid var(--color-red, #f44336)';
+				removeBtn.style.color = 'var(--color-red, #f44336)';
+				removeBtn.style.borderRadius = '4px';
+				removeBtn.style.padding = '4px 8px';
+				removeBtn.style.cursor = 'pointer';
+				removeBtn.style.fontSize = '11px';
+				removeBtn.onmouseover = () => removeBtn.style.background = 'rgba(244, 67, 54, 0.2)';
+				removeBtn.onmouseout = () => removeBtn.style.background = 'rgba(244, 67, 54, 0.1)';
+
+				actionContainer.appendChild( removeBtn );
+
+				itemRow.appendChild( nameSpan );
+				itemRow.appendChild( actionContainer );
+
+				scrollWrapper.appendChild( itemRow );
+
+				rows.push( { element: itemRow, text: materialName.toLowerCase() } );
+
+				removeBtn.onclick = async () => {
+
+					delete codes.materials[ id ];
+					TSLGraphLoader.setCodes( codes );
+					TSLGraphLoader.deleteGraph( id );
+					scrollWrapper.removeChild( itemRow );
+
+					const index = rows.findIndex( r => r.element === itemRow );
+					if ( index > - 1 ) rows.splice( index, 1 );
+
+					if ( rows.length === 0 ) {
+
+						modal.removeChild( listHeaderContainer );
+						modal.removeChild( scrollWrapper );
+
+						const listContainer = document.createElement( 'div' );
+						listContainer.style.padding = '10px';
+						listContainer.style.flex = '1';
+
+						const emptyMsg = document.createElement( 'div' );
+						emptyMsg.textContent = 'No saved materials found.';
+						emptyMsg.style.color = 'var(--text-secondary, #9a9aab)';
+						emptyMsg.style.padding = '10px';
+						emptyMsg.style.textAlign = 'center';
+						emptyMsg.style.fontFamily = 'var(--font-family, sans-serif)';
+						emptyMsg.style.fontSize = '12px';
+
+						listContainer.appendChild( emptyMsg );
+						modal.appendChild( listContainer );
+
+					}
+
+					_refMaterials.delete( this.material );
+
+					if ( this.material && this.material.userData.graphId === id ) {
+
+						this.restoreMaterial( this.material );
+
+						await this.setMaterial( null );
+
+					}
+
+					this.dispatchEvent( { type: 'remove', graphId: id } );
+
+				};
+
+			}
+
+			filterInput.addEventListener( 'input', ( e ) => {
+
+				const term = e.target.value.toLowerCase();
+				for ( const row of rows ) {
+
+					row.element.style.display = row.text.includes( term ) ? 'grid' : 'none';
+
+				}
+
+			} );
+
+			modal.appendChild( header );
+			modal.appendChild( listHeaderContainer );
+			modal.appendChild( scrollWrapper );
+
+		}
+
+		overlay.appendChild( modal );
+
+		this.content.appendChild( overlay );
+
+	}
+
+	_exportData() {
+
+		const codes = this.getCodes();
+		const materialIds = Object.keys( codes.materials || {} );
+
+		const exportPayload = {
+			codes: codes,
+			graphs: {}
+		};
+
+		for ( const id of materialIds ) {
+
+			const graphData = TSLGraphLoader.getGraph( id );
+
+			if ( graphData ) {
+
+				exportPayload.graphs[ id ] = graphData;
+
+			}
+
+		}
+
+		const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent( JSON.stringify( exportPayload, null, '\t' ) );
+		const downloadAnchorNode = document.createElement( 'a' );
+		downloadAnchorNode.setAttribute( 'href', dataStr );
+		downloadAnchorNode.setAttribute( 'download', 'tsl-graphs.json' );
+		document.body.appendChild( downloadAnchorNode );
+		downloadAnchorNode.click();
+		downloadAnchorNode.remove();
+
+	}
+
+	_importData() {
+
+		const fileInput = document.createElement( 'input' );
+		fileInput.type = 'file';
+		fileInput.accept = '.json';
+
+		fileInput.onchange = e => {
+
+			const file = e.target.files[ 0 ];
+
+			if ( ! file ) return;
+
+			const reader = new FileReader();
+			reader.onload = async ( event ) => {
+
+				try {
+
+					const importedData = TSLGraphLoader.setGraphs( JSON.parse( event.target.result ) );
+
+					this._codeData = importedData.codes;
+
+					// Reload visual state if we have a material open
+					if ( this.material ) {
+
+						// refresh material
+						await this._setMaterial( this.material );
+
+					}
+
+				} catch ( err ) {
+
+					error( 'TSLGraph: Failed to parse or load imported JSON.', err );
+
+				}
+
+			};
+
+			reader.readAsText( file );
+
+		};
+
+		fileInput.click();
+
+	}
+
+	getCodes() {
+
+		if ( this._codeData === null ) {
+
+			this._codeData = TSLGraphLoader.getCodes();
+
+		}
+
+		return this._codeData;
+
+	}
+
+	_saveCode() {
+
+		const graphId = this.material.userData.graphId;
+
+		clearTimeout( this._codeSaveTimeout );
+
+		this._codeSaveTimeout = setTimeout( async () => {
+
+			if ( this.material === null || graphId !== this.material.userData.graphId ) return;
+
+			const codes = this.getCodes();
+			const codeData = await this.getCode();
+
+			codes.materials[ graphId ] = codeData.material;
+
+			TSLGraphLoader.setCodes( codes );
+
+		}, 1000 );
+
+	}
+
+	_restoreMaterial() {
+
+		this.material.copy( this.materialDefault );
+
+	}
+
+	async _updateMaterial() {
+
+		this._restoreMaterial();
+
+		const applyNodes = await this.getTSLFunction();
+
+		const { uniforms } = applyNodes( this.material );
+
+		this.uniforms = uniforms;
+		this.material.needsUpdate = true;
+
+		this._saveCode();
+
+	}
+
+	_updateUniforms( uniforms ) {
+
+		if ( this.uniforms === null ) return;
+
+		for ( const uniform of uniforms ) {
+
+			const uniformNode = this.uniforms[ uniform.name ];
+			const uniformType = uniform.uniformType;
+
+			const value = uniform.value;
+
+			if ( uniformType.startsWith( 'vec' ) ) {
+
+				uniformNode.value.fromArray( value );
+
+			} else if ( uniformType.startsWith( 'color' ) ) {
+
+				uniformNode.value.setHex( parseInt( value.slice( 1 ), 16 ) );
+
+			} else {
+
+				uniformNode.value = value;
+
+			}
+
+		}
+
+		this._saveCode();
+
+	}
+
+	_isEditorMessage( value ) {
+
+		if ( ! value || typeof value !== 'object' ) return false;
+		return value.source === EDITOR_SOURCE && typeof value.type === 'string';
+
+	}
+
+	_makeRequestId() {
+
+		return `${Date.now()}-${Math.random().toString( 36 ).slice( 2, 10 )}`;
+
+	}
+
+	_post( message ) {
+
+		if ( this.iframe.contentWindow ) {
+
+			this.iframe.contentWindow.postMessage( message, this.editorOrigin );
+
+		}
+
+	}
+
+}

+ 281 - 0
examples/jsm/inspector/addons/tsl-graph/TSLGraphLoader.js

@@ -0,0 +1,281 @@
+import { FileLoader, error } from 'three';
+
+import * as THREE from 'three';
+import * as TSL from 'three/tsl';
+
+const _library = {
+	'three/tsl': { ...TSL }
+};
+
+const STORAGE_PREFIX = 'tsl-graph';
+const STORAGE_CODE = 'tsl-graph-code';
+
+function _storageKey( graphId ) {
+
+	return `${STORAGE_PREFIX}:${graphId}`;
+
+}
+
+class TSLGraphLoaderApplier {
+
+	constructor( tslGraphFns ) {
+
+		this.tslGraphFns = tslGraphFns;
+
+	}
+
+	apply( scene ) {
+
+		const tslGraphFns = this.tslGraphFns;
+
+		scene.traverse( ( object ) => {
+
+			if ( object.material && object.material.userData.graphId ) {
+
+				if ( tslGraphFns[ object.material.userData.graphId ] ) {
+
+					tslGraphFns[ object.material.userData.graphId ]( object.material );
+
+					object.material.needsUpdate = true;
+
+				}
+
+			}
+
+		} );
+
+	}
+
+}
+
+export class TSLGraphLoader extends FileLoader {
+
+	constructor( manager ) {
+
+		super( manager );
+
+	}
+
+	load( url, onLoad, onProgress, onError ) {
+
+		super.load( url, ( text ) => {
+
+			let json;
+
+			try {
+
+				json = JSON.parse( text );
+
+			} catch ( e ) {
+
+				if ( onError ) onError( e );
+
+				return;
+
+			}
+
+			const applier = this.parse( json );
+
+			if ( onLoad ) onLoad( applier );
+
+		}, onProgress, onError );
+
+	}
+
+	parseMaterial( json ) {
+
+		const baseFn = 'tslGraph';
+
+		const imports = {};
+		const materials = [ this._generateMaterialCode( json, baseFn, imports ) ];
+		const code = this._generateCode( materials, imports );
+
+		const tslFunction = new Function( code )()( THREE, imports );
+
+		return tslFunction;
+
+	}
+
+	parseMaterials( json ) {
+
+		const imports = {};
+		const materials = [];
+
+		for ( const [ name, material ] of Object.entries( json ) ) {
+
+			materials.push( this._generateMaterialCode( material, name, imports ) );
+
+		}
+
+		const code = this._generateCode( materials, imports );
+
+		const tslFunction = new Function( code )()( THREE, imports );
+
+		return tslFunction;
+
+	}
+
+	parse( json ) {
+
+		let result;
+
+		if ( json.material && json.material.code ) {
+
+			result = this.parseMaterial( json.material );
+
+		} else if ( json.materials ) {
+
+			result = this.parseMaterials( json.materials );
+
+		} else if ( json.codes && json.graphs ) {
+
+			result = this.parseMaterials( json.codes.materials );
+
+			TSLGraphLoader.setGraphs( json );
+
+		}
+
+		return new TSLGraphLoaderApplier( result );
+
+	}
+
+	_generateMaterialCode( json, name = 'tslGraph', imports = {} ) {
+
+		const code = json.code.replace( 'function tslGraph', `materials[ '${ name }' ] = function` ).replace( /\n|^/g, '\n\t' );
+
+		for ( const importData of json.imports ) {
+
+			if ( _library[ importData.from ] ) {
+
+				for ( const importName of importData.imports ) {
+
+					if ( _library[ importData.from ][ importName ] ) {
+
+						imports[ importName ] = _library[ importData.from ][ importName ];
+
+					} else {
+
+						error( `TSLGraph: Import ${ importName } not found in ${ importData.from }.` );
+
+					}
+
+				}
+
+			} else {
+
+				error( `TSLGraph: Library ${ importData.from } not found.` );
+
+			}
+
+		}
+
+		return code;
+
+	}
+
+	_generateCode( materials, imports ) {
+
+		const fnCode = `return ( THREE, { ${ Object.keys( imports ).join( ', ' ) } } ) => {\n\n\tconst materials = {};\n${ materials.join( '\n' ) }\n\n\treturn materials;\n\n}`;
+
+		return fnCode;
+
+	}
+
+	static get hasGraphs() {
+
+		return Object.keys( TSLGraphLoader.getCodes().materials ).length > 0;
+
+	}
+
+	static getCodes() {
+
+		const code = window.localStorage.getItem( STORAGE_CODE );
+
+		return code ? JSON.parse( code ) : { materials: {} };
+
+	}
+
+	static setCodes( codes ) {
+
+		window.localStorage.setItem( STORAGE_CODE, JSON.stringify( codes ) );
+
+	}
+
+	static setGraph( graphId, graphData ) {
+
+		window.localStorage.setItem( _storageKey( graphId ), JSON.stringify( graphData ) );
+
+	}
+
+	static getGraph( graphId ) {
+
+		const raw = window.localStorage.getItem( _storageKey( graphId ) );
+
+		if ( ! raw ) return null;
+
+		try {
+
+			return JSON.parse( raw );
+
+		} catch ( e ) {
+
+			error( 'TSLGraph: Invalid graph JSON in localStorage, ignoring.', e );
+			return null;
+
+		}
+
+	}
+
+	static deleteGraph( graphId ) {
+
+		window.localStorage.removeItem( _storageKey( graphId ) );
+
+	}
+
+	static setGraphs( json ) {
+
+		if ( ! json.codes || ! json.graphs ) {
+
+			throw new Error( 'TSLGraph: Invalid import file structure.' );
+
+		}
+
+		TSLGraphLoader.clearGraphs();
+
+		// Save imported graph visualizations
+		for ( const [ id, graphData ] of Object.entries( json.graphs ) ) {
+
+			TSLGraphLoader.setGraph( id, graphData );
+
+		}
+
+		// Fully overwrite codes
+		TSLGraphLoader.setCodes( json.codes );
+
+		return json;
+
+	}
+
+	static clearGraphs() {
+
+		const keysToRemove = [];
+		for ( let i = 0; i < window.localStorage.length; i ++ ) {
+
+			const key = window.localStorage.key( i );
+			if ( key.startsWith( STORAGE_PREFIX ) ) {
+
+				keysToRemove.push( key );
+
+			}
+
+		}
+
+		for ( const key of keysToRemove ) {
+
+			window.localStorage.removeItem( key );
+
+		}
+
+	}
+
+}

+ 75 - 37
examples/jsm/inspector/ui/Profiler.js

@@ -1,9 +1,12 @@
+import { EventDispatcher } from 'three';
 import { Style } from './Style.js';
 
-export class Profiler {
+export class Profiler extends EventDispatcher {
 
 	constructor() {
 
+		super();
+
 		this.tabs = {};
 		this.activeTabId = null;
 		this.isResizing = false;
@@ -32,6 +35,26 @@ export class Profiler {
 
 	}
 
+	getSize() {
+
+		if ( this.panel.classList.contains( 'visible' ) === false || this.panel.classList.contains( 'no-tabs' ) ) {
+
+			return { width: 0, height: 0 };
+
+		}
+
+		if ( this.position === 'right' ) {
+
+			return { width: this.panel.offsetWidth, height: 0 };
+
+		} else {
+
+			return { width: 0, height: this.panel.offsetHeight };
+
+		}
+
+	}
+
 	detectMobile() {
 
 		// Check for mobile devices
@@ -310,6 +333,8 @@ export class Profiler {
 
 				}
 
+				this.dispatchEvent( { type: 'resize' } );
+
 			};
 
 			const onEnd = () => {
@@ -402,6 +427,49 @@ export class Profiler {
 
 		}
 
+		this.dispatchEvent( { type: 'resize' } );
+
+	}
+
+	hide() {
+
+		this.miniPanel.classList.remove( 'visible' );
+
+		this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
+
+			content.style.display = 'none';
+
+		} );
+
+		this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ).forEach( btn => {
+
+			btn.classList.remove( 'active' );
+
+		} );
+
+	}
+
+	show( tab ) {
+
+		this.hide();
+
+		tab.builtinButton.classList.add( 'active' );
+
+		if ( ! tab.miniContent.firstChild ) {
+
+			const actualContent = tab.content.querySelector( '.list-scroll-wrapper' ) || tab.content.firstElementChild;
+
+			if ( actualContent ) {
+
+				tab.miniContent.appendChild( actualContent );
+
+			}
+
+		}
+
+		tab.miniContent.style.display = 'block';
+		this.miniPanel.classList.add( 'visible' );
+
 	}
 
 	addTab( tab ) {
@@ -488,47 +556,13 @@ export class Profiler {
 			// Toggle mini-panel for this tab
 			const isCurrentlyActive = miniContent.style.display !== 'none' && miniContent.children.length > 0;
 
-			// Hide all other mini-panel contents
-			this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
-
-				content.style.display = 'none';
-
-			} );
-
-			// Remove active state from all builtin buttons
-			this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ).forEach( btn => {
-
-				btn.classList.remove( 'active' );
-
-			} );
-
 			if ( isCurrentlyActive ) {
 
-				// Toggle off - hide mini-panel
-				this.miniPanel.classList.remove( 'visible' );
-				miniContent.style.display = 'none';
+				this.hide();
 
 			} else {
 
-				// Toggle on - show mini-panel with this tab's content
-				builtinButton.classList.add( 'active' );
-
-				// Move actual content to mini-panel (not clone) if not already there
-				if ( ! miniContent.firstChild ) {
-
-					const actualContent = tab.content.querySelector( '.list-scroll-wrapper' ) || tab.content.firstElementChild;
-
-					if ( actualContent ) {
-
-						miniContent.appendChild( actualContent );
-
-					}
-
-				}
-
-				// Show after content is moved
-				miniContent.style.display = 'block';
-				this.miniPanel.classList.add( 'visible' );
+				this.show( tab );
 
 			}
 
@@ -622,6 +656,8 @@ export class Profiler {
 
 		}
 
+		this.dispatchEvent( { type: 'resize' } );
+
 	}
 
 	setupTabDragAndDrop( tab ) {
@@ -1470,6 +1506,8 @@ export class Profiler {
 
 		} );
 
+		this.dispatchEvent( { type: 'resize' } );
+
 		this.saveLayout();
 
 	}

+ 24 - 0
examples/jsm/inspector/ui/Style.js

@@ -1097,6 +1097,11 @@ export class Style {
 	border-radius: 15px;
 }
 
+.console-filter-input:focus {
+	outline: none;
+	border-color: var(--text-secondary);
+}
+
 .console-copy-button {
 	background: transparent;
 	border: none;
@@ -1630,6 +1635,25 @@ body:has(#profiler-panel:not(.visible)) .detached-tab-panel {
 .detached-tab-content input[type="number"] {
 	-moz-appearance: textfield;
 }
+
+.panel-action-btn {
+	background: transparent;
+	color: var(--text-primary);
+	border: 1px solid var(--profiler-border);
+	border-radius: 4px;
+	padding: 6px 12px;
+	cursor: pointer;
+	font-family: var(--font-family);
+	font-size: 12px;
+	transition: background-color 0.2s;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.panel-action-btn:hover {
+	background-color: rgba(255, 255, 255, 0.05);
+}
 `;
 		const styleElement = document.createElement( 'style' );
 		styleElement.id = 'profiler-styles';

+ 5 - 1
examples/jsm/inspector/ui/Tab.js

@@ -1,3 +1,5 @@
+import { EventDispatcher } from 'three';
+
 /**
  * Tab class
  * @param {string} title - The title of the tab
@@ -23,10 +25,12 @@
  * tab3.showBuiltin(); // Show the builtin button and mini-content
  * tab3.hideBuiltin(); // Hide the builtin button and mini-content
  */
-export class Tab {
+export class Tab extends EventDispatcher {
 
 	constructor( title, options = {} ) {
 
+		super();
+
 		this.id = title.toLowerCase();
 		this.button = document.createElement( 'button' );
 		this.button.className = 'tab-btn';

BIN
examples/screenshots/webgpu_tsl_graph.jpg


+ 1309 - 0
examples/shaders/tsl-graphs.json

@@ -0,0 +1,1309 @@
+{
+	"codes": {
+		"materials": {
+			"mat_4-basic": {
+				"code": "// Expected material: MeshBasicNodeMaterial\nfunction tslGraph(material) {\n    const _node0 = time;\n    const _node1 = normalWorld;\n    const _node2 = color(\"#00a9ff\");\n    const _node3 = color(\"#ffffff\");\n    const _node4 = add(_node1, _node0);\n    const _node5 = mx_fractal_noise_float(_node4, 3, 3, 0.4, 0.5);\n    const _node6 = mix(_node2, _node3, _node5);\n    const _node7 = add(_node5, 0.6);\n\n    material.colorNode = _node6;\n    material.opacityNode = _node7;\n    material.transparent = true;\n    material.depthWrite = true;\n\n    const uniforms = {};\n\n    return { uniforms };\n}",
+				"error": null,
+				"imports": [
+					{
+						"from": "three/tsl",
+						"imports": [
+							"add",
+							"color",
+							"float",
+							"mix",
+							"mx_fractal_noise_float",
+							"normalWorld",
+							"time",
+							"vec3"
+						]
+					}
+				]
+			},
+			"mat_3-phong": {
+				"code": "// Expected material: MeshPhongNodeMaterial\nfunction tslGraph(material) {\n    const _node0 = positionWorld;\n    const _node1 = time;\n    const _node2 = color(\"#29fcff\");\n    const _node3 = mul(_node0, 3.7);\n    const _node4 = add(_node3, _node1);\n    const _node5 = mx_worley_noise_float(_node4, 1);\n    const _node6 = add(_node5, _node2);\n\n    material.colorNode = _node6;\n    material.opacityNode = float(1);\n\n    const uniforms = {};\n\n    return { uniforms };\n}",
+				"error": null,
+				"imports": [
+					{
+						"from": "three/tsl",
+						"imports": [
+							"add",
+							"color",
+							"float",
+							"mul",
+							"mx_worley_noise_float",
+							"positionWorld",
+							"time",
+							"vec3"
+						]
+					}
+				]
+			},
+			"mat_1-physical": {
+				"code": "// Expected material: MeshStandardNodeMaterial\nfunction tslGraph(material) {\n    const _node0 = positionWorld;\n    const _node1 = triNoise3D(_node0, 1, 0);\n\n    material.colorNode = _node1;\n    material.roughnessNode = float(1);\n    material.metalnessNode = float(0);\n\n    const uniforms = {};\n\n    return { uniforms };\n}",
+				"error": null,
+				"imports": [
+					{
+						"from": "three/tsl",
+						"imports": [
+							"color",
+							"float",
+							"positionWorld",
+							"triNoise3D",
+							"vec3"
+						]
+					}
+				]
+			},
+			"mat_2-standard": {
+				"code": "// Expected material: MeshStandardNodeMaterial\nfunction tslGraph(material) {\n    const _node0 = normalWorld;\n    const _node1 = positionWorld;\n    const _node2 = color(\"#a4fb47\");\n    const _node3 = color(\"#044813\");\n    const _node4 = clamp(_node0.y, 0, 1);\n    const _node5 = triNoise3D(_node1, 1, 0);\n    const _node6 = mul(_node1, 66.7);\n    const _node7 = mx_fractal_noise_float(_node6, 4, 2, 0.5, 0.5);\n    const _node8 = clamp(_node7, 0, 1);\n    const _node9 = mix(_node3, _node2, _node8);\n    const _node10 = mix(_node5, _node9, _node4);\n\n    material.colorNode = _node10;\n    material.roughnessNode = float(1);\n    material.metalnessNode = float(0);\n\n    const uniforms = {};\n\n    return { uniforms };\n}",
+				"error": null,
+				"imports": [
+					{
+						"from": "three/tsl",
+						"imports": [
+							"clamp",
+							"color",
+							"float",
+							"mix",
+							"mul",
+							"mx_fractal_noise_float",
+							"normalWorld",
+							"positionWorld",
+							"triNoise3D",
+							"vec3"
+						]
+					}
+				]
+			}
+		}
+	},
+	"graphs": {
+		"mat_4-basic": {
+			"version": 2,
+			"activeRootGraph": "material",
+			"nodes": [
+				{
+					"id": "geo/time-1773372379325",
+					"type": "geo/time",
+					"position": {
+						"x": -629.6646199110978,
+						"y": 132.77241839057913
+					},
+					"selected": false,
+					"data": {
+						"type": "geo/time",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 51,
+						"height": 76
+					},
+					"dragging": false
+				},
+				{
+					"id": "noise/fractal_noise_float-1773372389185",
+					"type": "noise/fractal_noise_float",
+					"position": {
+						"x": -108.19329226448991,
+						"y": 204.01144901950525
+					},
+					"selected": false,
+					"data": {
+						"type": "noise/fractal_noise_float",
+						"values": {
+							"lacunarity": 3,
+							"diminish": 0.4
+						},
+						"connected": {}
+					},
+					"measured": {
+						"width": 155,
+						"height": 164
+					},
+					"dragging": false
+				},
+				{
+					"id": "geo/normalWorld-1773372396270",
+					"type": "geo/normalWorld",
+					"position": {
+						"x": -631.6394825210048,
+						"y": -67.32705910748909
+					},
+					"selected": false,
+					"data": {
+						"type": "geo/normalWorld",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 117,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/multiOp-1773372424142",
+					"type": "math/multiOp",
+					"position": {
+						"x": -396.8985991102374,
+						"y": 8.110749830457827
+					},
+					"selected": false,
+					"data": {
+						"type": "math/multiOp",
+						"values": {},
+						"connected": {},
+						"operations": [
+							{
+								"id": "op_1773372424142_1471",
+								"op": "add"
+							}
+						]
+					},
+					"measured": {
+						"width": 167,
+						"height": 156
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/mix-1773372441343",
+					"type": "math/mix",
+					"position": {
+						"x": 163.707215774629,
+						"y": 89.46017629226604
+					},
+					"selected": false,
+					"data": {
+						"type": "math/mix",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 69,
+						"height": 120
+					},
+					"dragging": false
+				},
+				{
+					"id": "const/color-1773372452446",
+					"type": "const/color",
+					"position": {
+						"x": -91.240461081968,
+						"y": -181.34711479088952
+					},
+					"selected": false,
+					"data": {
+						"type": "const/color",
+						"values": {
+							"value": "#00a9ff"
+						},
+						"connected": {}
+					},
+					"measured": {
+						"width": 93,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "const/color-1773372478735",
+					"type": "const/color",
+					"position": {
+						"x": -98.77050453829891,
+						"y": -0.4595856112190475
+					},
+					"selected": false,
+					"data": {
+						"type": "const/color",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 93,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "root-material",
+					"type": "material/basic",
+					"position": {
+						"x": 529.8544423320919,
+						"y": 177.33904317581556
+					},
+					"data": {
+						"values": {
+							"transparent": true
+						},
+						"connected": {},
+						"type": "material/basic",
+						"activeInputs": [
+							"colorNode",
+							"opacityNode",
+							"transparent",
+							"depthWrite"
+						]
+					},
+					"measured": {
+						"width": 142,
+						"height": 142
+					},
+					"selected": false,
+					"dragging": false
+				},
+				{
+					"id": "math/multiOp-1773372904506",
+					"type": "math/multiOp",
+					"position": {
+						"x": 152.55183728862042,
+						"y": 260.6341647070865
+					},
+					"selected": false,
+					"data": {
+						"type": "math/multiOp",
+						"values": {
+							"op_op_1773372904506_8391_b": 0.6
+						},
+						"connected": {},
+						"operations": [
+							{
+								"id": "op_1773372904506_8391",
+								"op": "add"
+							}
+						]
+					},
+					"measured": {
+						"width": 207,
+						"height": 156
+					},
+					"dragging": false
+				}
+			],
+			"edges": [
+				{
+					"id": "edge-geo/normalWorld-1773372396270-out-math/multiOp-1773372424142-op_op_1773372424142_1471_a",
+					"source": "geo/normalWorld-1773372396270",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773372424142",
+					"targetHandle": "op_op_1773372424142_1471_a"
+				},
+				{
+					"source": "math/multiOp-1773372424142",
+					"sourceHandle": "out",
+					"target": "noise/fractal_noise_float-1773372389185",
+					"targetHandle": "position",
+					"id": "xy-edge__math/multiOp-1773372424142out-noise/fractal_noise_float-1773372389185position"
+				},
+				{
+					"source": "noise/fractal_noise_float-1773372389185",
+					"sourceHandle": "out",
+					"target": "math/mix-1773372441343",
+					"targetHandle": "t",
+					"id": "xy-edge__noise/fractal_noise_float-1773372389185out-math/mix-1773372441343t"
+				},
+				{
+					"source": "const/color-1773372452446",
+					"sourceHandle": "out",
+					"target": "math/mix-1773372441343",
+					"targetHandle": "a",
+					"id": "xy-edge__const/color-1773372452446out-math/mix-1773372441343a"
+				},
+				{
+					"source": "const/color-1773372478735",
+					"sourceHandle": "out",
+					"target": "math/mix-1773372441343",
+					"targetHandle": "b",
+					"id": "xy-edge__const/color-1773372478735out-math/mix-1773372441343b"
+				},
+				{
+					"source": "geo/time-1773372379325",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773372424142",
+					"targetHandle": "op_op_1773372424142_1471_b",
+					"id": "xy-edge__geo/time-1773372379325out-math/multiOp-1773372424142op_op_1773372424142_1471_b"
+				},
+				{
+					"source": "math/mix-1773372441343",
+					"sourceHandle": "out",
+					"target": "root-material",
+					"targetHandle": "colorNode",
+					"id": "xy-edge__math/mix-1773372441343out-root-materialcolorNode"
+				},
+				{
+					"id": "edge-noise/fractal_noise_float-1773372389185-out-math/multiOp-1773372904506-op_op_1773372904506_8391_a",
+					"source": "noise/fractal_noise_float-1773372389185",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773372904506",
+					"targetHandle": "op_op_1773372904506_8391_a"
+				},
+				{
+					"source": "math/multiOp-1773372904506",
+					"sourceHandle": "out",
+					"target": "root-material",
+					"targetHandle": "opacityNode",
+					"id": "xy-edge__math/multiOp-1773372904506out-root-materialopacityNode"
+				}
+			],
+			"postNodes": [
+				{
+					"id": "post-input",
+					"type": "post/input",
+					"position": {
+						"x": -400,
+						"y": 200
+					},
+					"data": {
+						"type": "post/input",
+						"values": {},
+						"connected": {}
+					}
+				},
+				{
+					"id": "post-output",
+					"type": "post/output",
+					"position": {
+						"x": 400,
+						"y": 200
+					},
+					"data": {
+						"type": "post/output",
+						"values": {},
+						"connected": {}
+					}
+				}
+			],
+			"postEdges": [],
+			"globals": [],
+			"subgraphs": {},
+			"codeNodes": {},
+			"previewSettings": {
+				"mesh": "sphere",
+				"isInstanced": false,
+				"instanceCount": 100,
+				"geometryScript": "",
+				"codeMode": "generated",
+				"customCode": "",
+				"environment": "none",
+				"environmentIntensity": 1,
+				"showEnvironmentBackground": false,
+				"showBackdrop": false,
+				"showGrid": true,
+				"postEnabled": true,
+				"config": {
+					"width": 1.5,
+					"height": 1.5,
+					"depth": 1.5,
+					"widthSegments": 1,
+					"heightSegments": 1,
+					"depthSegments": 1,
+					"radius": 1,
+					"widthSegmentsSphere": 32,
+					"heightSegmentsSphere": 16,
+					"radiusTorus": 0.8,
+					"tube": 0.3,
+					"radialSegments": 16,
+					"tubularSegments": 32,
+					"widthPlane": 1,
+					"heightPlane": 1,
+					"widthSegmentsPlane": 1,
+					"heightSegmentsPlane": 1,
+					"radiusTop": 1,
+					"radiusBottom": 1,
+					"heightCylinder": 1,
+					"radialSegmentsCylinder": 20,
+					"heightSegmentsCylinder": 20,
+					"openEnded": true
+				}
+			}
+		},
+		"mat_3-phong": {
+			"version": 2,
+			"activeRootGraph": "material",
+			"nodes": [
+				{
+					"id": "root-material",
+					"type": "material/phong",
+					"position": {
+						"x": 308.8327848876186,
+						"y": -19.015027519991655
+					},
+					"data": {
+						"values": {
+							"transparent": false,
+							"colorNode": "#da3939"
+						},
+						"connected": {},
+						"type": "material/phong",
+						"activeInputs": [
+							"colorNode",
+							"positionNode",
+							"opacityNode"
+						]
+					},
+					"measured": {
+						"width": 150,
+						"height": 120
+					},
+					"selected": false,
+					"dragging": false
+				},
+				{
+					"id": "noise/worley_float-1773373082664",
+					"type": "noise/worley_float",
+					"position": {
+						"x": -181.58472656498714,
+						"y": 26.292216277347077
+					},
+					"selected": false,
+					"data": {
+						"type": "noise/worley_float",
+						"values": {
+							"jitter": 1
+						},
+						"connected": {}
+					},
+					"measured": {
+						"width": 153,
+						"height": 98
+					},
+					"dragging": false
+				},
+				{
+					"id": "geo/positionWorld-1773373139191",
+					"type": "geo/positionWorld",
+					"position": {
+						"x": -921.2857614166131,
+						"y": -212.74155272575058
+					},
+					"selected": false,
+					"data": {
+						"type": "geo/positionWorld",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 122,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/multiOp-1773373148510",
+					"type": "math/multiOp",
+					"position": {
+						"x": -709.2537795020332,
+						"y": -81.61272195352191
+					},
+					"selected": false,
+					"data": {
+						"type": "math/multiOp",
+						"values": {
+							"op_op_1773373148510_2336_b": 3.7
+						},
+						"connected": {},
+						"operations": [
+							{
+								"id": "op_1773373148510_2336",
+								"op": "mul"
+							}
+						]
+					},
+					"measured": {
+						"width": 207,
+						"height": 156
+					},
+					"dragging": false
+				},
+				{
+					"id": "geo/time-1773373271419",
+					"type": "geo/time",
+					"position": {
+						"x": -608.432592117811,
+						"y": 138.10129480844304
+					},
+					"selected": false,
+					"data": {
+						"type": "geo/time",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 51,
+						"height": 76
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/multiOp-1773373290147",
+					"type": "math/multiOp",
+					"position": {
+						"x": -402.03009870276213,
+						"y": -2.588146485305984
+					},
+					"selected": false,
+					"data": {
+						"type": "math/multiOp",
+						"values": {},
+						"connected": {},
+						"operations": [
+							{
+								"id": "op_1773373290147_7719",
+								"op": "add"
+							}
+						]
+					},
+					"measured": {
+						"width": 167,
+						"height": 156
+					},
+					"dragging": false
+				},
+				{
+					"id": "const/color-1773373398767",
+					"type": "const/color",
+					"position": {
+						"x": -132.51582820969844,
+						"y": 145.92580777895932
+					},
+					"selected": false,
+					"data": {
+						"type": "const/color",
+						"values": {
+							"value": "#29fcff"
+						},
+						"connected": {}
+					},
+					"measured": {
+						"width": 93,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/multiOp-1773373409199",
+					"type": "math/multiOp",
+					"position": {
+						"x": 30.400676090869368,
+						"y": 104.03555298707752
+					},
+					"selected": false,
+					"data": {
+						"type": "math/multiOp",
+						"values": {},
+						"connected": {},
+						"operations": [
+							{
+								"id": "op_1773373409199_9297",
+								"op": "add"
+							}
+						]
+					},
+					"measured": {
+						"width": 167,
+						"height": 156
+					},
+					"dragging": false
+				}
+			],
+			"edges": [
+				{
+					"id": "edge-geo/positionWorld-1773373139191-out-math/multiOp-1773373148510-op_op_1773373148510_2336_a",
+					"source": "geo/positionWorld-1773373139191",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773373148510",
+					"targetHandle": "op_op_1773373148510_2336_a"
+				},
+				{
+					"id": "edge-math/multiOp-1773373148510-out-math/multiOp-1773373290147-op_op_1773373290147_7719_a",
+					"source": "math/multiOp-1773373148510",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773373290147",
+					"targetHandle": "op_op_1773373290147_7719_a"
+				},
+				{
+					"source": "math/multiOp-1773373290147",
+					"sourceHandle": "out",
+					"target": "noise/worley_float-1773373082664",
+					"targetHandle": "position",
+					"id": "xy-edge__math/multiOp-1773373290147out-noise/worley_float-1773373082664position"
+				},
+				{
+					"source": "geo/time-1773373271419",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773373290147",
+					"targetHandle": "op_op_1773373290147_7719_b",
+					"id": "xy-edge__geo/time-1773373271419out-math/multiOp-1773373290147op_op_1773373290147_7719_b"
+				},
+				{
+					"id": "edge-noise/worley_float-1773373082664-out-math/multiOp-1773373409199-op_op_1773373409199_9297_a",
+					"source": "noise/worley_float-1773373082664",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773373409199",
+					"targetHandle": "op_op_1773373409199_9297_a"
+				},
+				{
+					"source": "const/color-1773373398767",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773373409199",
+					"targetHandle": "op_op_1773373409199_9297_b",
+					"id": "xy-edge__const/color-1773373398767out-math/multiOp-1773373409199op_op_1773373409199_9297_b"
+				},
+				{
+					"source": "math/multiOp-1773373409199",
+					"sourceHandle": "out",
+					"target": "root-material",
+					"targetHandle": "colorNode",
+					"id": "xy-edge__math/multiOp-1773373409199out-root-materialcolorNode"
+				}
+			],
+			"postNodes": [
+				{
+					"id": "post-input",
+					"type": "post/input",
+					"position": {
+						"x": -400,
+						"y": 200
+					},
+					"data": {
+						"type": "post/input",
+						"values": {},
+						"connected": {}
+					}
+				},
+				{
+					"id": "post-output",
+					"type": "post/output",
+					"position": {
+						"x": 400,
+						"y": 200
+					},
+					"data": {
+						"type": "post/output",
+						"values": {},
+						"connected": {}
+					}
+				}
+			],
+			"postEdges": [],
+			"globals": [],
+			"subgraphs": {},
+			"codeNodes": {},
+			"previewSettings": {
+				"mesh": "sphere",
+				"isInstanced": false,
+				"instanceCount": 100,
+				"geometryScript": "",
+				"codeMode": "generated",
+				"customCode": "",
+				"environment": "none",
+				"environmentIntensity": 1,
+				"showEnvironmentBackground": false,
+				"showBackdrop": false,
+				"showGrid": true,
+				"postEnabled": true,
+				"config": {
+					"width": 1.5,
+					"height": 1.5,
+					"depth": 1.5,
+					"widthSegments": 1,
+					"heightSegments": 1,
+					"depthSegments": 1,
+					"radius": 1,
+					"widthSegmentsSphere": 32,
+					"heightSegmentsSphere": 16,
+					"radiusTorus": 0.8,
+					"tube": 0.3,
+					"radialSegments": 16,
+					"tubularSegments": 32,
+					"widthPlane": 1,
+					"heightPlane": 1,
+					"widthSegmentsPlane": 1,
+					"heightSegmentsPlane": 1,
+					"radiusTop": 1,
+					"radiusBottom": 1,
+					"heightCylinder": 1,
+					"radialSegmentsCylinder": 20,
+					"heightSegmentsCylinder": 20,
+					"openEnded": true
+				}
+			}
+		},
+		"mat_1-physical": {
+			"version": 2,
+			"activeRootGraph": "material",
+			"nodes": [
+				{
+					"id": "geo/positionWorld-1773373497071",
+					"type": "geo/positionWorld",
+					"position": {
+						"x": -479.37891233902394,
+						"y": -28.03331550830903
+					},
+					"selected": false,
+					"data": {
+						"type": "geo/positionWorld",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 122,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "noise/tri_noise_3d-1773373514019",
+					"type": "noise/tri_noise_3d",
+					"position": {
+						"x": -213.9845343995448,
+						"y": 131.5974524617551
+					},
+					"selected": false,
+					"data": {
+						"type": "noise/tri_noise_3d",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 111,
+						"height": 120
+					},
+					"dragging": false
+				},
+				{
+					"id": "root-material",
+					"type": "material/standard",
+					"position": {
+						"x": 0,
+						"y": 0
+					},
+					"data": {
+						"values": {
+							"colorNode": "#ffffff",
+							"roughnessNode": 1
+						},
+						"connected": {},
+						"type": "material/standard",
+						"activeInputs": [
+							"colorNode",
+							"positionNode",
+							"roughnessNode",
+							"metalnessNode"
+						]
+					},
+					"measured": {
+						"width": 170,
+						"height": 142
+					},
+					"selected": true
+				}
+			],
+			"edges": [
+				{
+					"source": "geo/positionWorld-1773373497071",
+					"sourceHandle": "out",
+					"target": "noise/tri_noise_3d-1773373514019",
+					"targetHandle": "position",
+					"id": "xy-edge__geo/positionWorld-1773373497071out-noise/tri_noise_3d-1773373514019position"
+				},
+				{
+					"source": "noise/tri_noise_3d-1773373514019",
+					"sourceHandle": "out",
+					"target": "root-material",
+					"targetHandle": "colorNode",
+					"id": "xy-edge__noise/tri_noise_3d-1773373514019out-root-materialcolorNode"
+				}
+			],
+			"postNodes": [
+				{
+					"id": "post-input",
+					"type": "post/input",
+					"position": {
+						"x": -400,
+						"y": 200
+					},
+					"data": {
+						"type": "post/input",
+						"values": {},
+						"connected": {}
+					}
+				},
+				{
+					"id": "post-output",
+					"type": "post/output",
+					"position": {
+						"x": 400,
+						"y": 200
+					},
+					"data": {
+						"type": "post/output",
+						"values": {},
+						"connected": {}
+					}
+				}
+			],
+			"postEdges": [],
+			"globals": [],
+			"subgraphs": {},
+			"codeNodes": {},
+			"previewSettings": {
+				"mesh": "sphere",
+				"isInstanced": false,
+				"instanceCount": 100,
+				"geometryScript": "",
+				"codeMode": "generated",
+				"customCode": "",
+				"environment": "none",
+				"environmentIntensity": 1,
+				"showEnvironmentBackground": false,
+				"showBackdrop": false,
+				"showGrid": true,
+				"postEnabled": true,
+				"config": {
+					"width": 1.5,
+					"height": 1.5,
+					"depth": 1.5,
+					"widthSegments": 1,
+					"heightSegments": 1,
+					"depthSegments": 1,
+					"radius": 1,
+					"widthSegmentsSphere": 32,
+					"heightSegmentsSphere": 16,
+					"radiusTorus": 0.8,
+					"tube": 0.3,
+					"radialSegments": 16,
+					"tubularSegments": 32,
+					"widthPlane": 1,
+					"heightPlane": 1,
+					"widthSegmentsPlane": 1,
+					"heightSegmentsPlane": 1,
+					"radiusTop": 1,
+					"radiusBottom": 1,
+					"heightCylinder": 1,
+					"radialSegmentsCylinder": 20,
+					"heightSegmentsCylinder": 20,
+					"openEnded": true
+				}
+			}
+		},
+		"mat_2-standard": {
+			"version": 2,
+			"activeRootGraph": "material",
+			"nodes": [
+				{
+					"id": "noise/tri_noise_3d-1773373560195",
+					"type": "noise/tri_noise_3d",
+					"position": {
+						"x": -571.5500459485202,
+						"y": -85.26775870089736
+					},
+					"selected": false,
+					"data": {
+						"type": "noise/tri_noise_3d",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 111,
+						"height": 120
+					},
+					"dragging": false
+				},
+				{
+					"id": "geo/normalWorld-1773373569960",
+					"type": "geo/normalWorld",
+					"position": {
+						"x": -546.0657202943696,
+						"y": 311.98499287363376
+					},
+					"selected": false,
+					"data": {
+						"type": "geo/normalWorld",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 117,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/mix-1773373573981",
+					"type": "math/mix",
+					"position": {
+						"x": -214.4059798432642,
+						"y": -2.1649144661627204
+					},
+					"selected": false,
+					"data": {
+						"type": "math/mix",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 69,
+						"height": 120
+					},
+					"dragging": false
+				},
+				{
+					"id": "geo/positionWorld-1773373584631",
+					"type": "geo/positionWorld",
+					"position": {
+						"x": -1433.94251744813,
+						"y": -135.01461143841146
+					},
+					"selected": false,
+					"data": {
+						"type": "geo/positionWorld",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 122,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "const/color-1773373695600",
+					"type": "const/color",
+					"position": {
+						"x": -891.4966712873143,
+						"y": 351.8835858072191
+					},
+					"selected": false,
+					"data": {
+						"type": "const/color",
+						"values": {
+							"value": "#a4fb47"
+						},
+						"connected": {}
+					},
+					"measured": {
+						"width": 93,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "noise/fractal_noise_float-1773373726185",
+					"type": "noise/fractal_noise_float",
+					"position": {
+						"x": -931.6729541310845,
+						"y": -4.0550609248271385
+					},
+					"selected": false,
+					"data": {
+						"type": "noise/fractal_noise_float",
+						"values": {
+							"octaves": 4,
+							"lacunarity": 2
+						},
+						"connected": {}
+					},
+					"measured": {
+						"width": 155,
+						"height": 164
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/mix-1773373826479",
+					"type": "math/mix",
+					"position": {
+						"x": -521.8025735699398,
+						"y": 154.9725741278549
+					},
+					"selected": false,
+					"data": {
+						"type": "math/mix",
+						"values": {
+							"a": 0,
+							"b": 1,
+							"t": 0.5
+						},
+						"connected": {}
+					},
+					"measured": {
+						"width": 69,
+						"height": 120
+					},
+					"dragging": false
+				},
+				{
+					"id": "const/color-1773373842192",
+					"type": "const/color",
+					"position": {
+						"x": -897.2658844184299,
+						"y": 177.1817694642336
+					},
+					"selected": false,
+					"data": {
+						"type": "const/color",
+						"values": {
+							"value": "#044813"
+						},
+						"connected": {}
+					},
+					"measured": {
+						"width": 93,
+						"height": 142
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/clamp-1773373874892",
+					"type": "math/clamp",
+					"position": {
+						"x": -317.6997222284098,
+						"y": 159.16643724155531
+					},
+					"selected": false,
+					"data": {
+						"type": "math/clamp",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 68,
+						"height": 120
+					}
+				},
+				{
+					"id": "math/clamp-1773373914936",
+					"type": "math/clamp",
+					"position": {
+						"x": -700.7459282048039,
+						"y": 32.28547527202021
+					},
+					"selected": false,
+					"data": {
+						"type": "math/clamp",
+						"values": {},
+						"connected": {}
+					},
+					"measured": {
+						"width": 68,
+						"height": 120
+					},
+					"dragging": false
+				},
+				{
+					"id": "math/multiOp-1773374085600",
+					"type": "math/multiOp",
+					"position": {
+						"x": -1181.154599038152,
+						"y": 57.500254462638964
+					},
+					"selected": false,
+					"data": {
+						"type": "math/multiOp",
+						"values": {
+							"op_op_1773374085601_8822_b": 66.7
+						},
+						"connected": {},
+						"operations": [
+							{
+								"id": "op_1773374085601_8822",
+								"op": "mul"
+							}
+						]
+					},
+					"measured": {
+						"width": 212,
+						"height": 156
+					}
+				},
+				{
+					"id": "root-material",
+					"type": "material/standard",
+					"position": {
+						"x": 38.21195091405497,
+						"y": -99.60597545702748
+					},
+					"data": {
+						"values": {
+							"roughnessNode": 1
+						},
+						"connected": {},
+						"type": "material/standard",
+						"activeInputs": [
+							"colorNode",
+							"positionNode",
+							"roughnessNode",
+							"metalnessNode"
+						]
+					},
+					"measured": {
+						"width": 170,
+						"height": 142
+					},
+					"selected": false,
+					"dragging": false
+				}
+			],
+			"edges": [
+				{
+					"source": "noise/tri_noise_3d-1773373560195",
+					"sourceHandle": "out",
+					"target": "math/mix-1773373573981",
+					"targetHandle": "a",
+					"id": "xy-edge__noise/tri_noise_3d-1773373560195out-math/mix-1773373573981a"
+				},
+				{
+					"source": "geo/positionWorld-1773373584631",
+					"sourceHandle": "out",
+					"target": "noise/tri_noise_3d-1773373560195",
+					"targetHandle": "position",
+					"id": "xy-edge__geo/positionWorld-1773373584631out-noise/tri_noise_3d-1773373560195position"
+				},
+				{
+					"source": "math/mix-1773373573981",
+					"sourceHandle": "out",
+					"target": "root-material",
+					"targetHandle": "colorNode",
+					"id": "xy-edge__math/mix-1773373573981out-root-materialcolorNode"
+				},
+				{
+					"id": "edge-const/color-1773373695600-out-math/mix-1773373826479-b",
+					"source": "const/color-1773373695600",
+					"sourceHandle": "out",
+					"target": "math/mix-1773373826479",
+					"targetHandle": "b"
+				},
+				{
+					"source": "math/mix-1773373826479",
+					"sourceHandle": "out",
+					"target": "math/mix-1773373573981",
+					"targetHandle": "b",
+					"id": "xy-edge__math/mix-1773373826479out-math/mix-1773373573981b"
+				},
+				{
+					"id": "edge-const/color-1773373842192-out-math/mix-1773373826479-a",
+					"source": "const/color-1773373842192",
+					"sourceHandle": "out",
+					"target": "math/mix-1773373826479",
+					"targetHandle": "a"
+				},
+				{
+					"source": "geo/normalWorld-1773373569960",
+					"sourceHandle": "y",
+					"target": "math/clamp-1773373874892",
+					"targetHandle": "x",
+					"id": "xy-edge__geo/normalWorld-1773373569960y-math/clamp-1773373874892x"
+				},
+				{
+					"source": "math/clamp-1773373874892",
+					"sourceHandle": "out",
+					"target": "math/mix-1773373573981",
+					"targetHandle": "t",
+					"id": "xy-edge__math/clamp-1773373874892out-math/mix-1773373573981t"
+				},
+				{
+					"source": "noise/fractal_noise_float-1773373726185",
+					"sourceHandle": "out",
+					"target": "math/clamp-1773373914936",
+					"targetHandle": "x",
+					"id": "xy-edge__noise/fractal_noise_float-1773373726185out-math/clamp-1773373914936x"
+				},
+				{
+					"source": "math/clamp-1773373914936",
+					"sourceHandle": "out",
+					"target": "math/mix-1773373826479",
+					"targetHandle": "t",
+					"id": "xy-edge__math/clamp-1773373914936out-math/mix-1773373826479t"
+				},
+				{
+					"id": "edge-geo/positionWorld-1773373584631-out-math/multiOp-1773374085600-op_op_1773374085601_8822_a",
+					"source": "geo/positionWorld-1773373584631",
+					"sourceHandle": "out",
+					"target": "math/multiOp-1773374085600",
+					"targetHandle": "op_op_1773374085601_8822_a"
+				},
+				{
+					"source": "math/multiOp-1773374085600",
+					"sourceHandle": "out",
+					"target": "noise/fractal_noise_float-1773373726185",
+					"targetHandle": "position",
+					"id": "xy-edge__math/multiOp-1773374085600out-noise/fractal_noise_float-1773373726185position"
+				}
+			],
+			"postNodes": [
+				{
+					"id": "post-input",
+					"type": "post/input",
+					"position": {
+						"x": -400,
+						"y": 200
+					},
+					"data": {
+						"type": "post/input",
+						"values": {},
+						"connected": {}
+					}
+				},
+				{
+					"id": "post-output",
+					"type": "post/output",
+					"position": {
+						"x": 400,
+						"y": 200
+					},
+					"data": {
+						"type": "post/output",
+						"values": {},
+						"connected": {}
+					}
+				}
+			],
+			"postEdges": [],
+			"globals": [],
+			"subgraphs": {},
+			"codeNodes": {},
+			"previewSettings": {
+				"mesh": "sphere",
+				"isInstanced": false,
+				"instanceCount": 100,
+				"geometryScript": "",
+				"codeMode": "generated",
+				"customCode": "",
+				"environment": "none",
+				"environmentIntensity": 1,
+				"showEnvironmentBackground": false,
+				"showBackdrop": false,
+				"showGrid": true,
+				"postEnabled": true,
+				"config": {
+					"width": 1.5,
+					"height": 1.5,
+					"depth": 1.5,
+					"widthSegments": 1,
+					"heightSegments": 1,
+					"depthSegments": 1,
+					"radius": 1,
+					"widthSegmentsSphere": 32,
+					"heightSegmentsSphere": 16,
+					"radiusTorus": 0.8,
+					"tube": 0.3,
+					"radialSegments": 16,
+					"tubularSegments": 32,
+					"widthPlane": 1,
+					"heightPlane": 1,
+					"widthSegmentsPlane": 1,
+					"heightSegmentsPlane": 1,
+					"radiusTop": 1,
+					"radiusBottom": 1,
+					"heightCylinder": 1,
+					"radialSegmentsCylinder": 20,
+					"heightSegmentsCylinder": 20,
+					"openEnded": true
+				}
+			}
+		}
+	}
+}

BIN
examples/textures/equirectangular/monochrome_studio_02_1k.hdr


+ 401 - 0
examples/webgpu_tsl_graph.html

@@ -0,0 +1,401 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - tsl graph</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="example.css">
+		<style>
+			.dg .property-name {
+				width: 20% !important;
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>TSL Graph</span>
+			</div>
+
+			<small>
+				TSL Graph Addons - <a href="https://www.tsl-graph.xyz/" target="_blank" rel="noopener">www.tsl-graph.xyz</a>
+			</small>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three/webgpu';
+
+			import { Fn, abs, fract, fwidth, length, max, saturate, smoothstep, vec3, vec4, positionWorld, float } from 'three/tsl';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			import { Inspector } from 'three/addons/inspector/Inspector.js';
+
+			import { TSLGraphLoader } from 'three/addons/inspector/addons/tsl-graph/TSLGraphLoader.js';
+			import { TSLGraphEditor } from 'three/addons/inspector/addons/tsl-graph/TSLGraphEditor.js';
+
+			let camera, scene, renderer;
+			let controls;
+			let prefab;
+
+			init();
+
+			async function initTSLGraph() {
+
+				// TSL Graph Editor
+
+				const tslGraph = new TSLGraphEditor();
+
+				renderer.inspector.addTab( tslGraph );
+				renderer.inspector.setActiveTab( tslGraph );
+
+				// Create Materials
+
+				const m1 = new THREE.MeshPhysicalNodeMaterial();
+				m1.userData.graphId = 'mat_1-physical';
+
+				const m2 = new THREE.MeshStandardNodeMaterial();
+				m2.userData.graphId = 'mat_2-standard';
+
+				const m3 = new THREE.MeshPhongNodeMaterial();
+				m3.userData.graphId = 'mat_3-phong';
+
+				const m4 = new THREE.MeshBasicNodeMaterial();
+				m4.userData.graphId = 'mat_4-basic';
+
+				const materials = [ m1, m2, m3, m4 ];
+
+				for ( let i = 0; i < materials.length; i ++ ) {
+
+					createShaderBall( materials[ i ], new THREE.Vector3( ( i & 1 ) * 4 - 2, 0, ( i & 2 ) * 2 - 2 ) );
+
+				}
+
+				// Initialize
+
+				// Load and apply TSL Graph from a file or from Local Storage if exists
+				// Every time a TSL Graph is changed, it will be stored in the local storage
+
+				if ( tslGraph.hasGraphs ) {
+
+					tslGraph.apply( scene );
+
+				} else {
+
+					// Load a TSL Graph from a file
+
+					const tslLoader = new TSLGraphLoader();
+					const applier = await tslLoader.setPath( './shaders/' ).loadAsync( 'tsl-graphs.json' );
+
+					applier.apply( scene );
+
+				}
+
+				// Picker a Material
+
+				let boundingBox = null;
+
+				const raycaster = new THREE.Raycaster();
+				const pointer = new THREE.Vector2();
+
+				function removeBoundingBox() {
+
+					scene.remove( boundingBox );
+					boundingBox.dispose();
+
+				}
+
+				tslGraph.addEventListener( 'change', ( { material } ) => {
+
+					if ( material === null && boundingBox ) {
+
+						removeBoundingBox();
+
+						boundingBox = null;
+
+					}
+
+				} );
+
+				tslGraph.addEventListener( 'remove', ( { graphId } ) => {
+
+					scene.traverse( ( object ) => {
+
+						if ( object.material && object.material.userData && object.material.userData.graphId === graphId ) {
+
+							tslGraph.restoreMaterial( object.material );
+
+						}
+
+					} );
+
+				} );
+
+				const pointerDownPosition = new THREE.Vector2();
+
+				renderer.domElement.addEventListener( 'pointerdown', ( e ) => {
+
+					pointerDownPosition.set( e.clientX, e.clientY );
+
+				} );
+
+				renderer.domElement.addEventListener( 'pointerup', ( e ) => {
+
+					if ( pointerDownPosition.distanceTo( pointer.set( e.clientX, e.clientY ) ) > 2 ) return;
+
+					const rect = renderer.domElement.getBoundingClientRect();
+					pointer.x = ( ( e.clientX - rect.left ) / rect.width ) * 2 - 1;
+					pointer.y = - ( ( e.clientY - rect.top ) / rect.height ) * 2 + 1;
+
+					raycaster.setFromCamera( pointer, camera );
+
+					const intersects = raycaster.intersectObjects( scene.children, true );
+
+					let graphMaterial = null;
+
+					if ( intersects.length > 0 ) {
+
+						for ( const intersect of intersects ) {
+
+							const object = intersect.object;
+							const material = object.material;
+
+							if ( material.userData && material.userData.graphId ) {
+
+								if ( boundingBox ) {
+
+									removeBoundingBox();
+
+								}
+
+								boundingBox = new THREE.BoxHelper( object, 0xffff00 );
+								scene.add( boundingBox );
+
+								graphMaterial = material;
+
+							}
+
+							if ( object.isMesh || object.isSprite ) {
+
+								break;
+
+							}
+
+						}
+
+					}
+
+					tslGraph.setMaterial( graphMaterial );
+
+				} );
+
+			}
+
+			async function init() {
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.toneMappingExposure = .9;
+				renderer.inspector = new Inspector();
+				renderer.setAnimationLoop( render );
+				document.body.appendChild( renderer.domElement );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 200 );
+				camera.position.set( 3, 5, 8 );
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x444444 );
+
+				await renderer.init();
+
+				// Ground plane
+
+				const plane = new THREE.Mesh( new THREE.CircleGeometry( 40 ), createGroudMaterial() );
+				plane.rotation.x = - Math.PI / 2;
+				plane.renderOrder = - 1;
+				scene.add( plane );
+
+				//
+
+				controls = new OrbitControls( camera );
+				controls.connect( renderer.domElement );
+				controls.enableDamping = true;
+				controls.minDistance = 2;
+				controls.maxDistance = 40;
+
+				//
+
+				const environment = await new HDRLoader().setPath( 'textures/equirectangular/' ).loadAsync( 'monochrome_studio_02_1k.hdr' );
+				environment.mapping = THREE.EquirectangularReflectionMapping;
+
+				const light = new THREE.DirectionalLight( 0xffffff, 1 );
+				light.position.set( 1, 1, 1 );
+				scene.add( light );
+
+				scene.environment = environment;
+
+				prefab = ( await new GLTFLoader().loadAsync( './models/gltf/ShaderBall.glb' ) ).scene;
+
+				await initTSLGraph();
+
+				initGUI();
+
+				// Events
+
+				resize();
+
+				window.addEventListener( 'resize', resize );
+				renderer.inspector.addEventListener( 'resize', resize );
+
+			}
+
+			async function createShaderBall( material, position = new THREE.Vector3() ) {
+
+				const model = prefab.clone();
+				model.position.copy( position );
+				scene.add( model );
+
+				//
+
+				const calibrationMesh = model.getObjectByName( 'Calibration_Mesh' );
+				calibrationMesh.material = material;
+
+				const previewMesh = model.getObjectByName( 'Preview_Mesh' );
+				previewMesh.material = material;
+
+				calibrationMesh.renderOrder = 1;
+				previewMesh.renderOrder = 2;
+
+			}
+
+			function initGUI() {
+
+				const gui = renderer.inspector.createParameters( 'Shader Ball' );
+
+				const API = {
+					showCalibrationMesh: true,
+					showPreviewMesh: true
+				};
+
+				gui.add( API, 'showCalibrationMesh' )
+					.name( 'Calibration Mesh' )
+					.onChange( function ( value ) {
+
+						setVisibility( 'Calibration_Mesh', value );
+
+					} );
+
+				gui.add( API, 'showPreviewMesh' )
+					.name( 'Preview Mesh' )
+					.onChange( function ( value ) {
+
+						setVisibility( 'Preview_Mesh', value );
+
+					} );
+
+				renderer.inspector.hide();
+
+			}
+
+			function resize() {
+
+				const size = renderer.inspector.getSize();
+				const width = window.innerWidth - size.width;
+				const height = window.innerHeight - size.height;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+
+			}
+
+			function render() {
+
+				if ( controls ) controls.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+			//
+
+			function setVisibility( name, visible ) {
+
+				scene.traverse( function ( node ) {
+
+					if ( node.isMesh ) {
+
+						if ( node.name == name ) node.visible = visible;
+
+					}
+
+				} );
+
+			}
+
+			function createGroudMaterial() {
+
+				const material = new THREE.MeshBasicNodeMaterial();
+
+				const grid = Fn( ( [ coord, lineWidth = float( 0.01 ), dotSize = float( 0.03 ) ] ) => {
+
+					// https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8
+
+					const g = fract( coord );
+					const fw = fwidth( coord );
+					const gx = abs( g.x.sub( 0.5 ) );
+					const gy = abs( g.y.sub( 0.5 ) );
+
+					const lineX = saturate( lineWidth.sub( gx ).div( fw.x ).add( 0.5 ) );
+					const lineY = saturate( lineWidth.sub( gy ).div( fw.y ).add( 0.5 ) );
+					const lines = max( lineX, lineY );
+
+					const squareDist = max( gx, gy );
+					const aa = max( fw.x, fw.y );
+					const dots = smoothstep( dotSize.add( aa ), dotSize.sub( aa ), squareDist );
+
+					return max( dots, lines );
+
+				} );
+
+				const fade = Fn( ( [ radius = float( 10.0 ), falloff = float( 1.0 ) ] ) => {
+
+					return smoothstep( radius, radius.sub( falloff ), length( positionWorld ) );
+
+				} );
+
+				const gridColor = vec4( vec3( 0.2 ), 1.0 );
+				const baseColor = vec4( vec3( 0.4 ), 0.0 );
+
+				material.colorNode = grid( positionWorld.xz, 0.007, 0.03 ).mix( baseColor, gridColor ).mul( fade( 30.0, 20.0 ) );
+				material.transparent = true;
+
+				return material;
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 1
src/nodes/utils/Remap.js

@@ -38,7 +38,7 @@ export const remap = /*@__PURE__*/ Fn( ( [ node, inLowNode, inHighNode, outLowNo
  * @param {?Node} [outHighNode=float(1)] - The target upper bound of the range.
  * @returns {Node}
  */
-function remapClamp( node, inLowNode, inHighNode, outLowNode = float( 0 ), outHighNode = float( 1 ) ) {
+export function remapClamp( node, inLowNode, inHighNode, outLowNode = float( 0 ), outHighNode = float( 1 ) ) {
 
 	return remap( node, inLowNode, inHighNode, outLowNode, outHighNode, true );
 

+ 6 - 1
src/renderers/common/InspectorBase.js

@@ -1,15 +1,20 @@
+import { EventDispatcher } from '../../core/EventDispatcher.js';
+
 /**
  * InspectorBase is the base class for all inspectors.
  *
  * @class InspectorBase
+ * @augments EventDispatcher
  */
-class InspectorBase {
+class InspectorBase extends EventDispatcher {
 
 	/**
 	 * Creates a new InspectorBase.
 	 */
 	constructor() {
 
+		super();
+
 		/**
 		 * The renderer associated with this inspector.
 		 *

粤ICP备19079148号