From 818d2ea0c3faecf514641a715a825d0365b51d2e Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 17:16:30 -0300 Subject: [PATCH 01/47] CodeNode: Added language and serialize. --- examples/jsm/nodes/core/CodeNode.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/examples/jsm/nodes/core/CodeNode.js b/examples/jsm/nodes/core/CodeNode.js index 2cf18442cd1872..a0e116861aecb2 100644 --- a/examples/jsm/nodes/core/CodeNode.js +++ b/examples/jsm/nodes/core/CodeNode.js @@ -3,13 +3,14 @@ import { nodeProxy } from '../shadernode/ShaderNode.js'; class CodeNode extends Node { - constructor( code = '', includes = [] ) { + constructor( code = '', includes = [], language = '' ) { super( 'code' ); this.isCodeNode = true; this.code = code; + this.language = language; this._includes = includes; @@ -46,10 +47,29 @@ class CodeNode extends Node { } + serialize( data ) { + + super.serialize( data ); + + data.code = this.code; + data.language = this.language; + + } + + deserialize( data ) { + + super.deserialize( data ); + + this.code = data.code; + this.language = data.language; + + } + } export default CodeNode; export const code = nodeProxy( CodeNode ); +export const js = ( src, includes ) => code( src, includes, 'js' ); addNodeClass( CodeNode ); From 49b4a18fff3c5bae5d39a58bb6546bbb1f42943f Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 17:22:33 -0300 Subject: [PATCH 02/47] TSL: Renamed inversesqrt -> inverseSqrt, faceforward -> faceForward, --- examples/jsm/nodes/Nodes.js | 6 ++++-- examples/jsm/nodes/display/NormalMapNode.js | 2 +- examples/jsm/nodes/math/MathNode.js | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/jsm/nodes/Nodes.js b/examples/jsm/nodes/Nodes.js index feb34faab9e251..5473d588d1829d 100644 --- a/examples/jsm/nodes/Nodes.js +++ b/examples/jsm/nodes/Nodes.js @@ -9,7 +9,7 @@ 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 } from './core/CodeNode.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'; @@ -35,8 +35,10 @@ export { default as UniformNode, uniform } from './core/UniformNode.js'; export { default as VarNode, label, temp } from './core/VarNode.js'; export { default as VaryingNode, varying } from './core/VaryingNode.js'; +export * as NodeUtils from './core/NodeUtils.js'; + // math -export { default as MathNode, EPSILON, INFINITY, radians, degrees, exp, exp2, log, log2, sqrt, inversesqrt, floor, ceil, normalize, fract, sin, cos, tan, asin, acos, atan, abs, sign, length, negate, invert, dFdx, dFdy, round, reciprocal, atan2, min, max, mod, step, reflect, distance, difference, dot, cross, pow, pow2, pow3, pow4, transformDirection, mix, clamp, refract, smoothstep, faceforward } from './math/MathNode.js'; +export { default as MathNode, EPSILON, INFINITY, radians, degrees, exp, exp2, log, log2, sqrt, inverseSqrt, floor, ceil, normalize, fract, sin, cos, tan, asin, acos, atan, abs, sign, length, negate, invert, dFdx, dFdy, round, reciprocal, atan2, min, max, mod, step, reflect, distance, difference, dot, cross, pow, pow2, pow3, pow4, transformDirection, mix, clamp, saturate, refract, smoothstep, faceForward } from './math/MathNode.js'; export { default as OperatorNode, add, sub, mul, div, remainder, equal, assign, lessThan, greaterThan, lessThanEqual, greaterThanEqual, and, or, xor, bitAnd, bitOr, bitXor, shiftLeft, shiftRight } from './math/OperatorNode.js'; export { default as CondNode, cond } from './math/CondNode.js'; diff --git a/examples/jsm/nodes/display/NormalMapNode.js b/examples/jsm/nodes/display/NormalMapNode.js index 9869c69e98aef8..e4fba88641e816 100644 --- a/examples/jsm/nodes/display/NormalMapNode.js +++ b/examples/jsm/nodes/display/NormalMapNode.js @@ -33,7 +33,7 @@ const perturbNormal2ArbNode = new ShaderNode( ( inputs ) => { const B = q1perp.mul( st0.y ).add( q0perp.mul( st1.y ) ); const det = T.dot( T ).max( B.dot( B ) ); - const scale = faceDirection.mul( det.inversesqrt() ); + const scale = faceDirection.mul( det.inverseSqrt() ); return add( T.mul( mapN.x, scale ), B.mul( mapN.y, scale ), N.mul( mapN.z ) ).normalize(); diff --git a/examples/jsm/nodes/math/MathNode.js b/examples/jsm/nodes/math/MathNode.js index 160ae72fc35183..924e6374b41a27 100644 --- a/examples/jsm/nodes/math/MathNode.js +++ b/examples/jsm/nodes/math/MathNode.js @@ -254,7 +254,7 @@ export const exp2 = nodeProxy( MathNode, MathNode.EXP2 ); export const log = nodeProxy( MathNode, MathNode.LOG ); export const log2 = nodeProxy( MathNode, MathNode.LOG2 ); export const sqrt = nodeProxy( MathNode, MathNode.SQRT ); -export const inversesqrt = nodeProxy( MathNode, MathNode.INVERSE_SQRT ); +export const inverseSqrt = nodeProxy( MathNode, MathNode.INVERSE_SQRT ); export const floor = nodeProxy( MathNode, MathNode.FLOOR ); export const ceil = nodeProxy( MathNode, MathNode.CEIL ); export const normalize = nodeProxy( MathNode, MathNode.NORMALIZE ); @@ -295,7 +295,7 @@ export const mix = nodeProxy( MathNode, MathNode.MIX ); export const clamp = ( value, low = 0, high = 1 ) => nodeObject( new MathNode( MathNode.CLAMP, nodeObject( value ), nodeObject( low ), nodeObject( high ) ) ); export const refract = nodeProxy( MathNode, MathNode.REFRACT ); export const smoothstep = nodeProxy( MathNode, MathNode.SMOOTHSTEP ); -export const faceforward = nodeProxy( MathNode, MathNode.FACEFORWARD ); +export const faceForward = nodeProxy( MathNode, MathNode.FACEFORWARD ); addNodeElement( 'radians', radians ); addNodeElement( 'degrees', degrees ); @@ -304,7 +304,7 @@ addNodeElement( 'exp2', exp2 ); addNodeElement( 'log', log ); addNodeElement( 'log2', log2 ); addNodeElement( 'sqrt', sqrt ); -addNodeElement( 'inversesqrt', inversesqrt ); +addNodeElement( 'inverseSqrt', inverseSqrt ); addNodeElement( 'floor', floor ); addNodeElement( 'ceil', ceil ); addNodeElement( 'normalize', normalize ); @@ -342,7 +342,7 @@ addNodeElement( 'mix', mix ); addNodeElement( 'clamp', clamp ); addNodeElement( 'refract', refract ); addNodeElement( 'smoothstep', smoothstep ); -addNodeElement( 'faceforward', faceforward ); +addNodeElement( 'faceForward', faceForward ); addNodeElement( 'difference', difference ); addNodeClass( MathNode ); From bb5ba6589fc2f6c87d97d1a8973f2510bcb00907 Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 17:23:00 -0300 Subject: [PATCH 03/47] TSL: Added saturate --- examples/jsm/nodes/math/MathNode.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/jsm/nodes/math/MathNode.js b/examples/jsm/nodes/math/MathNode.js index 924e6374b41a27..7c89d54687953f 100644 --- a/examples/jsm/nodes/math/MathNode.js +++ b/examples/jsm/nodes/math/MathNode.js @@ -293,6 +293,7 @@ export const transformDirection = nodeProxy( MathNode, MathNode.TRANSFORM_DIRECT export const mix = nodeProxy( MathNode, MathNode.MIX ); export const clamp = ( value, low = 0, high = 1 ) => nodeObject( new MathNode( MathNode.CLAMP, nodeObject( value ), nodeObject( low ), nodeObject( high ) ) ); +export const saturate = ( value ) => clamp( value ); export const refract = nodeProxy( MathNode, MathNode.REFRACT ); export const smoothstep = nodeProxy( MathNode, MathNode.SMOOTHSTEP ); export const faceForward = nodeProxy( MathNode, MathNode.FACEFORWARD ); @@ -344,5 +345,6 @@ addNodeElement( 'refract', refract ); addNodeElement( 'smoothstep', smoothstep ); addNodeElement( 'faceForward', faceForward ); addNodeElement( 'difference', difference ); +addNodeElement( 'saturate', saturate ); addNodeClass( MathNode ); From a1364768cfba9272debe37ba2a27f5537ce67e0d Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 18:46:55 -0300 Subject: [PATCH 04/47] TSL: Different params order if method chaining is used. --- examples/jsm/nodes/math/MathNode.js | 7 +++++-- examples/webgpu_sandbox.html | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/jsm/nodes/math/MathNode.js b/examples/jsm/nodes/math/MathNode.js index 7c89d54687953f..c071ac5bce15f3 100644 --- a/examples/jsm/nodes/math/MathNode.js +++ b/examples/jsm/nodes/math/MathNode.js @@ -298,6 +298,9 @@ export const refract = nodeProxy( MathNode, MathNode.REFRACT ); export const smoothstep = nodeProxy( MathNode, MathNode.SMOOTHSTEP ); export const faceForward = nodeProxy( MathNode, MathNode.FACEFORWARD ); +export const mixElement = ( t, e1, e2 ) => mix( e1, e2, t ); +export const smoothstepElement = ( x, low, high ) => smoothstep( low, high, x ); + addNodeElement( 'radians', radians ); addNodeElement( 'degrees', degrees ); addNodeElement( 'exp', exp ); @@ -339,10 +342,10 @@ addNodeElement( 'pow2', pow2 ); addNodeElement( 'pow3', pow3 ); addNodeElement( 'pow4', pow4 ); addNodeElement( 'transformDirection', transformDirection ); -addNodeElement( 'mix', mix ); +addNodeElement( 'mix', mixElement ); addNodeElement( 'clamp', clamp ); addNodeElement( 'refract', refract ); -addNodeElement( 'smoothstep', smoothstep ); +addNodeElement( 'smoothstep', smoothstepElement ); addNodeElement( 'faceForward', faceForward ); addNodeElement( 'difference', difference ); addNodeElement( 'saturate', saturate ); diff --git a/examples/webgpu_sandbox.html b/examples/webgpu_sandbox.html index d97804a81faa70..3ce37d484b61ed 100644 --- a/examples/webgpu_sandbox.html +++ b/examples/webgpu_sandbox.html @@ -125,7 +125,7 @@ const materialCompressed = new MeshBasicNodeMaterial(); materialCompressed.colorNode = texture( dxt5Texture ); - materialCompressed.emissiveNode = color( 0x663300 ); + materialCompressed.emissiveNode = oscSine().mix( color( 0x663300 ), color( 0x0000FF ) ); materialCompressed.alphaTestNode = oscSine(); materialCompressed.transparent = true; From 9070cca9939ed3359b740c498fc5fca1cbc5c33a Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 19:59:46 -0300 Subject: [PATCH 05/47] Nodes: Change mix order and added FogNode.mixAssign() --- examples/jsm/nodes/display/ColorAdjustmentNode.js | 2 +- examples/jsm/nodes/fog/FogNode.js | 4 ++-- examples/jsm/nodes/lighting/EnvironmentNode.js | 2 +- examples/jsm/nodes/materials/NodeMaterial.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/jsm/nodes/display/ColorAdjustmentNode.js b/examples/jsm/nodes/display/ColorAdjustmentNode.js index 0d16a13f7ccc1e..7b757853f2ad8f 100644 --- a/examples/jsm/nodes/display/ColorAdjustmentNode.js +++ b/examples/jsm/nodes/display/ColorAdjustmentNode.js @@ -6,7 +6,7 @@ import { addNodeElement, ShaderNode, nodeProxy, float, vec3, mat3 } from '../sha const saturationNode = new ShaderNode( ( { color, adjustment } ) => { - return luminance( color ).mix( color, adjustment ); + return adjustment.mix( luminance( color ), color ); } ); diff --git a/examples/jsm/nodes/fog/FogNode.js b/examples/jsm/nodes/fog/FogNode.js index 481f20adb7d55e..b819895ae1e811 100644 --- a/examples/jsm/nodes/fog/FogNode.js +++ b/examples/jsm/nodes/fog/FogNode.js @@ -14,9 +14,9 @@ class FogNode extends Node { } - mix( outputNode ) { + mixAssign( outputNode ) { - return outputNode.mix( this.colorNode, this ); + return this.mix( outputNode, this.colorNode ); } diff --git a/examples/jsm/nodes/lighting/EnvironmentNode.js b/examples/jsm/nodes/lighting/EnvironmentNode.js index b3f5f614093e43..2e0d8a6774bcad 100644 --- a/examples/jsm/nodes/lighting/EnvironmentNode.js +++ b/examples/jsm/nodes/lighting/EnvironmentNode.js @@ -37,7 +37,7 @@ class EnvironmentNode extends LightingNode { if ( reflectVec === undefined ) { reflectVec = positionViewDirection.negate().reflect( transformedNormalView ); - reflectVec = reflectVec.mix( transformedNormalView, roughness.mul( roughness ) ).normalize(); + reflectVec = roughness.mul( roughness ).mix( reflectVec, transformedNormalView ).normalize(); reflectVec = reflectVec.transformDirection( cameraViewMatrix ); } diff --git a/examples/jsm/nodes/materials/NodeMaterial.js b/examples/jsm/nodes/materials/NodeMaterial.js index 929aa77d126a1c..94ca092b96795b 100644 --- a/examples/jsm/nodes/materials/NodeMaterial.js +++ b/examples/jsm/nodes/materials/NodeMaterial.js @@ -276,7 +276,7 @@ class NodeMaterial extends ShaderMaterial { } - if ( fogNode ) outputNode = vec4( fogNode.mix( outputNode.rgb ), outputNode.a ); + if ( fogNode ) outputNode = vec4( fogNode.mixAssign( outputNode.rgb ), outputNode.a ); return outputNode; From c227d8cc955e15d17b875498e05a0685037af129 Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 23:38:15 -0300 Subject: [PATCH 06/47] Node: Ignore private properties on serialization. --- examples/jsm/nodes/core/NodeUtils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/jsm/nodes/core/NodeUtils.js b/examples/jsm/nodes/core/NodeUtils.js index eb3f4b5aafc300..50ebb51f5f98a8 100644 --- a/examples/jsm/nodes/core/NodeUtils.js +++ b/examples/jsm/nodes/core/NodeUtils.js @@ -30,6 +30,9 @@ export function* getNodeChildren( node ) { for ( const property in node ) { + // Ignore private properties. + if ( property.startsWith( '_' ) === true ) continue; + const object = node[ property ]; if ( Array.isArray( object ) === true ) { From 93007151509e7dbb08696480489a4fafb4d36df1 Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 23:55:19 -0300 Subject: [PATCH 07/47] Nodes: Added string, arrayBuffer --- examples/jsm/nodes/core/InputNode.js | 4 +- examples/jsm/nodes/core/NodeUtils.js | 68 +++++++++++++++++---- examples/jsm/nodes/shadernode/ShaderNode.js | 5 ++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/examples/jsm/nodes/core/InputNode.js b/examples/jsm/nodes/core/InputNode.js index 511b2a02c16222..3bcf14fe9fc898 100644 --- a/examples/jsm/nodes/core/InputNode.js +++ b/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; } diff --git a/examples/jsm/nodes/core/NodeUtils.js b/examples/jsm/nodes/core/NodeUtils.js index 50ebb51f5f98a8..df38767fcbb867 100644 --- a/examples/jsm/nodes/core/NodeUtils.js +++ b/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,7 +26,7 @@ export function getCacheKey( object ) { } -export function* getNodeChildren( node ) { +export function* getNodeChildren( node, toJSON = false ) { for ( const property in node ) { @@ -37,11 +37,11 @@ export function* getNodeChildren( node ) { 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 }; @@ -59,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 }; @@ -75,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; @@ -149,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; + +} diff --git a/examples/jsm/nodes/shadernode/ShaderNode.js b/examples/jsm/nodes/shadernode/ShaderNode.js index 0f402da9e3ee3d..7ad8411f96a7a4 100644 --- a/examples/jsm/nodes/shadernode/ShaderNode.js +++ b/examples/jsm/nodes/shadernode/ShaderNode.js @@ -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 From 8e071a973c38c9b7dcb74bfaec80783e24173f0b Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 23:56:10 -0300 Subject: [PATCH 08/47] Node: Added .getSerializeChildren() --- examples/jsm/nodes/core/Node.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/jsm/nodes/core/Node.js b/examples/jsm/nodes/core/Node.js index 33dba30f82068b..a53a564e8d6c10 100644 --- a/examples/jsm/nodes/core/Node.js +++ b/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 = {}; From 1e69094a0f25cd1130bc24588dd48cbab390798d Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 23:56:56 -0300 Subject: [PATCH 09/47] Added ScriptableNode --- examples/jsm/nodes/Nodes.js | 4 + .../jsm/nodes/scriptable/ScriptableNode.js | 434 ++++++++++++++++++ .../nodes/scriptable/ScriptableValueNode.js | 158 +++++++ 3 files changed, 596 insertions(+) create mode 100644 examples/jsm/nodes/scriptable/ScriptableNode.js create mode 100644 examples/jsm/nodes/scriptable/ScriptableValueNode.js diff --git a/examples/jsm/nodes/Nodes.js b/examples/jsm/nodes/Nodes.js index 5473d588d1829d..4d23571ca983f6 100644 --- a/examples/jsm/nodes/Nodes.js +++ b/examples/jsm/nodes/Nodes.js @@ -97,6 +97,10 @@ 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'; +// scriptable +export { default as ScriptableNode, scriptable } from './scriptable/ScriptableNode.js'; +export { default as ScriptableValueNode, scriptableValue } from './scriptable/ScriptableValueNode.js'; + // fog export { default as FogNode, fog } from './fog/FogNode.js'; export { default as FogRangeNode, rangeFog } from './fog/FogRangeNode.js'; diff --git a/examples/jsm/nodes/scriptable/ScriptableNode.js b/examples/jsm/nodes/scriptable/ScriptableNode.js new file mode 100644 index 00000000000000..54cfc5b85fd3e9 --- /dev/null +++ b/examples/jsm/nodes/scriptable/ScriptableNode.js @@ -0,0 +1,434 @@ +import * as THREE from 'three'; +import * as Nodes from 'three/nodes'; + +import Node, { addNodeClass } from '../core/Node.js'; +import { scriptableValue } from './ScriptableValueNode.js'; +import { getNodeChildren } from '../core/NodeUtils.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.editor = null; // defined by editor + + this.onRefresh = this.onRefresh.bind( this ); + + this.isScriptableNode = true; + + } + + get source() { + + return this.codeNode ? this.codeNode.code : ''; + + } + + onRefresh() { + + this.needsUpdate = true; + + this.getDefaultOutput(); + + } + + 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.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; + + } + + getParameter( name ) { + + return this.parameters[ name ]; + + } + + deleteParameter( name ) { + + if ( this.parameters[ name ] ) { + + this.parameters[ name ].events.removeEventListener( 'refresh', this.onRefresh ); + + delete this.parameters[ name ]; + + } + + 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._output.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 method = this.getMethod( this.codeNode ); + const params = [ parameters, this.local, global, refresh, setOutput, THREE, Nodes, this.editor ]; + + 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; + + 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; + + } + + getLayout() { + + return this.getObject().layout; + + } + + getDefaultOutputNode() { + + const output = this.getDefaultOutput().value; + + if ( output && output.isNode ) { + + return output; + + } + + return float(); + + } + + getDefaultOutput() { + + if ( this._needsOutputUpdate === true ) { + + this._value = this.call( 'main' ); + + this._needsOutputUpdate = false; + + } + + this._output.value = this._value; + + return this._output; + + } + + getMethod() { + + if ( this.needsUpdate ) this.dispose(); + if ( this._method !== null ) return this._method; + + // + + const parametersProps = [ 'parameters', 'local', 'global', 'refresh', 'setOutput', 'THREE', 'TSL', 'editor' ]; + const interfaceProps = [ 'layout', 'init', 'main', 'dispose' ]; + + const properties = interfaceProps.join( ', ' ); + const declarations = 'var ' + properties + ';\n'; + const returns = '\nreturn { ' + 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; + + } + +} + +export default ScriptableNode; + +export const scriptable = nodeProxy( ScriptableNode ); + +addNodeElement( 'scriptable', scriptable ); + +addNodeClass( ScriptableNode ); diff --git a/examples/jsm/nodes/scriptable/ScriptableValueNode.js b/examples/jsm/nodes/scriptable/ScriptableValueNode.js new file mode 100644 index 00000000000000..bd539c9fa30c9a --- /dev/null +++ b/examples/jsm/nodes/scriptable/ScriptableValueNode.js @@ -0,0 +1,158 @@ +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 && this.inputType === 'URL' && typeof value.value === 'string' ) { + + 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 ); From a5ea8a7cf9710ee1d15d2b36963787ba86552c75 Mon Sep 17 00:00:00 2001 From: sunag Date: Thu, 16 Mar 2023 23:57:26 -0300 Subject: [PATCH 10/47] Added scriptable example and serialization test. --- examples/webgpu_materials.html | 60 +++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/examples/webgpu_materials.html b/examples/webgpu_materials.html index 0aa8ec5017278f..171898fb810304 100644 --- a/examples/webgpu_materials.html +++ b/examples/webgpu_materials.html @@ -27,7 +27,7 @@