Pārlūkot izejas kodu

TSL: ScriptableNode (#25685)

* CodeNode: Added language and serialize.

* TSL: Renamed inversesqrt -> inverseSqrt, faceforward -> faceForward,

* TSL: Added saturate

* TSL: Different params order if method chaining is used.

* Nodes: Change mix order and added FogNode.mixAssign()

* Node: Ignore private properties on serialization.

* Nodes: Added string, arrayBuffer

* Node: Added .getSerializeChildren()

* Added ScriptableNode

* Added scriptable example and serialization test.

* update style

* cleanup

* cleanup

* Code Based Node moved to /code/

* ScriptableNode: Fix async refresh, added .setLocal(), .getLocal()

* added async scriptable and serialization test

* optional output

* added output example

* ScriptableNode: Move editor property to function call if needed.

* ScriptableValueNode: Added support to more CPU nodes.

* ShaderNode: Reduces the use of unnecessary conversions.

* webgpu_materials: Added CPU parameter as node.

* ScriptableNode: Move THREE, TSL to global environment vars.

* webgpu_materials: set globals

* cleanup

* cleanup

* Revert "cleanup"

This reverts commit 0cf06951c9016829fff6a244d2e5f5a1c827864f.

* Revert "fix conflicting"

This reverts commit baaa14bb5bfcdfcc29d7b8ec5c64b940ec46d8bb, reversing
changes made to 0923c2ec790d82b8c5be2995f634b49b365b08cd.

* PointerLockControls: Refactor class structure. (#25665)

* Bump eslint from 8.35.0 to 8.36.0 (#25666)

Bumps [eslint](https://github.com/eslint/eslint) from 8.35.0 to 8.36.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.35.0...v8.36.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update webgl_loader_md2.html (#25670)

Double index increment preventing all animations to be set in UI.

* Fix the Chinese translation of MeshToonMaterial (#25672)

In the original version, the Chinese translation of MeshToonMaterial documents did not work properly. This update aims to fix this error

* Bump puppeteer-core from 19.7.4 to 19.7.5 (#25671)

Bumps [puppeteer-core](https://github.com/puppeteer/puppeteer) from 19.7.4 to 19.7.5.
- [Release notes](https://github.com/puppeteer/puppeteer/releases)
- [Changelog](https://github.com/puppeteer/puppeteer/blob/main/release-please-config.json)
- [Commits](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.4...puppeteer-core-v19.7.5)

---
updated-dependencies:
- dependency-name: puppeteer-core
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Create renovate.json (#25636)

* Create renovate.json

* Update renovate.json

---------

Co-authored-by: mrdoob <[email protected]>

* Delete dependabot.yml

* Src: export AnimationAction (#25676)

* Fix a document translation omission (#25677)

* Create a translation document of DataArrayTexture

Create a Chinese translation document of DataArrayTexture

* Update DataArrayTexture.html

---------

Co-authored-by: Michael Herzog <[email protected]>

* Delete DataTexture2DArray.html

* CSS2DRenderer: Introduce CSS2DObject.center (#25673)

* CSS2DRenderer: Introduce CSS2DObject.center

* Update css2d_label screenshot

* fix gui

* Vector2: Add `angleTo()`. (#25678)

* GLTFLoader: Prevents set data uri in texture.name. (#25682)

* GLTFLoader: Prevents set data uri in texture.name.

* cleanup

* fix maybe optional value

* cleanup

* Object3D: Honor `up` in `toJSON()` and `ObjectLoader`. (#25687)

* WebGLRenderer: Add `onShaderError()`. (#25679)

* Updated builds.

* 3DMLoader: Clean up (#25423)

* 3DMLoader: Clean up

* Update 3DMLoader.js

* Update 3DMLoader.js

* Revert materials comparison change

* Fix `elements`

* Update 3DMLoader.js

* Update 3DMLoader.js

* Update 3DMLoader.js

---------

Co-authored-by: Michael Herzog <[email protected]>

* Examples: Fix webgl_geometry_spline_editor.html (#25690)

Call render() after detach so that the tooltip disappears when clicking away from the selected helper point, as one would expect.

* TSL: Different params order if method chaining is used. (#25683)

* CodeNode: Added language and serialize.

* TSL: Renamed inversesqrt -> inverseSqrt, faceforward -> faceForward,

* TSL: Added saturate

* TSL: Different params order if method chaining is used.

* Nodes: Change mix order and added FogNode.mixAssign()

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Michael Herzog <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: llzen44 <[email protected]>
Co-authored-by: 幽离 <[email protected]>
Co-authored-by: Don McCurdy <[email protected]>
Co-authored-by: mrdoob <[email protected]>
Co-authored-by: 林炳权 <[email protected]>
Co-authored-by: WestLangley <[email protected]>
Co-authored-by: Levi Pesin <[email protected]>
Co-authored-by: puqeko <[email protected]>
sunag 2 gadi atpakaļ
vecāks
revīzija
911afb2796

+ 8 - 4
examples/jsm/nodes/Nodes.js

@@ -9,12 +9,8 @@ export { default as ArrayUniformNode /* @TODO: arrayUniform */ } from './core/Ar
 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 CodeNode, code, js } from './core/CodeNode.js';
 export { default as ConstNode } from './core/ConstNode.js';
 export { default as ContextNode, context } from './core/ContextNode.js';
-export { default as ExpressionNode, expression } from './core/ExpressionNode.js';
-export { default as FunctionCallNode, call } from './core/FunctionCallNode.js';
-export { default as FunctionNode, func, fn } from './core/FunctionNode.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';
@@ -97,6 +93,14 @@ export { default as PosterizeNode, posterize } from './display/PosterizeNode.js'
 export { default as ToneMappingNode, toneMapping } from './display/ToneMappingNode.js';
 export { default as ViewportNode, viewportCoordinate, viewportResolution, viewportTopLeft, viewportBottomLeft, viewportTopRight, viewportBottomRight } from './display/ViewportNode.js';
 
+// code
+export { default as ExpressionNode, expression } from './code/ExpressionNode.js';
+export { default as CodeNode, code, js } from './code/CodeNode.js';
+export { default as FunctionCallNode, call } from './code/FunctionCallNode.js';
+export { default as FunctionNode, func, fn } from './code/FunctionNode.js';
+export { default as ScriptableNode, scriptable, global } from './code/ScriptableNode.js';
+export { default as ScriptableValueNode, scriptableValue } from './code/ScriptableValueNode.js';
+
 // fog
 export { default as FogNode, fog } from './fog/FogNode.js';
 export { default as FogRangeNode, rangeFog } from './fog/FogRangeNode.js';

+ 1 - 1
examples/jsm/nodes/core/CodeNode.js → examples/jsm/nodes/code/CodeNode.js

@@ -1,4 +1,4 @@
-import Node, { addNodeClass } from './Node.js';
+import Node, { addNodeClass } from '../core/Node.js';
 import { nodeProxy } from '../shadernode/ShaderNode.js';
 
 class CodeNode extends Node {

+ 1 - 1
examples/jsm/nodes/core/ExpressionNode.js → examples/jsm/nodes/code/ExpressionNode.js

@@ -1,4 +1,4 @@
-import Node, { addNodeClass } from './Node.js';
+import Node, { addNodeClass } from '../core/Node.js';
 import { nodeProxy } from '../shadernode/ShaderNode.js';
 
 class ExpressionNode extends Node {

+ 2 - 2
examples/jsm/nodes/core/FunctionCallNode.js → examples/jsm/nodes/code/FunctionCallNode.js

@@ -1,5 +1,5 @@
-import TempNode from './TempNode.js';
-import { addNodeClass } from './Node.js';
+import TempNode from '../core/TempNode.js';
+import { addNodeClass } from '../core/Node.js';
 import { addNodeElement, nodeArray, nodeObject, nodeObjects } from '../shadernode/ShaderNode.js';
 
 class FunctionCallNode extends TempNode {

+ 1 - 1
examples/jsm/nodes/core/FunctionNode.js → examples/jsm/nodes/code/FunctionNode.js

@@ -1,5 +1,5 @@
 import CodeNode from './CodeNode.js';
-import { addNodeClass } from './Node.js';
+import { addNodeClass } from '../core/Node.js';
 import { nodeObject } from '../shadernode/ShaderNode.js';
 
 class FunctionNode extends CodeNode {

+ 488 - 0
examples/jsm/nodes/code/ScriptableNode.js

@@ -0,0 +1,488 @@
+import Node, { addNodeClass } from '../core/Node.js';
+import { scriptableValue } from './ScriptableValueNode.js';
+import { addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';
+
+class Resources extends Map {
+
+	get( key, callback = null, ...params ) {
+
+		if ( this.has( key ) ) return super.get( key );
+
+		if ( callback !== null ) {
+
+			const value = callback( ...params );
+			this.set( key, value );
+			return value;
+
+		}
+
+	}
+
+}
+
+class Parameters {
+
+	constructor( scriptableNode ) {
+
+		this.scriptableNode = scriptableNode;
+
+	}
+
+	get parameters() {
+
+		return this.scriptableNode.parameters;
+
+	}
+
+	get layout() {
+
+		return this.scriptableNode.getLayout();
+
+	}
+
+	getInputLayout( id ) {
+
+		return this.scriptableNode.getInputLayout( id );
+
+	}
+
+	get( name ) {
+
+		const param = this.parameters[ name ];
+		const value = param ? param.getValue() : null;
+
+		return value;
+
+	}
+
+}
+
+export const global = new Resources();
+
+class ScriptableNode extends Node {
+
+	constructor( codeNode = null, parameters = {} ) {
+
+		super();
+
+		this.codeNode = codeNode;
+		this.parameters = parameters;
+
+		this._local = new Resources();
+		this._output = scriptableValue();
+		this._outputs = {};
+		this._source = this.source;
+		this._method = null;
+		this._object = null;
+		this._value = null;
+		this._needsOutputUpdate = true;
+
+		this.onRefresh = this.onRefresh.bind( this );
+
+		this.isScriptableNode = true;
+
+	}
+
+	get source() {
+
+		return this.codeNode ? this.codeNode.code : '';
+
+	}
+
+	setLocal( name, value ) {
+
+		return this._local.set( name, value );
+
+	}
+
+	getLocal( name ) {
+
+		return this._local.get( name );
+
+	}
+
+	onRefresh() {
+
+		this._refresh();
+
+	}
+
+	getInputLayout( id ) {
+
+		for ( const element of this.getLayout() ) {
+
+			if ( element.inputType && ( element.id === id || element.name === id ) ) {
+
+				return element;
+
+			}
+
+		}
+
+	}
+
+	getOutputLayout( id ) {
+
+		for ( const element of this.getLayout() ) {
+
+			if ( element.outputType && ( element.id === id || element.name === id ) ) {
+
+				return element;
+
+			}
+
+		}
+
+	}
+
+	setOutput( name, value ) {
+
+		const outputs = this._outputs;
+
+		if ( outputs[ name ] === undefined ) {
+
+			outputs[ name ] = scriptableValue( value );
+
+		} else {
+
+			outputs[ name ].value = value;
+
+		}
+
+		return this;
+
+	}
+
+	getOutput( name ) {
+
+		return this._outputs[ name ];
+
+	}
+
+	getParameter( name ) {
+
+		return this.parameters[ name ];
+
+	}
+
+	setParameter( name, value ) {
+
+		const parameters = this.parameters;
+
+		if ( value && value.isScriptableNode ) {
+
+			this.deleteParameter( name );
+
+			parameters[ name ] = value;
+			parameters[ name ].getDefaultOutput().events.addEventListener( 'refresh', this.onRefresh );
+
+		} else if ( value && value.isScriptableValueNode ) {
+
+			this.deleteParameter( name );
+
+			parameters[ name ] = value;
+			parameters[ name ].events.addEventListener( 'refresh', this.onRefresh );
+
+		} else if ( parameters[ name ] === undefined ) {
+
+			parameters[ name ] = scriptableValue( value );
+			parameters[ name ].events.addEventListener( 'refresh', this.onRefresh );
+
+		} else {
+
+			parameters[ name ].value = value;
+
+		}
+
+		return this;
+
+	}
+
+	getValue() {
+
+		return this.getDefaultOutput().getValue();
+
+	}
+
+	deleteParameter( name ) {
+
+		let valueNode = this.parameters[ name ];
+
+		if ( valueNode ) {
+
+			if ( valueNode.isScriptableNode ) valueNode = valueNode.getDefaultOutput();
+
+			valueNode.events.removeEventListener( 'refresh', this.onRefresh );
+
+		}
+
+		return this;
+
+	}
+
+	clearParameters() {
+
+		for ( const name of Object.keys( this.parameters ) ) {
+
+			this.deleteParameter( name );
+
+		}
+
+		this.needsUpdate = true;
+
+		return this;
+
+	}
+
+	call( name, ...params ) {
+
+		const object = this.getObject();
+		const method = object[ name ];
+
+		if ( typeof method === 'function' ) {
+
+			return method( ...params );
+
+		}
+
+	}
+
+	async callAsync( name, ...params ) {
+
+		const object = this.getObject();
+		const method = object[ name ];
+
+		if ( typeof method === 'function' ) {
+
+			return method.constructor.name === 'AsyncFunction' ? await method( ...params ) : method( ...params );
+
+		}
+
+	}
+
+	getNodeType( builder ) {
+
+		return this.getDefaultOutputNode().getNodeType( builder );
+
+	}
+
+	refresh( output = null ) {
+
+		if ( output !== null ) {
+
+			this.getOutput( output ).refresh();
+
+		} else {
+
+			this._refresh();
+
+		}
+
+	}
+
+	getObject() {
+
+		if ( this.needsUpdate ) this.dispose();
+		if ( this._object !== null ) return this._object;
+
+		//
+
+		const refresh = () => this.refresh();
+		const setOutput = ( id, value ) => this.setOutput( id, value );
+
+		const parameters = new Parameters( this );
+
+		const THREE = global.get( 'THREE' );
+		const TSL = global.get( 'TSL' );
+
+		const method = this.getMethod( this.codeNode );
+		const params = [ parameters, this._local, global, refresh, setOutput, THREE, TSL ];
+
+		this._object = method( ...params );
+
+		const layout = this._object.layout;
+
+		if ( layout ) {
+
+			if ( layout.cache === false ) {
+
+				this._local.clear();
+
+			}
+
+			// default output
+			this._output.outputType = layout.outputType || null;
+
+			if ( Array.isArray( layout.elements ) ) {
+
+				for ( const element of layout.elements ) {
+
+					const id = element.id || element.name;
+
+					if ( element.inputType ) {
+
+						if ( this.getParameter( id ) === undefined ) this.setParameter( id, null );
+
+						this.getParameter( id ).inputType = element.inputType;
+
+					}
+
+					if ( element.outputType ) {
+
+						if ( this.getOutput( id ) === undefined ) this.setOutput( id, null );
+
+						this.getOutput( id ).outputType = element.outputType;
+
+					}
+
+				}
+
+			}
+
+		}
+
+		return this._object;
+
+	}
+
+	deserialize( data ) {
+
+		super.deserialize( data );
+
+		for ( const name in this.parameters ) {
+
+			let valueNode = this.parameters[ name ];
+
+			if ( valueNode.isScriptableNode ) valueNode = valueNode.getDefaultOutput();
+
+			valueNode.events.addEventListener( 'refresh', this.onRefresh );
+
+		}
+
+	}
+
+	getLayout() {
+
+		return this.getObject().layout;
+
+	}
+
+	getDefaultOutputNode() {
+
+		const output = this.getDefaultOutput().value;
+
+		if ( output && output.isNode ) {
+
+			return output;
+
+		}
+
+		return float();
+
+	}
+
+	getDefaultOutput()	{
+
+		return this._exec()._output;
+
+	}
+
+	getMethod() {
+
+		if ( this.needsUpdate ) this.dispose();
+		if ( this._method !== null ) return this._method;
+
+		//
+
+		const parametersProps = [ 'parameters', 'local', 'global', 'refresh', 'setOutput', 'THREE', 'TSL' ];
+		const interfaceProps = [ 'layout', 'init', 'main', 'dispose' ];
+
+		const properties = interfaceProps.join( ', ' );
+		const declarations = 'var ' + properties + '; var output = {};\n';
+		const returns = '\nreturn { ...output, ' + properties + ' };';
+
+		const code = declarations + this.codeNode.code + returns;
+
+		//
+
+		this._method = new Function( ...parametersProps, code );
+
+		return this._method;
+
+	}
+
+	dispose() {
+
+		if ( this._method === null ) return;
+
+		if ( this._object && typeof this._object.dispose === 'function' ) {
+
+			this._object.dispose();
+
+		}
+
+		this._method = null;
+		this._object = null;
+		this._source = null;
+		this._value = null;
+		this._needsOutputUpdate = true;
+		this._output.value = null;
+		this._outputs = {};
+
+	}
+
+	construct() {
+
+		return this.getDefaultOutputNode();
+
+	}
+
+	set needsUpdate( value ) {
+
+		if ( value === true ) this.dispose();
+
+	}
+
+	get needsUpdate() {
+
+		return this.source !== this._source;
+
+	}
+
+	_exec()	{
+
+		if ( this.codeNode === null ) return this;
+
+		if ( this._needsOutputUpdate === true ) {
+
+			this._value = this.call( 'main' );
+
+			this._needsOutputUpdate = false;
+
+		}
+
+		this._output.value = this._value;
+
+		return this;
+
+	}
+
+	_refresh() {
+
+		this.needsUpdate = true;
+
+		this._exec();
+
+		this._output.refresh();
+
+	}
+
+}
+
+export default ScriptableNode;
+
+export const scriptable = nodeProxy( ScriptableNode );
+
+addNodeElement( 'scriptable', scriptable );
+
+addNodeClass( ScriptableNode );

+ 167 - 0
examples/jsm/nodes/code/ScriptableValueNode.js

@@ -0,0 +1,167 @@
+import Node, { addNodeClass } from '../core/Node.js';
+import { arrayBufferToBase64, base64ToArrayBuffer } from '../core/NodeUtils.js';
+import { addNodeElement, nodeProxy, float } from '../shadernode/ShaderNode.js';
+import { EventDispatcher } from 'three';
+
+class ScriptableValueNode extends Node {
+
+	constructor( value = null ) {
+
+		super();
+
+		this._value = value;
+		this._cache = null;
+
+		this.inputType = null;
+		this.outpuType = null;
+
+		this.events = new EventDispatcher();
+
+		this.isScriptableValueNode = true;
+
+	}
+
+	get isScriptableOutputNode() {
+
+		return this.outputType !== null;
+
+	}
+
+	set value( val ) {
+
+		if ( this._value === val ) return;
+
+		if ( this._cache && this.inputType === 'URL' && this.value.value instanceof ArrayBuffer ) {
+
+			URL.revokeObjectURL( this._cache );
+
+			this._cache = null;
+
+		}
+
+		this._value = val;
+
+		this.events.dispatchEvent( { type: 'change' } );
+
+		this.refresh();
+
+	}
+
+	get value() {
+
+		return this._value;
+
+	}
+
+	refresh() {
+
+		this.events.dispatchEvent( { type: 'refresh' } );
+
+	}
+
+	getValue() {
+
+		const value = this.value;
+
+		if ( value && this._cache === null && this.inputType === 'URL' && value.value instanceof ArrayBuffer ) {
+
+			this._cache = URL.createObjectURL( new Blob( [ value.value ] ) );
+
+		} else if ( value && value.value !== null && value.value !== undefined && (
+			( ( this.inputType === 'URL' || this.inputType === 'String' ) && typeof value.value === 'string' ) ||
+			( this.inputType === 'Number' && typeof value.value === 'number' ) ||
+			( this.inputType === 'Vector2' && value.value.isVector2 ) ||
+			( this.inputType === 'Vector3' && value.value.isVector3 ) ||
+			( this.inputType === 'Vector4' && value.value.isVector4 ) ||
+			( this.inputType === 'Color' && value.value.isColor ) ||
+			( this.inputType === 'Matrix3' && value.value.isMatrix3 ) ||
+			( this.inputType === 'Matrix4' && value.value.isMatrix4 )
+		) ) {
+
+			return value.value;
+
+		}
+
+		return this._cache || value;
+
+	}
+
+	getNodeType( builder ) {
+
+		return this.value && this.value.isNode ? this.value.getNodeType( builder ) : 'float';
+
+	}
+
+	construct() {
+
+		return this.value && this.value.isNode ? this.value : float();
+
+	}
+
+	serialize( data ) {
+
+		super.serialize( data );
+
+		if ( this.value !== null ) {
+
+			if ( this.inputType === 'ArrayBuffer' ) {
+
+				data.value = arrayBufferToBase64( this.value );
+
+			} else {
+
+				data.value = this.value ? this.value.toJSON( data.meta ).uuid : null;
+
+			}
+
+		} else {
+
+			data.value = null;
+
+		}
+
+		data.inputType = this.inputType;
+		data.outputType = this.outputType;
+
+	}
+
+	deserialize( data ) {
+
+		super.deserialize( data );
+
+		let value = null;
+
+		if ( data.value !== null ) {
+
+			if ( data.inputType === 'ArrayBuffer' ) {
+
+				value = base64ToArrayBuffer( data.value );
+
+			} else if ( data.inputType === 'Texture' ) {
+
+				value = data.meta.textures[ data.value ];
+
+			} else {
+
+				value = data.meta.nodes[ data.value ] || null;
+
+			}
+
+		}
+
+		this.value = value;
+
+		this.inputType = data.inputType;
+		this.outputType = data.outputType;
+
+	}
+
+}
+
+export default ScriptableValueNode;
+
+export const scriptableValue = nodeProxy( ScriptableValueNode );
+
+addNodeElement( 'scriptableValue', scriptableValue );
+
+addNodeClass( ScriptableValueNode );

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

@@ -1,5 +1,5 @@
 import Node, { addNodeClass } from './Node.js';
-import { getValueType, getValueFromType } from './NodeUtils.js';
+import { getValueType, getValueFromType, arrayBufferToBase64 } from './NodeUtils.js';
 
 class InputNode extends Node {
 
@@ -51,6 +51,8 @@ class InputNode extends Node {
 		data.valueType = getValueType( this.value );
 		data.nodeType = this.nodeType;
 
+		if ( data.valueType === 'ArrayBuffer' ) data.value = arrayBufferToBase64( data.value );
+
 		data.precision = this.precision;
 
 	}

+ 7 - 1
examples/jsm/nodes/core/Node.js

@@ -239,9 +239,15 @@ class Node {
 
 	}
 
+	getSerializeChildren() {
+
+		return getNodeChildren( this );
+
+	}
+
 	serialize( json ) {
 
-		const nodeChildren = getNodeChildren( this );
+		const nodeChildren = this.getSerializeChildren();
 
 		const inputNodes = {};
 

+ 58 - 13
examples/jsm/nodes/core/NodeUtils.js

@@ -1,6 +1,6 @@
 import { Color, Matrix3, Matrix4, Vector2, Vector3, Vector4 } from 'three';
 
-export function getCacheKey( object )  {
+export function getCacheKey( object ) {
 
 	let cacheKey = '{';
 
@@ -26,19 +26,22 @@ export function getCacheKey( object )  {
 
 }
 
-export function* getNodeChildren( node ) {
+export function* getNodeChildren( node, toJSON = false ) {
 
 	for ( const property in node ) {
 
+		// Ignore private properties.
+		if ( property.startsWith( '_' ) === true ) continue;
+
 		const object = node[ property ];
 
 		if ( Array.isArray( object ) === true ) {
 
-			for ( let i = 0; i < object.length; i++ ) {
+			for ( let i = 0; i < object.length; i ++ ) {
 
 				const child = object[ i ];
 
-				if ( child && child.isNode === true ) {
+				if ( child && ( child.isNode === true || toJSON && typeof child.toJSON === 'function' ) ) {
 
 					yield { property, index: i, childNode: child };
 
@@ -56,7 +59,7 @@ export function* getNodeChildren( node ) {
 
 				const child = object[ subProperty ];
 
-				if ( child && child.isNode === true ) {
+				if ( child && ( child.isNode === true || toJSON && typeof child.toJSON === 'function' ) ) {
 
 					yield { property, index: subProperty, childNode: child };
 
@@ -72,38 +75,50 @@ export function* getNodeChildren( node ) {
 
 export function getValueType( value ) {
 
-	if ( typeof value === 'number' ) {
+	if ( value === undefined || value === null ) return null;
+
+	const typeOf = typeof value;
+
+	if ( typeOf === 'number' ) {
 
 		return 'float';
 
-	} else if ( typeof value === 'boolean' ) {
+	} else if ( typeOf === 'boolean' ) {
 
 		return 'bool';
 
-	} else if ( value && value.isVector2 === true ) {
+	} else if ( typeOf === 'string' ) {
+
+		return 'string';
+
+	} else if ( value.isVector2 === true ) {
 
 		return 'vec2';
 
-	} else if ( value && value.isVector3 === true ) {
+	} else if ( value.isVector3 === true ) {
 
 		return 'vec3';
 
-	} else if ( value && value.isVector4 === true ) {
+	} else if ( value.isVector4 === true ) {
 
 		return 'vec4';
 
-	} else if ( value && value.isMatrix3 === true ) {
+	} else if ( value.isMatrix3 === true ) {
 
 		return 'mat3';
 
-	} else if ( value && value.isMatrix4 === true ) {
+	} else if ( value.isMatrix4 === true ) {
 
 		return 'mat4';
 
-	} else if ( value && value.isColor === true ) {
+	} else if ( value.isColor === true ) {
 
 		return 'color';
 
+	} else if ( value instanceof ArrayBuffer ) {
+
+		return 'ArrayBuffer';
+
 	}
 
 	return null;
@@ -146,8 +161,38 @@ export function getValueFromType( type, ...params ) {
 
 		return params[ 0 ] || 0;
 
+	} else if ( type === 'string' ) {
+
+		return params[ 0 ] || '';
+
+	} else if ( type === 'ArrayBuffer' ) {
+
+		return base64ToArrayBuffer( params[ 0 ] );
+
 	}
 
 	return null;
 
 }
+
+export function arrayBufferToBase64( arrayBuffer ) {
+
+	let chars = '';
+
+	const array = new Uint8Array( arrayBuffer );
+
+	for ( let i = 0; i < array.length; i ++ ) {
+
+		chars += String.fromCharCode( array[ i ] );
+
+	}
+
+	return btoa( chars );
+
+}
+
+export function base64ToArrayBuffer( base64 ) {
+
+	return Uint8Array.from( atob( base64 ), c => c.charCodeAt( 0 ) ).buffer;
+
+}

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

@@ -1,7 +1,7 @@
 import Node, { addNodeClass } from './Node.js';
 import { assign } from '../math/OperatorNode.js';
 import { bypass } from '../core/BypassNode.js';
-import { expression } from '../core/ExpressionNode.js';
+import { expression } from '../code/ExpressionNode.js';
 import { cond } from '../math/CondNode.js';
 import { nodeProxy, shader } from '../shadernode/ShaderNode.js';
 

+ 1 - 1
examples/jsm/nodes/materialx/lib/mx_hsv.js

@@ -1,4 +1,4 @@
-import { fn } from '../../core/FunctionNode.js';
+import { fn } from '../../code/FunctionNode.js';
 
 // Original shader code from:
 // https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/stdlib/genglsl/lib/mx_hsv.glsl

+ 2 - 2
examples/jsm/nodes/materialx/lib/mx_noise.js

@@ -1,5 +1,5 @@
-import { code } from '../../core/CodeNode.js';
-import { fn } from '../../core/FunctionNode.js';
+import { code } from '../../code/CodeNode.js';
+import { fn } from '../../code/FunctionNode.js';
 
 // Original shader code from:
 // https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/stdlib/genglsl/lib/mx_noise.glsl

+ 2 - 2
examples/jsm/nodes/materialx/lib/mx_transform_color.js

@@ -1,5 +1,5 @@
-import { code } from '../../core/CodeNode.js';
-import { fn } from '../../core/FunctionNode.js';
+import { code } from '../../code/CodeNode.js';
+import { fn } from '../../code/FunctionNode.js';
 
 // Original shader code from:
 // https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/stdlib/genglsl/lib/mx_transform_color.glsl

+ 7 - 2
examples/jsm/nodes/shadernode/ShaderNode.js

@@ -4,7 +4,7 @@ import ConvertNode from '../utils/ConvertNode.js';
 import JoinNode from '../utils/JoinNode.js';
 import SplitNode from '../utils/SplitNode.js';
 import ConstNode from '../core/ConstNode.js';
-import { getValueFromType } from '../core/NodeUtils.js';
+import { getValueFromType, getValueType } from '../core/NodeUtils.js';
 
 const NodeElements = new Map(); // @TODO: Currently only a few nodes are added, probably also add others
 
@@ -281,7 +281,7 @@ const ConvertType = function ( type, cacheMap = null ) {
 
 			if ( nodes.length === 1 ) {
 
-				return nodeObject( nodes[ 0 ].nodeType === type ? nodes[ 0 ] : new ConvertNode( nodes[ 0 ], type ) );
+				return nodeObject( nodes[ 0 ].nodeType === type || getValueType( nodes[ 0 ].value ) === type ? nodes[ 0 ] : new ConvertNode( nodes[ 0 ], type ) );
 
 			}
 
@@ -352,6 +352,9 @@ export const imat4 = new ConvertType( 'imat4' );
 export const umat4 = new ConvertType( 'umat4' );
 export const bmat4 = new ConvertType( 'bmat4' );
 
+export const string = ( value = '' ) => nodeObject( new ConstNode( value, 'string' ) );
+export const arrayBuffer = ( value ) => nodeObject( new ConstNode( value, 'ArrayBuffer' ) );
+
 addNodeElement( 'color', color );
 addNodeElement( 'float', float );
 addNodeElement( 'int', int );
@@ -377,6 +380,8 @@ addNodeElement( 'mat4', mat4 );
 addNodeElement( 'imat4', imat4 );
 addNodeElement( 'umat4', umat4 );
 addNodeElement( 'bmat4', bmat4 );
+addNodeElement( 'string', string );
+addNodeElement( 'arrayBuffer', arrayBuffer );
 
 // basic nodes
 // HACK - we cannot export them from the corresponding files because of the cyclic dependency

+ 1 - 1
examples/jsm/nodes/utils/DiscardNode.js

@@ -1,5 +1,5 @@
 import CondNode from '../math/CondNode.js';
-import { expression } from '../core/ExpressionNode.js';
+import { expression } from '../code/ExpressionNode.js';
 import { addNodeClass } from '../core/Node.js';
 import { addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';
 

+ 136 - 1
examples/webgpu_materials.html

@@ -27,7 +27,9 @@
 		<script type="module">
 
 			import * as THREE from 'three';
-			import { attribute, positionLocal, normalLocal, normalWorld, normalView, color, texture, ShaderNode, func, uv, vec3, triplanarTexture, viewportBottomLeft, MeshBasicNodeMaterial } from 'three/nodes';
+			import * as Nodes from 'three/nodes';
+
+			import { attribute, positionLocal, normalLocal, normalWorld, normalView, color, texture, ShaderNode, func, uv, vec3, triplanarTexture, viewportBottomLeft, js, string, global, MeshBasicNodeMaterial, NodeObjectLoader } from 'three/nodes';
 
 			import WebGPU from 'three/addons/capabilities/WebGPU.js';
 			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
@@ -203,6 +205,112 @@
 				material.colorNode = texture( uvTexture, viewportBottomLeft );
 				materials.push( material );
 
+				// Scriptable
+
+				global.set( 'THREE', THREE );
+				global.set( 'TSL', Nodes );
+
+				const asyncNode = js( `
+
+					layout = {
+						outputType: 'node'
+					};
+
+					const { float } = TSL;
+
+					function init() {
+
+						setTimeout( () => {
+
+							local.set( 'result', float( 1.0 ) );
+
+							refresh(); // refresh the node
+
+						}, 1000 );
+
+						return float( 0.0 );
+
+					}
+
+					function main() {
+
+						const result = local.get( 'result', init );
+
+						//console.log( 'result', result );
+
+						return result;
+
+					}
+
+				` ).scriptable();
+
+				const scriptableNode = js( `
+
+					layout = {
+						outputType: 'node',
+						elements: [
+							{ name: 'source', inputType: 'node' },
+							{ name: 'contrast', inputType: 'node' },
+							{ name: 'vector3', inputType: 'Vector3' },
+							{ name: 'message', inputType: 'string' },
+							{ name: 'binary', inputType: 'ArrayBuffer' },
+							{ name: 'object3d', inputType: 'Object3D' },
+							{ name: 'execFrom', inputType: 'string' }
+						]
+					};
+
+					const { saturation, float, oscSine, mul } = TSL;
+
+					function helloWorld() {
+
+						console.log( "Hello World!" );
+
+					}
+
+					function main() {
+
+						const source = parameters.get( 'source' ) || float();
+						const contrast = parameters.get( 'contrast' ) || float();
+
+						const material = local.get( 'material' );
+
+						//console.log( 'vector3', parameters.get( 'vector3' ) );
+
+						if ( parameters.get( 'execFrom' ) === 'serialized' ) {
+
+							//console.log( 'message', parameters.get( 'message' ).value );
+							//console.log( 'binary', parameters.get( 'binary' ) );
+							//console.log( 'object3d', parameters.get( 'object3d' ) ); // unserializable yet
+
+							//console.log( global.get( 'renderer' ) );
+
+						}
+
+						if ( material ) material.needsUpdate = true;
+
+						return mul( saturation( source, oscSine() ), contrast );
+
+					}
+
+					output = { helloWorld };
+
+				` ).scriptable();
+
+				scriptableNode.setParameter( 'source', texture( uvTexture ).xyz );
+				scriptableNode.setParameter( 'contrast', asyncNode );
+				scriptableNode.setParameter( 'vector3', vec3( new THREE.Vector3( 1, 1, 1 ) ) );
+				scriptableNode.setParameter( 'message', string( 'Hello World!' ) );
+				scriptableNode.setParameter( 'binary', new ArrayBuffer( 4 ) );
+				scriptableNode.setParameter( 'object3d', new THREE.Group() );
+
+				scriptableNode.call( 'helloWorld' );
+
+				material = new MeshBasicNodeMaterial();
+				material.colorNode = scriptableNode;
+				materials.push( material );
+
+				scriptableNode.setLocal( 'material', material );
+
 				//
 				// Geometry
 				//
@@ -215,6 +323,8 @@
 
 				}
 
+				const serializeMesh = scene.children[ scene.children.length - 1 ];
+
 				//
 
 				renderer = new WebGPURenderer();
@@ -232,6 +342,10 @@
 
 				window.addEventListener( 'resize', onWindowResize );
 
+				//
+
+				setTimeout( () => testSerialization( serializeMesh ), 1000 );
+
 			}
 
 			function addMesh( geometry, material ) {
@@ -251,6 +365,27 @@
 
 			}
 
+			function testSerialization( mesh ) {
+
+				const json = mesh.toJSON();
+				const loader = new NodeObjectLoader();
+				const serializedMesh = loader.parse( json );
+
+				serializedMesh.position.x = ( objects.length % 4 ) * 200 - 400;
+				serializedMesh.position.z = Math.floor( objects.length / 4 ) * 200 - 200;
+
+				const scriptableNode = serializedMesh.material.colorNode;
+
+				// it's because local.get( 'material' ) is used in the example ( local/global is unserializable )
+				scriptableNode.setLocal( 'material', serializedMesh.material );
+				scriptableNode.setParameter( 'execFrom', 'serialized' );
+
+				objects.push( serializedMesh );
+
+				scene.add( serializedMesh );
+
+			}
+
 			function onWindowResize() {
 
 				camera.aspect = window.innerWidth / window.innerHeight;