Jelajahi Sumber

TSL Transpiler: GLSL -> TSL (#26982)

* tsl transpiler - initial version

* update syntax

* fix names

* fix title

* update thumbnail

* more operators and initial boolean

* update primitives

* cleanup

* cleanup

* update

* conditional

* unary, qualifier

* update test

* unary before

* imports, variables, for to loop or while

* cleanup

* ShaderToyDecoder, example and revision

* update title

* fit color with shadertoy

* cleanup

* fix rename .value to .property

* array accessor test

* revisions

* revision

* revision

* ternary test

* cleanup

* TSL: Primitive conversion for vectors

* Add ParameterNode

* transpile mutable parameters

* sync eval() using iife and revision beautifier

* cleanup

* cleanup

* Nodes -> TSL

* cleanup

* revision

* cleanup

* cleanup

* cleanup

* revisions

* revision

* revision

* update

* fixes

* revision

* update

* update

* update
sunag 1 tahun lalu
induk
melakukan
328ec21f59

+ 2 - 0
examples/files.json

@@ -343,12 +343,14 @@
 		"webgpu_particles",
 		"webgpu_rtt",
 		"webgpu_sandbox",
+		"webgpu_shadertoy",
 		"webgpu_shadowmap",
 		"webgpu_skinning",
 		"webgpu_skinning_instancing",
 		"webgpu_skinning_points",
 		"webgpu_sprites",
 		"webgpu_tsl_editor",
+		"webgpu_tsl_transpiler",
 		"webgpu_video_panorama"
 	],
 	"webaudio": [

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

@@ -26,6 +26,7 @@ export { default as NodeKeywords } from './core/NodeKeywords.js';
 export { default as NodeUniform } from './core/NodeUniform.js';
 export { default as NodeVar } from './core/NodeVar.js';
 export { default as NodeVarying } from './core/NodeVarying.js';
+export { default as ParameterNode, parameter } from './core/ParameterNode.js';
 export { default as PropertyNode, property, output, diffuseColor, roughness, metalness, clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, specularColor, shininess, dashSize, gapSize, pointWidth } from './core/PropertyNode.js';
 export { default as StackNode, stack } from './core/StackNode.js';
 export { default as TempNode } from './core/TempNode.js';

+ 43 - 5
examples/jsm/nodes/core/NodeBuilder.js

@@ -5,7 +5,7 @@ import NodeVar from './NodeVar.js';
 import NodeCode from './NodeCode.js';
 import NodeKeywords from './NodeKeywords.js';
 import NodeCache from './NodeCache.js';
-import PropertyNode from './PropertyNode.js';
+import ParameterNode from './ParameterNode.js';
 import { createNodeMaterialFromType } from '../materials/NodeMaterial.js';
 import { NodeUpdateType, defaultBuildStages, shaderStages } from './constants.js';
 
@@ -817,11 +817,28 @@ class NodeBuilder {
 	flowShaderNode( shaderNode ) {
 
 		const layout = shaderNode.layout;
-		const inputs = {};
 
-		for ( const input of layout.inputs ) {
+		let inputs;
 
-			inputs[ input.name ] = new PropertyNode( input.type, input.name, false )
+		if ( shaderNode.isArrayInput ) {
+
+			inputs = [];
+
+			for ( const input of layout.inputs ) {
+
+				inputs.push( new ParameterNode( input.type, input.name ) );
+
+			}
+
+		} else {
+
+			inputs = {};
+
+			for ( const input of layout.inputs ) {
+
+				inputs[ input.name ] = new ParameterNode( input.type, input.name );
+
+			}
 
 		}
 
@@ -1087,6 +1104,18 @@ class NodeBuilder {
 
 	}
 
+	getPrimitiveType( type ) {
+
+		let primitiveType;
+
+		if ( type[ 0 ] === 'i' ) primitiveType = 'int';
+		else if ( type[ 0 ] === 'u' ) primitiveType = 'uint';
+		else primitiveType = 'float';
+
+		return primitiveType;
+
+	}
+
 	format( snippet, fromType, toType ) {
 
 		fromType = this.getVectorType( fromType );
@@ -1129,7 +1158,7 @@ class NodeBuilder {
 
 		}
 
-		if ( toTypeLength === 4 ) { // toType is vec4-like
+		if ( toTypeLength === 4 && fromTypeLength > 1 ) { // toType is vec4-like
 
 			return `${ this.getType( toType ) }( ${ this.format( snippet, fromType, 'vec3' ) }, 1.0 )`;
 
@@ -1141,6 +1170,15 @@ class NodeBuilder {
 
 		}
 
+		if ( fromTypeLength === 1 && toTypeLength > 1 && fromType[ 0 ] !== toType[ 0 ] ) { // fromType is float-like
+
+			// convert a number value to vector type, e.g:
+			// vec3( 1u ) -> vec3( float( 1u ) )
+
+			snippet = `${ this.getType( this.getPrimitiveType( toType ) ) }( ${ snippet } )`;
+
+		}
+
 		return `${ this.getType( toType ) }( ${ snippet } )`; // fromType is float-like
 
 	}

+ 4 - 2
examples/jsm/nodes/core/NodeUtils.js

@@ -137,9 +137,11 @@ export function getValueFromType( type, ...params ) {
 
 	const last4 = type ? type.slice( - 4 ) : undefined;
 
-	if ( ( last4 === 'vec2' || last4 === 'vec3' || last4 === 'vec4' ) && params.length === 1 ) { // ensure same behaviour as in NodeBuilder.format()
+	if ( params.length === 1 ) { // ensure same behaviour as in NodeBuilder.format()
 
-		params = last4 === 'vec2' ? [ params[ 0 ], params[ 0 ] ] : [ params[ 0 ], params[ 0 ], params[ 0 ] ];
+		if ( last4 === 'vec2' ) params = [ params[ 0 ], params[ 0 ] ];
+		else if ( last4 === 'vec3' ) params = [ params[ 0 ], params[ 0 ], params[ 0 ] ];
+		else if ( last4 === 'vec4' ) params = [ params[ 0 ], params[ 0 ], params[ 0 ], params[ 0 ] ];
 
 	}
 

+ 33 - 0
examples/jsm/nodes/core/ParameterNode.js

@@ -0,0 +1,33 @@
+import { addNodeClass } from './Node.js';
+import { nodeObject } from '../shadernode/ShaderNode.js';
+import PropertyNode from './PropertyNode.js';
+
+class ParameterNode extends PropertyNode {
+
+	constructor( nodeType, name = null ) {
+
+		super( nodeType, name );
+
+		this.isParameterNode = true;
+
+	}
+
+	getHash() {
+
+		return this.uuid;
+
+	}
+
+	generate() {
+
+		return this.name;
+
+	}
+
+}
+
+export default ParameterNode;
+
+export const parameter = ( type, name ) => nodeObject( new ParameterNode( type, name ) );
+
+addNodeClass( 'ParameterNode', ParameterNode );

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

@@ -3,12 +3,11 @@ import { nodeImmutable, nodeObject } from '../shadernode/ShaderNode.js';
 
 class PropertyNode extends Node {
 
-	constructor( nodeType, name = null, declare = true ) {
+	constructor( nodeType, name = null ) {
 
 		super( nodeType );
 
 		this.name = name;
-		this.declare = declare;
 
 		this.isPropertyNode = true;
 
@@ -28,8 +27,6 @@ class PropertyNode extends Node {
 
 	generate( builder ) {
 
-		if ( this.declare === false ) return this.name;
-
 		return builder.getPropertyName( builder.getVarFromNode( this, this.name ) );
 
 	}

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

@@ -1,6 +1,4 @@
 import Node, { addNodeClass } from './Node.js';
-import { bypass } from '../core/BypassNode.js';
-import { expression } from '../code/ExpressionNode.js';
 import { cond } from '../math/CondNode.js';
 import { ShaderNode, nodeProxy, getCurrentStack, setCurrentStack } from '../shadernode/ShaderNode.js';
 
@@ -29,7 +27,7 @@ class StackNode extends Node {
 
 	add( node ) {
 
-		this.nodes.push( bypass( expression(), node ) );
+		this.nodes.push( node );
 
 		return this;
 

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

@@ -52,6 +52,7 @@ export default VarNode;
 
 export const temp = nodeProxy( VarNode );
 
-addNodeElement( 'temp', temp );
+addNodeElement( 'temp', temp ); // @TODO: Will be removed in the future
+addNodeElement( 'toVar', ( ...params ) => temp( ...params ).append() );
 
 addNodeClass( 'VarNode', VarNode );

+ 14 - 4
examples/jsm/nodes/display/ViewportNode.js

@@ -81,9 +81,7 @@ class ViewportNode extends Node {
 			let outX = output.x;
 			let outY = output.y;
 
-			if ( /top/i.test( scope ) && builder.isFlipY() ) outY = outY.oneMinus();
-			else if ( /bottom/i.test( scope ) && builder.isFlipY() === false ) outY = outY.oneMinus();
-
+			if ( /bottom/i.test( scope ) ) outY = outY.oneMinus();
 			if ( /right/i.test( scope ) ) outX = outX.oneMinus();
 
 			output = vec2( outX, outY );
@@ -98,7 +96,19 @@ class ViewportNode extends Node {
 
 		if ( this.scope === ViewportNode.COORDINATE ) {
 
-			return builder.getFragCoord();
+			let coord = builder.getFragCoord();
+
+			if ( builder.isFlipY() ) {
+
+				// follow webgpu standards
+
+				const resolution = viewportResolution.build( builder );
+
+				coord = `${ builder.getType( 'vec2' ) }( ${ coord }.x, ${ resolution}.y - ${ coord }.y )`;
+
+			}
+
+			return coord;
 
 		}
 

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

@@ -609,10 +609,10 @@ export const mx_perlin_noise_vec3 = glslFn( 'vec3 mx_perlin_noise_vec3( any p )'
 export const mx_cell_noise_float = glslFn( 'float mx_cell_noise_float( vec3 p )', includes );
 
 export const mx_worley_noise_float = glslFn( 'float mx_worley_noise_float( any p, float jitter, int metric )', includes );
-export const mx_worley_noise_vec2 = glslFn( 'float mx_worley_noise_vec2( any p, float jitter, int metric )', includes );
-export const mx_worley_noise_vec3 = glslFn( 'float mx_worley_noise_vec3( any p, float jitter, int metric )', includes );
+export const mx_worley_noise_vec2 = glslFn( 'vec2 mx_worley_noise_vec2( any p, float jitter, int metric )', includes );
+export const mx_worley_noise_vec3 = glslFn( 'vec3 mx_worley_noise_vec3( any p, float jitter, int metric )', includes );
 
 export const mx_fractal_noise_float = glslFn( 'float mx_fractal_noise_float( vec3 p, int octaves, float lacunarity, float diminish )', includes );
-export const mx_fractal_noise_vec2 = glslFn( 'float mx_fractal_noise_vec2( vec3 p, int octaves, float lacunarity, float diminish )', includes );
-export const mx_fractal_noise_vec3 = glslFn( 'float mx_fractal_noise_vec3( vec3 p, int octaves, float lacunarity, float diminish )', includes );
-export const mx_fractal_noise_vec4 = glslFn( 'float mx_fractal_noise_vec4( vec3 p, int octaves, float lacunarity, float diminish )', includes );
+export const mx_fractal_noise_vec2 = glslFn( 'vec2 mx_fractal_noise_vec2( vec3 p, int octaves, float lacunarity, float diminish )', includes );
+export const mx_fractal_noise_vec3 = glslFn( 'vec3 mx_fractal_noise_vec3( vec3 p, int octaves, float lacunarity, float diminish )', includes );
+export const mx_fractal_noise_vec4 = glslFn( 'vec4 mx_fractal_noise_vec4( vec3 p, int octaves, float lacunarity, float diminish )', includes );

+ 2 - 0
examples/jsm/nodes/math/OperatorNode.js

@@ -216,6 +216,7 @@ export const mul = nodeProxy( OperatorNode, '*' );
 export const div = nodeProxy( OperatorNode, '/' );
 export const remainder = nodeProxy( OperatorNode, '%' );
 export const equal = nodeProxy( OperatorNode, '==' );
+export const notEqual = nodeProxy( OperatorNode, '!=' );
 export const lessThan = nodeProxy( OperatorNode, '<' );
 export const greaterThan = nodeProxy( OperatorNode, '>' );
 export const lessThanEqual = nodeProxy( OperatorNode, '<=' );
@@ -235,6 +236,7 @@ addNodeElement( 'mul', mul );
 addNodeElement( 'div', div );
 addNodeElement( 'remainder', remainder );
 addNodeElement( 'equal', equal );
+addNodeElement( 'notEqual', notEqual );
 addNodeElement( 'lessThan', lessThan );
 addNodeElement( 'greaterThan', greaterThan );
 addNodeElement( 'lessThanEqual', lessThanEqual );

+ 39 - 6
examples/jsm/nodes/shadernode/ShaderNode.js

@@ -271,12 +271,12 @@ class ShaderCallNodeInternal extends Node {
 
 			}
 
-			return nodeObject( functionNode.call( nodeObjects( inputNodes ) ) );
+			return nodeObject( functionNode.call( inputNodes ) );
 
 		}
 
 		const jsFunc = shaderNode.jsFunc;
-		const outputNode = inputNodes !== null ? jsFunc( nodeObjects( inputNodes ), builder.stack, builder ) : jsFunc( builder.stack, builder );
+		const outputNode = inputNodes !== null ? jsFunc( inputNodes, builder.stack, builder ) : jsFunc( builder.stack, builder );
 
 		return nodeObject( outputNode );
 
@@ -321,6 +321,12 @@ class ShaderNodeInternal extends Node {
 
 	}
 
+	get isArrayInput() {
+
+		return /^\(\s+?\[/.test( this.jsFunc.toString() );
+
+	}
+
 	setLayout( layout ) {
 
 		this.layout = layout;
@@ -331,6 +337,8 @@ class ShaderNodeInternal extends Node {
 
 	call( inputs = null ) {
 
+		nodeObjects( inputs );
+
 		return nodeObject( new ShaderCallNodeInternal( this, inputs ) );
 
 	}
@@ -460,16 +468,34 @@ export const tslFn = ( jsFunc ) => {
 
 	const shaderNode = new ShaderNode( jsFunc );
 
-	const fn = ( inputs ) => shaderNode.call( inputs );
-	fn.shaderNode = shaderNode;
+	const fn = ( ...params ) => {
+
+		let inputs;
 
+		nodeObjects( params );
+
+		if ( params[ 0 ] && params[ 0 ].isNode ) {
+
+			inputs = [ ...params ];
+
+		} else {
+
+			inputs = params[ 0 ];
+
+		}
+
+		return shaderNode.call( inputs );
+
+	};
+
+	fn.shaderNode = shaderNode;
 	fn.setLayout = ( layout ) => {
 
 		shaderNode.setLayout( layout );
 
 		return fn;
 
-	}
+	};
 
 	return fn;
 
@@ -483,7 +509,14 @@ export const setCurrentStack = stack => currentStack = stack;
 export const getCurrentStack = () => currentStack;
 
 export const If = ( ...params ) => currentStack.if( ...params );
-export const append = ( ...params ) => currentStack.add( ...params );
+
+export function append( node ) {
+
+	if ( currentStack ) currentStack.add( node );
+
+	return node;
+
+}
 
 addNodeElement( 'append', append );
 

+ 11 - 1
examples/jsm/nodes/utils/JoinNode.js

@@ -28,11 +28,21 @@ class JoinNode extends TempNode {
 		const type = this.getNodeType( builder );
 		const nodes = this.nodes;
 
+		const primitiveType = builder.getPrimitiveType( type );
+
 		const snippetValues = [];
 
 		for ( const input of nodes ) {
 
-			const inputSnippet = input.build( builder );
+			let inputSnippet = input.build( builder );
+
+			const inputPrimitiveType = builder.getPrimitiveType( input.getNodeType( builder ) );
+
+			if ( inputPrimitiveType !== primitiveType ) {
+
+				inputSnippet = builder.format( inputSnippet, inputPrimitiveType, primitiveType );
+
+			}
 
 			snippetValues.push( inputSnippet );
 

+ 36 - 24
examples/jsm/nodes/utils/LoopNode.js

@@ -32,9 +32,12 @@ class LoopNode extends Node {
 
 		for ( let i = 0, l = this.params.length - 1; i < l; i ++ ) {
 
-			const prop = this.getVarName( i );
+			const param = this.params[ i ];
 
-			inputs[ prop ] = expression( prop, 'int' );
+			const name = ( param.isNode !== true && param.name ) || this.getVarName( i );
+			const type = ( param.isNode !== true && param.type ) || 'int';
+
+			inputs[ name ] = expression( name, type );
 
 		}
 
@@ -73,50 +76,54 @@ class LoopNode extends Node {
 		for ( let i = 0, l = params.length - 1; i < l; i ++ ) {
 
 			const param = params[ i ];
-			const property = this.getVarName( i );
 
-			let start = null, end = null, direction = null;
+			let start = null, end = null, name = null, type = null, condition = null, update = null;
 
 			if ( param.isNode ) {
 
+				type = 'int';
+				name = this.getVarName( i );
 				start = '0';
-				end = param.build( builder, 'int' );
-				direction = 'forward';
+				end = param.build( builder, type );
+				condition = '<';
 
 			} else {
 
+				type = param.type || 'int';
+				name = param.name || this.getVarName( i );
 				start = param.start;
 				end = param.end;
-				direction = param.direction;
+				condition = param.condition;
+				update = param.update;
 
 				if ( typeof start === 'number' ) start = start.toString();
-				else if ( start && start.isNode ) start = start.build( builder, 'int' );
+				else if ( start && start.isNode ) start = start.build( builder, type );
 
 				if ( typeof end === 'number' ) end = end.toString();
-				else if ( end && end.isNode ) end = end.build( builder, 'int' );
+				else if ( end && end.isNode ) end = end.build( builder, type );
 
 				if ( start !== undefined && end === undefined ) {
 
 					start = start + ' - 1';
 					end = '0';
-					direction = 'backwards';
+					condition = '>=';
 
 				} else if ( end !== undefined && start === undefined ) {
 
 					start = '0';
-					direction = 'forward';
+					condition = '<';
 
 				}
 
-				if ( direction === undefined ) {
+				if ( condition === undefined ) {
 
 					if ( Number( start ) > Number( end ) ) {
 
-						direction = 'backwards';
+						condition = '>=';
 
 					} else {
 
-						direction = 'forward';
+						condition = '<';
 
 					}
 
@@ -124,7 +131,7 @@ class LoopNode extends Node {
 
 			}
 
-			const internalParam = { start, end, direction };
+			const internalParam = { start, end, condition };
 
 			//
 
@@ -135,22 +142,27 @@ class LoopNode extends Node {
 			let conditionalSnippet = '';
 			let updateSnippet = '';
 
-			declarationSnippet += builder.getVar( 'int', property ) + ' = ' + startSnippet;
+			if ( ! update ) {
 
-			if ( internalParam.direction === 'backwards' ) {
+				if ( type === 'int' ) {
 
-				conditionalSnippet += property + ' >= ' + endSnippet;
-				updateSnippet += property + ' --';
+					if ( condition.includes( '<' ) ) update = '++';
+					else update = '--';
 
-			} else {
+				} else {
 
-				// forward
+					if ( condition.includes( '<' ) ) update = '+= 1';
+					else update = '-= 1';
 
-				conditionalSnippet += property + ' < ' + endSnippet;
-				updateSnippet += property + ' ++';
+				}
 
 			}
 
+			declarationSnippet += builder.getVar( type, name ) + ' = ' + startSnippet;
+
+			conditionalSnippet += name + ' ' + condition + ' ' + endSnippet;
+			updateSnippet += name + ' ' + update;
+
 			const forSnippet = `for ( ${ declarationSnippet }; ${ conditionalSnippet }; ${ updateSnippet } )`;
 
 			builder.addFlowCode( ( i === 0 ? '\n' : '' ) + builder.tab + forSnippet + ' {\n\n' ).addFlowTab();
@@ -179,7 +191,7 @@ class LoopNode extends Node {
 
 export default LoopNode;
 
-export const loop = ( ...params ) => nodeObject( new LoopNode( nodeArray( params, 'int' ) ) );
+export const loop = ( ...params ) => nodeObject( new LoopNode( nodeArray( params, 'int' ) ) ).append();
 
 addNodeElement( 'loop', ( returns, ...params ) => bypass( returns, loop( ...params ) ) );
 

+ 9 - 5
examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js

@@ -117,11 +117,15 @@ ${ flowData.code }
 
 		const vars = this.vars[ shaderStage ];
 
-		for ( const variable of vars ) {
+		if ( vars !== undefined ) {
 
-			if ( variable.isOutputStructVar ) continue;
+			for ( const variable of vars ) {
 
-			snippets.push( `${ this.getVar( variable.type, variable.name ) };` );
+				if ( variable.isOutputStructVar ) continue;
+
+				snippets.push( `${ this.getVar( variable.type, variable.name ) };` );
+
+			}
 
 		}
 
@@ -259,7 +263,7 @@ ${ flowData.code }
 
 		if ( structs.length === 0 ) {
 
-			return "layout( location = 0 ) out vec4 fragColor;\n";
+			return 'layout( location = 0 ) out vec4 fragColor;\n';
 
 		}
 
@@ -267,7 +271,7 @@ ${ flowData.code }
 
 			const struct = structs[ index ];
 
-			let snippet = `\n`;
+			let snippet = '\n';
 			snippet += this.getStructMembers( struct );
 			snippet += '\n';
 

+ 231 - 0
examples/jsm/transpiler/AST.js

@@ -0,0 +1,231 @@
+export class Program {
+
+	constructor() {
+
+		this.body = [];
+
+		this.isProgram = true;
+
+	}
+
+}
+
+export class VariableDeclaration {
+
+	constructor( type, name, value = null, next = null, immutable = false ) {
+
+		this.type = type;
+		this.name = name;
+		this.value = value;
+		this.next = next;
+
+		this.immutable = immutable;
+
+		this.isVariableDeclaration = true;
+
+	}
+
+}
+
+export class FunctionParameter {
+
+	constructor( type, name, qualifier = null, immutable = true ) {
+
+		this.type = type;
+		this.name = name;
+		this.qualifier = qualifier;
+		this.immutable = immutable;
+
+		this.isFunctionParameter = true;
+
+	}
+
+}
+
+export class FunctionDeclaration {
+
+	constructor( type, name, params = [] ) {
+
+		this.type = type;
+		this.name = name;
+		this.params = params;
+		this.body = [];
+
+		this.isFunctionDeclaration = true;
+
+	}
+
+}
+
+export class Expression {
+
+	constructor( expression ) {
+
+		this.expression = expression;
+
+		this.isExpression = true;
+
+	}
+
+}
+
+export class Ternary {
+
+	constructor( cond, left, right ) {
+
+		this.cond = cond;
+		this.left = left;
+		this.right = right;
+
+		this.isTernary = true;
+
+	}
+
+}
+
+export class Operator {
+
+	constructor( type, left, right ) {
+
+		this.type = type;
+		this.left = left;
+		this.right = right;
+
+		this.isOperator = true;
+
+	}
+
+}
+
+
+export class Unary {
+
+	constructor( type, expression, after = false ) {
+
+		this.type = type;
+		this.expression = expression;
+		this.after = after;
+
+		this.isUnary = true;
+
+	}
+
+}
+
+export class Number {
+
+	constructor( value, type = 'float' ) {
+
+		this.type = type;
+		this.value = value;
+
+		this.isNumber = true;
+
+	}
+
+}
+
+export class Conditional {
+
+	constructor( cond = null ) {
+
+		this.cond = cond;
+
+		this.body = [];
+		this.elseConditional = null;
+
+		this.isConditional = true;
+
+	}
+
+}
+
+export class FunctionCall {
+
+	constructor( name, params = [] ) {
+
+		this.name = name;
+		this.params = params;
+
+		this.isFunctionCall = true;
+
+	}
+
+}
+
+export class Return {
+
+	constructor( value ) {
+
+		this.value = value;
+
+		this.isReturn = true;
+
+	}
+
+}
+
+export class Accessor {
+
+	constructor( property ) {
+
+		this.property = property;
+
+		this.isAccessor = true;
+
+	}
+
+}
+
+export class StaticElement {
+
+	constructor( value ) {
+
+		this.value = value;
+
+		this.isStaticElement = true;
+
+	}
+
+}
+
+export class DynamicElement {
+
+	constructor( value ) {
+
+		this.value = value;
+
+		this.isDynamicElement = true;
+
+	}
+
+}
+
+export class AccessorElements {
+
+	constructor( property, elements = [] ) {
+
+		this.property = property;
+		this.elements = elements;
+
+		this.isAccessorElements = true;
+
+	}
+
+}
+
+export class For {
+
+	constructor( initialization, condition, afterthought ) {
+
+		this.initialization = initialization;
+		this.condition = condition;
+		this.afterthought = afterthought;
+
+		this.body = [];
+
+		this.isFor = true;
+
+	}
+
+}

+ 899 - 0
examples/jsm/transpiler/GLSLDecoder.js

@@ -0,0 +1,899 @@
+import { Program, FunctionDeclaration, For, AccessorElements, Ternary, DynamicElement, StaticElement, FunctionParameter, Unary, Conditional, VariableDeclaration, Operator, Number, FunctionCall, Return, Accessor } from './AST.js';
+
+const unaryOperators = [
+	'+', '-', '~', '!', '++', '--'
+];
+
+const precedenceOperators = [
+	'*', '/', '%',
+	'-', '+',
+	'<<', '>>',
+	'<', '>', '<=', '>=',
+	'==', '!=',
+	'&',
+	'^',
+	'|',
+	'&&',
+	'^^',
+	'||',
+	'?',
+	'=',
+	'+=', '-=', '*=', '/=', '%=', '^=', '&=', '|=', '<<=', '>>=',
+	','
+].reverse();
+
+const spaceRegExp = /^((\t| )\n*)+/;
+const lineRegExp = /^\n+/;
+const commentRegExp = /^\/\*[\s\S]*?\*\//;
+const inlineCommentRegExp = /^\/\/.*?(\n|$)/;
+
+const numberRegExp = /^((0x\w+)|(\.?\d+\.?\d*((e-?\d+)|\w)?))/;
+const stringDoubleRegExp = /^(\"((?:[^"\\]|\\.)*)\")/;
+const stringSingleRegExp = /^(\'((?:[^'\\]|\\.)*)\')/;
+const literalRegExp = /^[A-Za-z](\w|\.)*/;
+const operatorsRegExp = new RegExp( '^(\\' + [
+	'<<=', '>>=', '++', '--', '<<', '>>', '+=', '-=', '*=', '/=', '%=', '&=', '^^', '^=', '|=',
+	'<=', '>=', '==', '!=', '&&', '||',
+	'(', ')', '[', ']', '{', '}',
+	'.', ',', ';', '!', '=', '~', '*', '/', '%', '+', '-', '<', '>', '&', '^', '|', '?', ':', '#'
+].join( '$' ).split( '' ).join( '\\' ).replace( /\\\$/g, '|' ) + ')' );
+
+function getGroupDelta( str ) {
+
+	if ( str === '(' || str === '[' || str === '{' ) return 1;
+	if ( str === ')' || str === ']' || str === '}' ) return - 1;
+
+	return 0;
+
+}
+
+class Token {
+
+	constructor( tokenizer, type, str, pos ) {
+
+		this.tokenizer = tokenizer;
+
+		this.type = type;
+
+		this.str = str;
+		this.pos = pos;
+
+		this.tag = null;
+
+	}
+
+	get endPos() {
+
+		return this.pos + this.str.length;
+
+	}
+
+	get isNumber() {
+
+		return this.type === Token.NUMBER;
+
+	}
+
+	get isString() {
+
+		return this.type === Token.STRING;
+
+	}
+
+	get isLiteral() {
+
+		return this.type === Token.LITERAL;
+
+	}
+
+	get isOperator() {
+
+		return this.type === Token.OPERATOR;
+
+	}
+
+}
+
+Token.LINE = 'line';
+Token.COMMENT = 'comment';
+Token.NUMBER = 'number';
+Token.STRING = 'string';
+Token.LITERAL = 'literal';
+Token.OPERATOR = 'operator';
+
+const TokenParserList = [
+	{ type: Token.LINE, regexp: lineRegExp, isTag: true },
+	{ type: Token.COMMENT, regexp: commentRegExp, isTag: true },
+	{ type: Token.COMMENT, regexp: inlineCommentRegExp, isTag: true },
+	{ type: Token.NUMBER, regexp: numberRegExp },
+	{ type: Token.STRING, regexp: stringDoubleRegExp, group: 2 },
+	{ type: Token.STRING, regexp: stringSingleRegExp, group: 2 },
+	{ type: Token.LITERAL, regexp: literalRegExp },
+	{ type: Token.OPERATOR, regexp: operatorsRegExp }
+];
+
+class Tokenizer {
+
+	constructor( source ) {
+
+		this.source = source;
+		this.position = 0;
+
+		this.tokens = [];
+
+	}
+
+	tokenize() {
+
+		let token = this.readToken();
+
+		while ( token ) {
+
+			this.tokens.push( token );
+
+			token = this.readToken();
+
+		}
+
+		return this;
+
+	}
+
+	skip( ...params ) {
+
+		let remainingCode = this.source.substr( this.position );
+		let i = params.length;
+
+		while ( i -- ) {
+
+			const skip = params[ i ].exec( remainingCode );
+			const skipLength = skip ? skip[ 0 ].length : 0;
+
+			if ( skipLength > 0 ) {
+
+				this.position += skipLength;
+
+				remainingCode = this.source.substr( this.position );
+
+				// re-skip, new remainingCode is generated
+				// maybe exist previous regexp non detected
+				i = params.length;
+
+			}
+
+		}
+
+		return remainingCode;
+
+	}
+
+	readToken() {
+
+		const remainingCode = this.skip( spaceRegExp );
+
+		for ( var i = 0; i < TokenParserList.length; i ++ ) {
+
+			const parser = TokenParserList[ i ];
+			const result = parser.regexp.exec( remainingCode );
+
+			if ( result ) {
+
+				const token = new Token( this, parser.type, result[ parser.group || 0 ], this.position );
+
+				this.position += token.str.length;
+
+				if ( parser.isTag ) {
+
+					const nextToken = this.readToken();
+
+					if ( nextToken ) {
+
+						nextToken.tag = token;
+
+					}
+
+					return nextToken;
+
+				}
+
+				return token;
+
+			}
+
+		}
+
+	}
+
+}
+
+const isType = ( str ) => /void|bool|float|u?int|(u|i)?vec[234]/.test( str );
+
+class GLSLDecoder {
+
+	constructor() {
+
+		this.index = 0;
+		this.tokenizer = null;
+		this.keywords = [];
+
+		this._currentFunction = null;
+
+		this.addKeyword( 'gl_FragCoord', 'vec2 gl_FragCoord = vec2( viewportCoordinate.x, viewportCoordinate.y.oneMinus() );' );
+
+	}
+
+	addKeyword( name, polyfill ) {
+
+		this.keywords.push( { name, polyfill } );
+
+		return this;
+
+	}
+
+	get tokens() {
+
+		return this.tokenizer.tokens;
+
+	}
+
+	readToken() {
+
+		return this.tokens[ this.index ++ ];
+
+	}
+
+	getToken( offset = 0 ) {
+
+		return this.tokens[ this.index + offset ];
+
+	}
+
+	getTokensUntil( str, tokens, offset = 0 ) {
+
+		const output = [];
+
+		let groupIndex = 0;
+
+		for ( let i = offset; i < tokens.length; i ++ ) {
+
+			const token = tokens[ i ];
+
+			groupIndex += getGroupDelta( token.str );
+
+			output.push( token );
+
+			if ( groupIndex === 0 && token.str === str ) {
+
+				break;
+
+			}
+
+		}
+
+		return output;
+
+	}
+
+	readTokensUntil( str ) {
+
+		const tokens = this.getTokensUntil( str, this.tokens, this.index );
+
+		this.index += tokens.length;
+
+		return tokens;
+
+	}
+
+	parseExpressionFromTokens( tokens ) {
+
+		if ( tokens.length === 0 ) return null;
+
+		const firstToken = tokens[ 0 ];
+		const lastToken = tokens[ tokens.length - 1 ];
+
+		// precedence operators
+
+		let groupIndex = 0;
+
+		for ( const operator of precedenceOperators ) {
+
+			for ( let i = 0; i < tokens.length; i ++ ) {
+
+				const token = tokens[ i ];
+
+				groupIndex += getGroupDelta( token.str );
+
+				if ( ! token.isOperator || i === 0 || i === tokens.length - 1 ) continue;
+
+				if ( groupIndex === 0 && token.str === operator ) {
+
+					if ( operator === '?' ) {
+
+						const conditionTokens = tokens.slice( 0, i );
+						const leftTokens = this.getTokensUntil( ':', tokens, i + 1 ).slice( 0, - 1 );
+						const rightTokens = tokens.slice( i + leftTokens.length + 2 );
+
+						const condition = this.parseExpressionFromTokens( conditionTokens );
+						const left = this.parseExpressionFromTokens( leftTokens );
+						const right = this.parseExpressionFromTokens( rightTokens );
+
+						return new Ternary( condition, left, right );
+
+					} else {
+
+						const left = this.parseExpressionFromTokens( tokens.slice( 0, i ) );
+						const right = this.parseExpressionFromTokens( tokens.slice( i + 1, tokens.length ) );
+
+						return this._evalOperator( new Operator( operator, left, right ) );
+
+					}
+
+				}
+
+				if ( groupIndex < 0 ) {
+
+					return this.parseExpressionFromTokens( tokens.slice( 0, i ) );
+
+				}
+
+			}
+
+		}
+
+		// unary operators (before)
+
+		if ( firstToken.isOperator ) {
+
+			for ( const operator of unaryOperators ) {
+
+				if ( firstToken.str === operator ) {
+
+					const right = this.parseExpressionFromTokens( tokens.slice( 1 ) );
+
+					return new Unary( operator, right );
+
+				}
+
+			}
+
+		}
+
+
+		// unary operators (after)
+
+		if ( lastToken.isOperator ) {
+
+			for ( const operator of unaryOperators ) {
+
+				if ( lastToken.str === operator ) {
+
+					const left = this.parseExpressionFromTokens( tokens.slice( 0, tokens.length - 1 ) );
+
+					return new Unary( operator, left, true );
+
+				}
+
+			}
+
+		}
+
+		// groups
+
+		if ( firstToken.str === '(' ) {
+
+			const leftTokens = this.getTokensUntil( ')', tokens );
+
+			const left = this.parseExpressionFromTokens( leftTokens.slice( 1, leftTokens.length - 1 ) );
+
+			const operator = tokens[ leftTokens.length ];
+
+			if ( operator ) {
+
+				const rightTokens = tokens.slice( leftTokens.length + 1 );
+				const right = this.parseExpressionFromTokens( rightTokens );
+
+				return this._evalOperator( new Operator( operator.str, left, right ) );
+
+			}
+
+			return left;
+
+		}
+
+		// primitives and accessors
+
+		if ( firstToken.isNumber ) {
+
+			let type;
+
+			if ( /^(0x)/.test( firstToken.str ) ) type = 'int';
+			else if ( /u$/.test( firstToken.str ) ) type = 'uint';
+			else if ( /f|e|\./.test( firstToken.str ) ) type = 'float';
+			else type = 'int';
+
+			const str = firstToken.str.replace( /u$/, '' );
+
+			return new Number( str, type );
+
+		} else if ( firstToken.isLiteral ) {
+
+			if ( firstToken.str === 'return' ) {
+
+				return new Return( this.parseExpressionFromTokens( tokens.slice( 1 ) ) );
+
+			}
+
+			const secondToken = tokens[ 1 ];
+
+			if ( secondToken ) {
+
+				if ( secondToken.str === '(' ) {
+
+					// function call
+
+					const paramsTokens = this.parseFunctionParametersFromTokens( tokens.slice( 2, tokens.length - 1 ) );
+
+					return new FunctionCall( firstToken.str, paramsTokens );
+
+				} else if ( secondToken.str === '[' ) {
+
+					// array accessor
+
+					const elements = [];
+
+					let currentTokens = tokens.slice( 1 );
+
+					while ( currentTokens.length > 0 ) {
+
+						const token = currentTokens[ 0 ];
+
+						if ( token.str === '[' ) {
+
+							const accessorTokens = this.getTokensUntil( ']', currentTokens );
+
+							const element = this.parseExpressionFromTokens( accessorTokens.slice( 1, accessorTokens.length - 1 ) );
+
+							currentTokens = currentTokens.slice( accessorTokens.length );
+
+							elements.push( new DynamicElement( element ) );
+
+						} else if ( token.str === '.' ) {
+
+							const accessorTokens = currentTokens.slice( 1, 2 );
+
+							const element = this.parseExpressionFromTokens( accessorTokens );
+
+							currentTokens = currentTokens.slice( 2 );
+
+							elements.push( new StaticElement( element ) );
+
+						} else {
+
+							console.error( 'Unknown accessor expression', token );
+
+							break;
+
+						}
+
+					}
+
+					return new AccessorElements( firstToken.str, elements );
+
+				}
+
+			}
+
+			return new Accessor( firstToken.str );
+
+		}
+
+	}
+
+	parseFunctionParametersFromTokens( tokens ) {
+
+		if ( tokens.length === 0 ) return [];
+
+		const expression = this.parseExpressionFromTokens( tokens );
+		const params = [];
+
+		let current = expression;
+
+		while ( current.type === ',' ) {
+
+			params.push( current.left );
+
+			current = current.right;
+
+		}
+
+		params.push( current );
+
+		return params;
+
+	}
+
+	parseExpression() {
+
+		const tokens = this.readTokensUntil( ';' );
+
+		const exp = this.parseExpressionFromTokens( tokens.slice( 0, tokens.length - 1 ) );
+
+		return exp;
+
+	}
+
+	parseFunctionParams( tokens ) {
+
+		const params = [];
+
+		for ( let i = 0; i < tokens.length; i ++ ) {
+
+			const immutable = tokens[ i ].str === 'const';
+			if ( immutable ) i ++;
+
+			let qualifier = tokens[ i ].str;
+
+			if ( /^(in|out|inout)$/.test( qualifier ) ) {
+
+				i ++;
+
+			} else {
+
+				qualifier = null;
+
+			}
+
+			const type = tokens[ i ++ ].str;
+			const name = tokens[ i ++ ].str;
+
+			params.push( new FunctionParameter( type, name, qualifier, immutable ) );
+
+			if ( tokens[ i ] && tokens[ i ].str !== ',' ) throw new Error( 'Expected ","' );
+
+		}
+
+		return params;
+
+	}
+
+	parseFunction() {
+
+		const type = this.readToken().str;
+		const name = this.readToken().str;
+
+		const paramsTokens = this.readTokensUntil( ')' );
+
+		const params = this.parseFunctionParams( paramsTokens.slice( 1, paramsTokens.length - 1 ) );
+
+		const func = new FunctionDeclaration( type, name, params );
+
+		this._currentFunction = func;
+
+		this.parseBlock( func );
+
+		this._currentFunction = null;
+
+		return func;
+
+	}
+
+	parseVariablesFromToken( tokens, type ) {
+
+		let index = 0;
+		const immutable = tokens[ 0 ].str === 'const';
+
+		if ( immutable ) index ++;
+
+		type = type || tokens[ index ++ ].str;
+		const name = tokens[ index ++ ].str;
+
+		const token = tokens[ index ];
+
+		let init = null;
+		let next = null;
+
+		if ( token ) {
+
+			const initTokens = this.getTokensUntil( ',', tokens, index );
+
+			if ( initTokens[ 0 ].str === '=' ) {
+
+				const expressionTokens = initTokens.slice( 1 );
+				if ( expressionTokens[ expressionTokens.length - 1 ].str === ',' ) expressionTokens.pop();
+
+				init = this.parseExpressionFromTokens( expressionTokens );
+
+			}
+
+			const nextTokens = tokens.slice( initTokens.length + ( index - 1 ) );
+
+			if ( nextTokens[ 0 ] && nextTokens[ 0 ].str === ',' ) {
+
+				next = this.parseVariablesFromToken( nextTokens.slice( 1 ), type );
+
+			}
+
+		}
+
+		const variable = new VariableDeclaration( type, name, init, next, immutable );
+
+		return variable;
+
+	}
+
+	parseVariables() {
+
+		const tokens = this.readTokensUntil( ';' );
+
+		return this.parseVariablesFromToken( tokens.slice( 0, tokens.length - 1 ) );
+
+	}
+
+	parseReturn() {
+
+		this.readToken(); // skip 'return'
+
+		const expression = this.parseExpression();
+
+		return new Return( expression );
+
+	}
+
+	parseFor() {
+
+		this.readToken(); // skip 'for'
+
+		const forTokens = this.readTokensUntil( ')' ).slice( 1, - 1 );
+
+		const initializationTokens = this.getTokensUntil( ';', forTokens, 0 ).slice( 0, - 1 );
+		const conditionTokens = this.getTokensUntil( ';', forTokens, initializationTokens.length + 1 ).slice( 0, - 1 );
+		const afterthoughtTokens = forTokens.slice( initializationTokens.length + conditionTokens.length + 2 );
+
+		let initialization;
+
+		if ( initializationTokens[ 0 ] && isType( initializationTokens[ 0 ].str ) ) {
+
+			initialization = this.parseVariablesFromToken( initializationTokens );
+
+		} else {
+
+			initialization = this.parseExpressionFromTokens( initializationTokens );
+
+		}
+
+		const condition = this.parseExpressionFromTokens( conditionTokens );
+		const afterthought = this.parseExpressionFromTokens( afterthoughtTokens );
+
+		const statement = new For( initialization, condition, afterthought );
+
+		if ( this.getToken().str === '{' ) {
+
+			this.parseBlock( statement );
+
+		} else {
+
+			statement.body.push( this.parseExpression() );
+
+		}
+
+		return statement;
+
+	}
+
+	parseIf() {
+
+		const parseIfExpression = () => {
+
+			this.readToken(); // skip 'if'
+
+			const condTokens = this.readTokensUntil( ')' );
+
+			return this.parseExpressionFromTokens( condTokens.slice( 1, condTokens.length - 1 ) );
+
+		};
+
+		const parseIfBlock = ( cond ) => {
+
+			if ( this.getToken().str === '{' ) {
+
+				this.parseBlock( cond );
+
+			} else {
+
+				cond.body.push( this.parseExpression() );
+
+			}
+
+		};
+
+		//
+
+		const conditional = new Conditional( parseIfExpression() );
+
+		parseIfBlock( conditional );
+
+		//
+
+		let current = conditional;
+
+		while ( this.getToken().str === 'else' ) {
+
+			this.readToken(); // skip 'else'
+
+			const previous = current;
+
+			if ( this.getToken().str === 'if' ) {
+
+				current = new Conditional( parseIfExpression() );
+
+			} else {
+
+				current = new Conditional();
+
+			}
+
+			previous.elseConditional = current;
+
+			parseIfBlock( current );
+
+		}
+
+		return conditional;
+
+	}
+
+	parseBlock( scope ) {
+
+		const firstToken = this.getToken();
+
+		if ( firstToken.str === '{' ) {
+
+			this.readToken(); // skip '{'
+
+		}
+
+		let groupIndex = 0;
+
+		while ( this.index < this.tokens.length ) {
+
+			const token = this.getToken();
+
+			let statement = null;
+
+			groupIndex += getGroupDelta( token.str );
+
+			if ( groupIndex < 0 ) {
+
+				this.readToken(); // skip '}'
+
+				break;
+
+			}
+
+			//
+
+			if ( token.isLiteral ) {
+
+				if ( token.str === 'const' ) {
+
+					statement = this.parseVariables();
+
+				} else if ( isType( token.str ) ) {
+
+					if ( this.getToken( 2 ).str === '(' ) {
+
+						statement = this.parseFunction();
+
+					} else {
+
+						statement = this.parseVariables();
+
+					}
+
+				} else if ( token.str === 'return' ) {
+
+					statement = this.parseReturn();
+
+				} else if ( token.str === 'if' ) {
+
+					statement = this.parseIf();
+
+				} else if ( token.str === 'for' ) {
+
+					statement = this.parseFor();
+
+				} else {
+
+					statement = this.parseExpression();
+
+				}
+
+			}
+
+			if ( statement ) {
+
+				scope.body.push( statement );
+
+			} else {
+
+				this.index ++;
+
+			}
+
+		}
+
+	}
+
+	_evalOperator( operator ) {
+
+		if ( operator.type.includes( '=' ) ) {
+
+			const parameter = this._getFunctionParameter( operator.left.property );
+
+			if ( parameter !== undefined ) {
+
+				// Parameters are immutable in WGSL
+
+				parameter.immutable = false;
+
+			}
+
+		}
+
+		return operator;
+
+	}
+
+	_getFunctionParameter( name ) {
+
+		if ( this._currentFunction ) {
+
+			for ( const param of this._currentFunction.params ) {
+
+				if ( param.name === name ) {
+
+					return param;
+
+				}
+
+			}
+
+		}
+
+	}
+
+	parse( source ) {
+
+		let polyfill = '';
+
+		for ( const keyword of this.keywords ) {
+
+			if ( new RegExp( `(^|\\b)${ keyword.name }($|\\b)`, 'gm' ).test( source ) ) {
+
+				polyfill += keyword.polyfill + '\n';
+
+			}
+
+		}
+
+		if ( polyfill ) {
+
+			polyfill = '// Polyfills\n\n' + polyfill + '\n';
+
+		}
+
+		this.index = 0;
+		this.tokenizer = new Tokenizer( polyfill + source ).tokenize();
+
+		const program = new Program();
+
+		this.parseBlock( program );
+
+		return program;
+
+
+	}
+
+}
+
+export default GLSLDecoder;

+ 49 - 0
examples/jsm/transpiler/ShaderToyDecoder.js

@@ -0,0 +1,49 @@
+import { Return, VariableDeclaration, Accessor } from './AST.js';
+import GLSLDecoder from './GLSLDecoder.js';
+
+class ShaderToyDecoder extends GLSLDecoder {
+
+	constructor() {
+
+		super();
+
+		this.addKeyword( 'iTime', 'float iTime = timerGlobal();' );
+		this.addKeyword( 'iResolution', 'vec2 iResolution = viewportResolution;' );
+		this.addKeyword( 'fragCoord', 'vec2 fragCoord = vec2( viewportCoordinate.x, viewportResolution.y - viewportCoordinate.y );' );
+
+	}
+
+	parseFunction() {
+
+		const node = super.parseFunction();
+
+		if ( node.name === 'mainImage' ) {
+
+			node.params = []; // remove default parameters
+			node.type = 'vec4';
+			node.layout = false; // for now
+
+			const fragColor = new Accessor( 'fragColor' );
+
+			for ( const subNode of node.body ) {
+
+				if ( subNode.isReturn ) {
+
+					subNode.value = fragColor;
+
+				}
+
+			}
+
+			node.body.unshift( new VariableDeclaration( 'vec4', 'fragColor' ) );
+			node.body.push( new Return( fragColor ) );
+
+		}
+
+		return node;
+
+	}
+
+}
+
+export default ShaderToyDecoder;

+ 611 - 0
examples/jsm/transpiler/TSLEncoder.js

@@ -0,0 +1,611 @@
+import { REVISION } from 'three';
+import { VariableDeclaration, Accessor } from './AST.js';
+import * as Nodes from 'three/nodes';
+
+const opLib = {
+	'=': 'assign',
+	'+': 'add',
+	'-': 'sub',
+	'*': 'mul',
+	'/': 'div',
+	'%': 'remainder',
+	'<': 'lessThan',
+	'>': 'greaterThan',
+	'<=': 'lessThanEqual',
+	'>=': 'greaterThanEqual',
+	'==': 'equal',
+	'&&': 'and',
+	'||': 'or',
+	'^^': 'xor',
+	'&': 'bitAnd',
+	'|': 'bitOr',
+	'^': 'bitXor',
+	'<<': 'shiftLeft',
+	'>>': 'shiftRight',
+	'+=': 'addAssign',
+	'-=': 'subAssign',
+	'*=': 'mulAssign',
+	'/=': 'divAssign',
+	'%=': 'remainderAssign',
+	'^=': 'xorAssign',
+	'&=': 'bitAndAssign',
+	'|=': 'bitOrAssign',
+	'<<=': 'shiftLeftAssign',
+	'>>=': 'shiftRightAssign'
+};
+
+const unaryLib = {
+	'+': '', // positive
+	'-': 'negate',
+	'~': 'bitNot',
+	'!': 'not',
+	'++': 'increment', // incrementBefore
+	'--': 'decrement' // decrementBefore
+};
+
+const isPrimitive = ( value ) => /^(true|false|-?\d)/.test( value );
+
+class TSLEncoder {
+
+	constructor() {
+
+		this.tab = '';
+		this.imports = new Set();
+		this.functions = new Set();
+		this.layoutsCode = '';
+		this.iife = false;
+		this.uniqueNames = false;
+
+		this._currentProperties = {};
+		this._lastStatment = null;
+
+	}
+
+	addImport( name ) {
+
+		// import only if it's a node
+
+		name = name.split( '.' )[ 0 ];
+
+		if ( Nodes[ name ] !== undefined && this.functions.has( name ) === false && this._currentProperties[ name ] === undefined ) {
+
+			this.imports.add( name );
+
+		}
+
+	}
+
+	emitExpression( node ) {
+
+		let code;
+
+		if ( node.isAccessor ) {
+
+			this.addImport( node.property );
+
+			code = node.property;
+
+		} else if ( node.isNumber ) {
+
+			if ( node.type === 'int' || node.type === 'uint' ) {
+
+				code = node.type + '( ' + node.value + ' )';
+
+				this.addImport( node.type );
+
+			} else {
+
+				code = node.value;
+
+			}
+
+		} else if ( node.isOperator ) {
+
+			const opFn = opLib[ node.type ] || node.type;
+
+			const left = this.emitExpression( node.left );
+			const right = this.emitExpression( node.right );
+
+			if ( isPrimitive( left ) && isPrimitive( right ) ) {
+
+				return left + ' ' + node.type + ' ' + right;
+
+			}
+
+			if ( isPrimitive( left ) ) {
+
+				code = opFn + '( ' + left + ', ' + right + ' )';
+
+				this.addImport( opFn );
+
+			} else {
+
+				code = left + '.' + opFn + '( ' + right + ' )';
+
+			}
+
+		} else if ( node.isFunctionCall ) {
+
+			const params = [];
+
+			for ( const parameter of node.params ) {
+
+				params.push( this.emitExpression( parameter ) );
+
+			}
+
+			this.addImport( node.name );
+
+			const paramsStr = params.length > 0 ? ' ' + params.join( ', ' ) + ' ' : '';
+
+			code = `${ node.name }(${ paramsStr })`;
+
+		} else if ( node.isReturn ) {
+
+			code = 'return';
+
+			if ( node.value ) {
+
+				code += ' ' + this.emitExpression( node.value );
+
+			}
+
+		} else if ( node.isAccessorElements ) {
+
+			code = node.property;
+
+			for ( const element of node.elements ) {
+
+				if ( element.isStaticElement ) {
+
+					code += '.' + this.emitExpression( element.value );
+
+				} else if ( element.isDynamicElement ) {
+
+					const value = this.emitExpression( element.value );
+
+					if ( isPrimitive( value ) ) {
+
+						code += `[ ${ value } ]`;
+
+					} else {
+
+						code += `.element( ${ value } )`;
+
+					}
+
+				}
+
+			}
+
+		} else if ( node.isDynamicElement ) {
+
+			code = this.emitExpression( node.value );
+
+		} else if ( node.isStaticElement ) {
+
+			code = this.emitExpression( node.value );
+
+		} else if ( node.isFor ) {
+
+			code = this.emitFor( node );
+
+		} else if ( node.isVariableDeclaration ) {
+
+			code = this.emitVariables( node );
+
+		} else if ( node.isTernary ) {
+
+			code = this.emitTernary( node );
+
+		} else if ( node.isConditional ) {
+
+			code = this.emitConditional( node );
+
+		} else if ( node.isUnary && node.expression.isNumber ) {
+
+			code = node.type + node.expression.value;
+
+		} else if ( node.isUnary ) {
+
+			let type = unaryLib[ node.type ];
+
+			if ( node.after === false && ( node.type === '++' || node.type === '--' ) ) {
+
+				type += 'Before';
+
+			}
+
+			const exp = this.emitExpression( node.expression );
+
+			if ( isPrimitive( exp ) ) {
+
+				this.addImport( type );
+
+				code = type + '( ' + exp + ' )';
+
+			} else {
+
+				code = exp + '.' + type + '()';
+
+			}
+
+		} else {
+
+			console.error( 'Unknown node type', node );
+
+		}
+
+		if ( ! code ) code = '/* unknown statement */';
+
+		return code;
+
+	}
+
+	emitBody( body ) {
+
+		this.setLastStatement( null );
+
+		let code = '';
+
+		this.tab += '\t';
+
+		for ( const statement of body ) {
+
+			code += this.emitExtraLine( statement );
+			code += this.tab + this.emitExpression( statement );
+
+			if ( code.slice( - 1 ) !== '}' ) code += ';';
+
+			code += '\n';
+
+			this.setLastStatement( statement );
+
+		}
+
+		code = code.slice( 0, - 1 ); // remove the last extra line
+
+		this.tab = this.tab.slice( 0, - 1 );
+
+		return code;
+
+
+	}
+
+	emitTernary( node ) {
+
+		const condStr = this.emitExpression( node.cond );
+		const leftStr = this.emitExpression( node.left );
+		const rightStr = this.emitExpression( node.right );
+
+		this.addImport( 'cond' );
+
+		return `cond( ${ condStr }, ${ leftStr }, ${ rightStr } )`;
+
+	}
+
+	emitConditional( node ) {
+
+		const condStr = this.emitExpression( node.cond );
+		const bodyStr = this.emitBody( node.body );
+
+		let ifStr = `If( ${ condStr }, () => {
+
+${ bodyStr } 
+
+${ this.tab }} )`;
+
+		let current = node;
+
+		while ( current.elseConditional ) {
+
+			const elseBodyStr = this.emitBody( current.elseConditional.body );
+
+			if ( current.elseConditional.cond ) {
+
+				const elseCondStr = this.emitExpression( current.elseConditional.cond );
+
+				ifStr += `.elseif( ( ${ elseCondStr } ) => {
+
+${ elseBodyStr }
+
+${ this.tab }} )`;
+
+			} else {
+
+				ifStr += `.else( () => {
+
+${ elseBodyStr }
+
+${ this.tab }} )`;
+
+			}
+
+			current = current.elseConditional;
+
+
+		}
+
+		this.imports.add( 'If' );
+
+		return ifStr;
+
+	}
+
+	emitLoop( node ) {
+
+		const start = this.emitExpression( node.initialization.value );
+		const end = this.emitExpression( node.condition.right );
+
+		const name = node.initialization.name;
+		const type = node.initialization.type;
+		const condition = node.condition.type;
+		const update = node.afterthought.type;
+
+		const nameParam = name !== 'i' ? `, name: '${ name }'` : '';
+		const typeParam = type !== 'int' ? `, type: '${ type }'` : '';
+		const conditionParam = condition !== '<' ? `, condition: '${ condition }'` : '';
+		const updateParam = update !== '++' ? `, update: '${ update }'` : '';
+
+		let loopStr = `loop( { start: ${ start }, end: ${ end + nameParam + typeParam + conditionParam + updateParam } }, ( { ${ name } } ) => {\n\n`;
+
+		loopStr += this.emitBody( node.body ) + '\n\n';
+
+		loopStr += this.tab + '} )';
+
+		this.imports.add( 'loop' );
+
+		return loopStr;
+
+	}
+
+	emitFor( node ) {
+
+		const { initialization, condition, afterthought } = node;
+
+		if ( ( initialization && initialization.isVariableDeclaration && initialization.next === null ) &&
+			( condition && condition.left.isAccessor && condition.left.property === initialization.name ) &&
+			( afterthought && afterthought.isUnary ) &&
+			( initialization.name === afterthought.expression.property )
+		) {
+
+			return this.emitLoop( node );
+
+		}
+
+		return this.emitForWhile( node );
+
+	}
+
+	emitForWhile( node ) {
+
+		const initialization = this.emitExpression( node.initialization );
+		const condition = this.emitExpression( node.condition );
+		const afterthought = this.emitExpression( node.afterthought );
+
+		this.tab += '\t';
+
+		let forStr = '{\n\n' + this.tab + initialization + ';\n\n';
+		forStr += `${ this.tab }While( ${ condition }, () => {\n\n`;
+
+		forStr += this.emitBody( node.body ) + '\n\n';
+
+		forStr += this.tab + '\t' + afterthought + ';\n\n';
+
+		forStr += this.tab + '} )\n\n';
+
+		this.tab = this.tab.slice( 0, - 1 );
+
+		forStr += this.tab + '}';
+
+		this.imports.add( 'While' );
+
+		return forStr;
+
+	}
+
+	emitVariables( node, isRoot = true ) {
+
+		const { name, type, value, next } = node;
+
+		const valueStr = value ? this.emitExpression( value ) : '';
+
+		let varStr = isRoot ? 'const ' : '';
+		varStr += name;
+
+		if ( value ) {
+
+			if ( value.isFunctionCall && value.name === type ) {
+
+				varStr += ' = ' + valueStr;
+
+			} else {
+
+				varStr += ` = ${ type }( ${ valueStr } )`;
+
+			}
+
+		} else {
+
+			varStr += ` = ${ type }()`;
+
+		}
+
+		if ( node.immutable === false ) {
+
+			varStr += '.toVar()';
+
+		}
+
+		if ( next ) {
+
+			varStr += ', ' + this.emitVariables( next, false );
+
+		}
+
+		this.addImport( type );
+
+		return varStr;
+
+	}
+
+	emitFunction( node ) {
+
+		const { name, type } = node;
+
+		this._currentProperties = { name: node };
+
+		const params = [];
+		const inputs = [];
+		const mutableParams = [];
+
+		let hasPointer = false;
+
+		for ( const param of node.params ) {
+
+			let str = `{ name: '${ param.name }', type: '${ param.type }'`;
+
+			let name = param.name;
+
+			if ( param.immutable === false && ( param.qualifier !== 'inout' && param.qualifier !== 'out' ) ) {
+
+				name = name + '_immutable';
+
+				mutableParams.push( param );
+
+			}
+
+			if ( param.qualifier ) {
+
+				if ( param.qualifier === 'inout' || param.qualifier === 'out' ) {
+
+					hasPointer = true;
+
+				}
+
+				str += ', qualifier: \'' + param.qualifier + '\'';
+
+			}
+
+			inputs.push( str + ' }' );
+			params.push( name );
+
+			this._currentProperties[ name ] = param;
+
+		}
+
+		for ( const param of mutableParams ) {
+
+			node.body.unshift( new VariableDeclaration( param.type, param.name, new Accessor( param.name + '_immutable' ) ) );
+
+		}
+
+		const paramsStr = params.length > 0 ? ' [ ' + params.join( ', ' ) + ' ] ' : '';
+		const bodyStr = this.emitBody( node.body );
+
+		const funcStr = `const ${ name } = tslFn( (${ paramsStr }) => {
+
+${ bodyStr }
+
+${ this.tab }} );\n`;
+
+		const layoutInput = inputs.length > 0 ? '\n\t\t' + this.tab + inputs.join( ',\n\t\t' + this.tab ) + '\n\t' + this.tab : '';
+
+		if ( node.layout !== false && hasPointer === false ) {
+
+			const uniqueName = this.uniqueNames ? name + '_' + Math.random().toString( 36 ).slice( 2 ) : name;
+
+			this.layoutsCode += `${ this.tab + name }.setLayout( {
+${ this.tab }\tname: '${ uniqueName }',
+${ this.tab }\ttype: '${ type }',
+${ this.tab }\tinputs: [${ layoutInput }]
+${ this.tab }} );\n\n`;
+
+		}
+
+		this.imports.add( 'tslFn' );
+
+		this.functions.add( node.name );
+
+		return funcStr;
+
+	}
+
+	setLastStatement( statement ) {
+
+		this._lastStatment = statement;
+
+	}
+
+	emitExtraLine( statement ) {
+
+		const last = this._lastStatment;
+		if ( last === null ) return '';
+
+		if ( statement.isReturn ) return '\n';
+
+		const isExpression = ( st ) => st.isFunctionDeclaration !== true && st.isFor !== true && st.isConditional !== true;
+		const lastExp = isExpression( last );
+		const currExp = isExpression( statement );
+
+		if ( lastExp !== currExp || ( ! lastExp && ! currExp ) ) return '\n';
+
+		return '';
+
+	}
+
+	emit( ast ) {
+
+		let code = '\n';
+
+		if ( this.iife ) this.tab += '\t';
+
+		for ( const statement of ast.body ) {
+
+			code += this.emitExtraLine( statement );
+
+			if ( statement.isFunctionDeclaration ) {
+
+				code += this.tab + this.emitFunction( statement );
+
+			} else {
+
+				code += this.tab + this.emitExpression( statement ) + ';\n';
+
+			}
+
+			this.setLastStatement( statement );
+
+		}
+
+		const imports = [ ...this.imports ];
+		const functions = [ ...this.functions ];
+
+		const layouts = this.layoutsCode.length > 0 ? `\n${ this.tab }// layouts\n\n` + this.layoutsCode : '';
+
+		let header = '// Three.js Transpiler r' + REVISION + '\n\n';
+		let footer = '';
+
+		if ( this.iife ) {
+
+			header += '( function ( TSL ) {\n\n';
+
+			header += imports.length > 0 ? '\tconst { ' + imports.join( ', ' ) + ' } = TSL;\n' : '';
+			footer += functions.length > 0 ? '\treturn { ' + functions.join( ', ' ) + ' };\n' : '';
+
+			footer += '\n} );';
+
+		} else {
+
+			header += imports.length > 0 ? 'import { ' + imports.join( ', ' ) + ' } from \'three/nodes\';\n' : '';
+			footer += functions.length > 0 ? 'export { ' + functions.join( ', ' ) + ' };\n' : '';
+
+		}
+
+		return header + code + layouts + footer;
+
+	}
+
+}
+
+export default TSLEncoder;

+ 18 - 0
examples/jsm/transpiler/Transpiler.js

@@ -0,0 +1,18 @@
+class Transpiler {
+
+	constructor( decoder, encoder ) {
+
+		this.decoder = decoder;
+		this.encoder = encoder;
+
+	}
+
+	parse( source ) {
+
+		return this.encoder.emit( this.decoder.parse( source ) );
+
+	}
+
+}
+
+export default Transpiler;

TEMPAT SAMPAH
examples/screenshots/webgpu_shadertoy.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_tsl_transpiler.jpg


+ 2 - 2
examples/webgl_nodes_points.html

@@ -25,7 +25,7 @@
 		<script type="module">
 
 			import * as THREE from 'three';
-			import { attribute, timerLocal, positionLocal, spritesheetUV, pointUV, vec2, texture, uniform, mix, PointsNodeMaterial } from 'three/nodes';
+			import { attribute, timerLocal, positionLocal, spritesheetUV, pointUV, vec2, vec3, texture, uniform, mix, PointsNodeMaterial } from 'three/nodes';
 
 			import Stats from 'three/addons/libs/stats.module.js';
 
@@ -122,7 +122,7 @@
 					blending: THREE.AdditiveBlending
 				} );
 
-				material.colorNode = fire;
+				material.colorNode = vec3( fire );
 				material.sizeNode = particleSize;
 				material.positionNode = positionNode;
 

+ 309 - 0
examples/webgpu_shadertoy.html

@@ -0,0 +1,309 @@
+<html lang="en">
+	<head>
+		<title>three.js - shadertoy</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - ShaderToy
+			<br />Shader created by <a href="https://www.shadertoy.com/view/Mt2SzR" target="_blank" rel="noopener">jackdavenport</a> and <a href="https://www.shadertoy.com/view/3tcBzH" target="_blank" rel="noopener">trinketMage</a>.
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="x-shader/x-fragment" id="example1">
+			// https://www.shadertoy.com/view/Mt2SzR
+
+			float random(float x) {
+ 
+				return fract(sin(x) * 10000.);
+					  
+			}
+			
+			float noise(vec2 p) {
+			
+				return random(p.x + p.y * 10000.);
+						
+			}
+			
+			vec2 sw(vec2 p) { return vec2(floor(p.x), floor(p.y)); }
+			vec2 se(vec2 p) { return vec2(ceil(p.x), floor(p.y)); }
+			vec2 nw(vec2 p) { return vec2(floor(p.x), ceil(p.y)); }
+			vec2 ne(vec2 p) { return vec2(ceil(p.x), ceil(p.y)); }
+			
+			float smoothNoise(vec2 p) {
+			
+				vec2 interp = smoothstep(0., 1., fract(p));
+				float s = mix(noise(sw(p)), noise(se(p)), interp.x);
+				float n = mix(noise(nw(p)), noise(ne(p)), interp.x);
+				return mix(s, n, interp.y);
+					
+			}
+			
+			float fractalNoise(vec2 p) {
+			
+				float x = 0.;
+				x += smoothNoise(p      );
+				x += smoothNoise(p * 2. ) / 2.;
+				x += smoothNoise(p * 4. ) / 4.;
+				x += smoothNoise(p * 8. ) / 8.;
+				x += smoothNoise(p * 16.) / 16.;
+				x /= 1. + 1./2. + 1./4. + 1./8. + 1./16.;
+				return x;
+						
+			}
+			
+			float movingNoise(vec2 p) {
+			 
+				float x = fractalNoise(p + iTime);
+				float y = fractalNoise(p - iTime);
+				return fractalNoise(p + vec2(x, y));   
+				
+			}
+			
+			// call this for water noise function
+			float nestedNoise(vec2 p) {
+				
+				float x = movingNoise(p);
+				float y = movingNoise(p + 100.);
+				return movingNoise(p + vec2(x, y));
+				
+			}
+
+			void mainImage( out vec4 fragColor, in vec2 fragCoord )
+			{
+				vec2 uv = fragCoord.xy / iResolution.xy;
+				float n = nestedNoise(uv * 6.);
+
+				fragColor = vec4(mix(vec3(.4, .6, 1.), vec3(.1, .2, 1.), n), 1.);
+			}
+
+		</script>
+
+		<script type="x-shader/x-fragment" id="example2">
+			// https://www.shadertoy.com/view/3tcBzH
+
+			float rand(vec2 co){
+				return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
+			}
+			
+			float hermite(float t)
+			{
+			  return t * t * (3.0 - 2.0 * t);
+			}
+			
+			float noise(vec2 co, float frequency)
+			{
+			  vec2 v = vec2(co.x * frequency, co.y * frequency);
+			
+			  float ix1 = floor(v.x);
+			  float iy1 = floor(v.y);
+			  float ix2 = floor(v.x + 1.0);
+			  float iy2 = floor(v.y + 1.0);
+			
+			  float fx = hermite(fract(v.x));
+			  float fy = hermite(fract(v.y));
+			
+			  float fade1 = mix(rand(vec2(ix1, iy1)), rand(vec2(ix2, iy1)), fx);
+			  float fade2 = mix(rand(vec2(ix1, iy2)), rand(vec2(ix2, iy2)), fx);
+			
+			  return mix(fade1, fade2, fy);
+			}
+			
+			float pnoise(vec2 co, float freq, int steps, float persistence)
+			{
+			  float value = 0.0;
+			  float ampl = 1.0;
+			  float sum = 0.0;
+			  for(int i=0 ; i<steps ; i++)
+			  {
+				sum += ampl;
+				value += noise(co, freq) * ampl;
+				freq *= 2.0;
+				ampl *= persistence;
+			  }
+			  return value / sum;
+			}
+			
+			void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
+				vec2 uv = fragCoord.xy / iResolution.xy;
+				float gradient = 1.0 - uv.y;
+				float gradientStep = 0.2;
+				
+				vec2 pos = fragCoord.xy / iResolution.x;
+				pos.y -= iTime * 0.3125;
+				
+				vec4 brighterColor = vec4(1.0, 0.65, 0.1, 0.25);
+				vec4 darkerColor = vec4(1.0, 0.0, 0.15, 0.0625);
+				vec4 middleColor = mix(brighterColor, darkerColor, 0.5);
+			
+				float noiseTexel = pnoise(pos, 10.0, 5, 0.5);
+				
+				float firstStep = smoothstep(0.0, noiseTexel, gradient);
+				float darkerColorStep = smoothstep(0.0, noiseTexel, gradient - gradientStep);
+				float darkerColorPath = firstStep - darkerColorStep;
+				vec4 color = mix(brighterColor, darkerColor, darkerColorPath);
+			
+				float middleColorStep = smoothstep(0.0, noiseTexel, gradient - 0.2 * 2.0);
+				
+				color = mix(color, middleColor, darkerColorStep - middleColorStep);
+				color = mix(vec4(0.0), color, firstStep);
+				fragColor = color;
+			}
+
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import * as Nodes from 'three/nodes';
+
+			import Transpiler from './jsm/transpiler/Transpiler.js';
+			import ShaderToyDecoder from './jsm/transpiler/ShaderToyDecoder.js';
+			import TSLEncoder from './jsm/transpiler/TSLEncoder.js';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGL from 'three/addons/capabilities/WebGL.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			class ShaderToyNode extends Nodes.Node {
+
+				constructor() {
+
+					super( 'vec4' );
+
+					this.mainImage = null;
+
+				}
+
+				transpile( glsl, iife = false ) {
+
+					const decoder = new ShaderToyDecoder();
+
+					const encoder = new TSLEncoder();
+					encoder.iife = iife;
+					encoder.uniqueNames = true;
+
+					const jsCode = new Transpiler( decoder, encoder ).parse( glsl );
+
+					return jsCode;
+
+				}
+
+				parse( glsl ) {
+
+					const jsCode = this.transpile( glsl, true );
+
+					const { mainImage } = eval( jsCode )( Nodes );
+
+					this.mainImage = mainImage;
+
+				}
+
+				async parseAsync( glsl ) {
+
+					const jsCode = this.transpile( glsl );
+
+					const { mainImage } = await import( `data:text/javascript,${ encodeURIComponent( jsCode ) }` );
+
+					this.mainImage = mainImage;
+
+				}
+
+				setup( builder ) {
+
+					if ( this.mainImage === null ) {
+
+						throw new Error( 'ShaderToyNode: .parse() must be called first.' );
+
+					}
+
+					return this.mainImage();
+
+				}
+
+
+			}
+
+			let renderer, camera, scene;
+			const dpr = window.devicePixelRatio;
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false && WebGL.isWebGL2Available() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU or WebGL2 support' );
+
+				}
+
+				//
+
+				const example1Code = document.getElementById( 'example1' ).textContent;
+				const example2Code = document.getElementById( 'example2' ).textContent;
+
+				const shaderToy1Node = new ShaderToyNode();
+				shaderToy1Node.parse( example1Code );
+
+				const shaderToy2Node = new ShaderToyNode();
+				shaderToy2Node.parse( example2Code );
+
+				//
+
+				camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+				scene = new THREE.Scene();
+
+				const geometry = new THREE.PlaneGeometry( 2, 2 );
+
+				const material = new Nodes.MeshBasicNodeMaterial();
+				material.colorNode = Nodes.oscSine( Nodes.timerLocal( .3 ) ).mix( shaderToy1Node, shaderToy2Node );
+
+				const quad = new THREE.Mesh( geometry, material );
+				scene.add( quad );
+
+				//
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( dpr );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
+				document.body.appendChild( renderer.domElement );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 136 - 0
examples/webgpu_tsl_transpiler.html

@@ -0,0 +1,136 @@
+<html lang="en">
+	<head>
+		<title>three.js - webgpu - tsl transpiler</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<style>
+			#source {
+				position: absolute;
+				top: 0;
+				left: 0;
+				width: 50%;
+				height: 100%;
+			}
+			#result {
+				position: absolute;
+				top: 0;
+				right: 0;
+				width: 50%;
+				height: 100%;
+			}
+		</style>
+
+		<div id="source"></div>
+		<div id="result"></div>
+		<script src="https://cdn.jsdelivr.net/npm/monaco-editor@latest/min/vs/loader.min.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import Transpiler from './jsm/transpiler/Transpiler.js';
+			import GLSLDecoder from './jsm/transpiler/GLSLDecoder.js';
+			import TSLEncoder from './jsm/transpiler/TSLEncoder.js';
+
+			init();
+
+			function init() {
+
+				// editor
+
+				window.require.config( { paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@latest/min/vs' } } );
+
+				require( [ 'vs/editor/editor.main' ], () => {
+
+					let timeout = null;
+
+					const editorDOM = document.getElementById( 'source' );
+					const resultDOM = document.getElementById( 'result' );
+
+					const glslCode = `// Put here your GLSL code to transpile to TSL:
+
+float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {
+
+	float a2 = pow2( alpha );
+
+	float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );
+	float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );
+
+	return 0.5 / max( gv + gl, EPSILON );
+
+}
+`;
+
+					const editor = window.monaco.editor.create( editorDOM, {
+						value: glslCode,
+						language: 'glsl',
+						theme: 'vs-dark',
+						automaticLayout: true
+					} );
+
+					const result = window.monaco.editor.create( resultDOM, {
+						value: '',
+						language: 'javascript',
+						theme: 'vs-dark',
+						automaticLayout: true,
+						readOnly: true
+					} );
+
+					const showCode = ( code ) => {
+
+						result.setValue( code );
+						result.revealLine( 1 );
+
+					};
+
+					const build = () => {
+
+						try {
+
+							const glsl = editor.getValue();
+
+							const decoder = new GLSLDecoder();
+							const encoder = new TSLEncoder();
+
+							const transpiler = new Transpiler( decoder, encoder );
+							const tsl = transpiler.parse( glsl );
+
+							showCode( tsl );
+
+						} catch ( e ) {
+
+							result.setValue( 'Error: ' + e.message );
+
+						}
+
+					};
+
+					build();
+
+					editor.getModel().onDidChangeContent( () => {
+
+						if ( timeout ) clearTimeout( timeout );
+
+						timeout = setTimeout( build, 1000 );
+
+					} );
+
+				} );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 2 - 0
test/e2e/puppeteer.js

@@ -124,9 +124,11 @@ const exceptionList = [
 	'webgpu_materials_video',
 	'webgpu_particles',
 	'webgpu_sandbox',
+	'webgpu_shadertoy',
 	'webgpu_shadowmap',
 	'webgpu_sprites',
 	'webgpu_tsl_editor',
+	'webgpu_tsl_transpiler',
 	'webgpu_video_panorama'
 
 ];