diff --git a/src/webgl/ShaderGenerator.js b/src/webgl/ShaderGenerator.js index 2e716ce39f..b392a9f884 100644 --- a/src/webgl/ShaderGenerator.js +++ b/src/webgl/ShaderGenerator.js @@ -7,61 +7,46 @@ import { parse } from 'acorn'; import { ancestor } from 'acorn-walk'; import escodegen from 'escodegen'; -import noiseGLSL from './shaders/functions/noiseGLSL.glsl'; function shadergenerator(p5, fn) { let GLOBAL_SHADER; let BRANCH; - const oldModify = p5.Shader.prototype.modify; + const oldModify = p5.Shader.prototype.modify - p5.Shader.prototype.modify = function(shaderModifier, scope = {}) { + p5.Shader.prototype.modify = function(shaderModifier, options = { parser: true, srcLocations: false }) { if (shaderModifier instanceof Function) { - // TODO make this public. Currently for debugging only. - const options = { parser: true, srcLocations: false }; let generatorFunction; if (options.parser) { - // #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing - // https://github.com/acornjs/acorn/issues/1385 - const sourceString = `(${shaderModifier.toString()})`; - const ast = parse(sourceString, { + const sourceString = shaderModifier.toString() + // Wrap in parentheses so anonymous functions parse as FunctionExpression + const ast = parse(`(${sourceString})`, { ecmaVersion: 2021, locations: options.srcLocations }); - ancestor(ast, ASTCallbacks, undefined, { varyings: {} }); - const transpiledSource = escodegen.generate(ast); - const scopeKeys = Object.keys(scope); - const internalGeneratorFunction = new Function( - // Create a parameter called __p5, not just p5, because users of instance mode - // may pass in a variable called p5 as a scope variable. If we rely on a variable called - // p5, then the scope variable called p5 might accidentally override internal function - // calls to p5 static methods. - '__p5', - ...scopeKeys, + // For wrapped parsing, the function body is at ast.body[0].expression + const astRoot = ast.body?.[0]?.expression || ast; + ancestor(astRoot, ASTCallbacks, undefined, { varyings: {} }); + const transpiledSource = escodegen.generate(astRoot); + generatorFunction = new Function( transpiledSource - .slice( - transpiledSource.indexOf('{') + 1, - transpiledSource.lastIndexOf('}') - ).replaceAll(';', '') + .slice( + transpiledSource.indexOf('{') + 1, + transpiledSource.lastIndexOf('}') + ).replaceAll(';', '') ); - generatorFunction = () => - internalGeneratorFunction(p5, ...scopeKeys.map(key => scope[key])); } else { generatorFunction = shaderModifier; } - const generator = new ShaderGenerator( - generatorFunction, - this, - options.srcLocations - ); + const generator = new ShaderGenerator(generatorFunction, this, options.srcLocations); const generatedModifyArgument = generator.generate(); return oldModify.call(this, generatedModifyArgument); } else { - return oldModify.call(this, shaderModifier); + return oldModify.call(this, shaderModifier) } - }; + } // AST Transpiler Callbacks and helper functions function replaceBinaryOperator(codeSource) { @@ -81,38 +66,29 @@ function shadergenerator(p5, fn) { } } - function nodeIsUniform(ancestor) { + function ancestorIsUniform(ancestor) { return ancestor.type === 'CallExpression' - && ( - ( - // Global mode - ancestor.callee?.type === 'Identifier' && - ancestor.callee?.name.startsWith('uniform') - ) || ( - // Instance mode - ancestor.callee?.type === 'MemberExpression' && - ancestor.callee?.property.name.startsWith('uniform') - ) - ); + && ancestor.callee?.type === 'Identifier' + && ancestor.callee?.name.startsWith('uniform'); } const ASTCallbacks = { - UnaryExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + UnaryExpression(node, _state, _ancestors) { + if (_ancestors.some(ancestorIsUniform)) { return; } const signNode = { type: 'Literal', - value: node.operator - }; + value: node.operator, + } - const standardReplacement = node => { - node.type = 'CallExpression'; - node.callee = { - type: 'Identifier', - name: '__p5.unaryNode' - }; - node.arguments = [node.argument, signNode]; - }; + const standardReplacement = (node) => { + node.type = 'CallExpression' + node.callee = { + type: 'Identifier', + name: 'unaryNode', + } + node.arguments = [node.argument, signNode] + } if (node.type === 'MemberExpression') { const property = node.argument.property.name; @@ -132,9 +108,9 @@ function shadergenerator(p5, fn) { type: 'CallExpression', callee: { type: 'Identifier', - name: '__p5.unaryNode' + name: 'unaryNode' }, - arguments: [node.argument.object, signNode] + arguments: [node.argument.object, signNode], }; node.property = { type: 'Identifier', @@ -149,26 +125,24 @@ function shadergenerator(p5, fn) { delete node.argument; delete node.operator; }, - VariableDeclarator(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - if (nodeIsUniform(node.init)) { + VariableDeclarator(node, _state, _ancestors) { + if (node.init.callee && node.init.callee.name?.startsWith('uniform')) { const uniformNameLiteral = { type: 'Literal', value: node.id.name - }; + } node.init.arguments.unshift(uniformNameLiteral); } if (node.init.callee && node.init.callee.name?.startsWith('varying')) { const varyingNameLiteral = { type: 'Literal', value: node.id.name - }; + } node.init.arguments.unshift(varyingNameLiteral); _state.varyings[node.id.name] = varyingNameLiteral; } }, - Identifier(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + Identifier(node, _state, _ancestors) { if (_state.varyings[node.name] && !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) { node.type = 'ExpressionStatement'; @@ -183,26 +157,24 @@ function shadergenerator(p5, fn) { property: { type: 'Identifier', name: 'getValue' - } + }, }, - arguments: [] - }; + arguments: [], + } } }, // The callbacks for AssignmentExpression and BinaryExpression handle // operator overloading including +=, *= assignment expressions - ArrayExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + ArrayExpression(node, _state, _ancestors) { const original = JSON.parse(JSON.stringify(node)); node.type = 'CallExpression'; node.callee = { type: 'Identifier', - name: '__p5.dynamicNode' + name: 'dynamicNode', }; node.arguments = [original]; }, - AssignmentExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + AssignmentExpression(node, _state, _ancestors) { if (node.operator !== '=') { const methodName = replaceBinaryOperator(node.operator.replace('=','')); const rightReplacementNode = { @@ -212,37 +184,37 @@ function shadergenerator(p5, fn) { object: node.left, property: { type: 'Identifier', - name: methodName - } - }, - arguments: [node.right] - }; - node.operator = '='; - node.right = rightReplacementNode; - } - if (_state.varyings[node.left.name]) { - node.type = 'ExpressionStatement'; - node.expression = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: node.left.name + name: methodName, }, - property: { - type: 'Identifier', - name: 'bridge' - } }, arguments: [node.right] - }; - } - }, - BinaryExpression(node, _state, ancestors) { + } + node.operator = '='; + node.right = rightReplacementNode; + } + if (_state.varyings[node.left.name]) { + node.type = 'ExpressionStatement'; + node.expression = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: node.left.name + }, + property: { + type: 'Identifier', + name: 'bridge', + } + }, + arguments: [node.right], + } + } + }, + BinaryExpression(node, _state, _ancestors) { // Don't convert uniform default values to node methods, as // they should be evaluated at runtime, not compiled. - if (ancestors.some(nodeIsUniform)) { return; } + if (_ancestors.some(ancestorIsUniform)) { return; } // If the left hand side of an expression is one of these types, // we should construct a node from it. const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; @@ -251,10 +223,10 @@ function shadergenerator(p5, fn) { type: 'CallExpression', callee: { type: 'Identifier', - name: '__p5.dynamicNode' + name: 'dynamicNode', }, arguments: [node.left] - }; + } node.left = leftReplacementNode; } // Replace the binary operator with a call expression @@ -265,12 +237,12 @@ function shadergenerator(p5, fn) { object: node.left, property: { type: 'Identifier', - name: replaceBinaryOperator(node.operator) - } + name: replaceBinaryOperator(node.operator), + }, }; node.arguments = [node.right]; - } - }; + }, + } // Javascript Node API. class BaseNode { @@ -319,16 +291,14 @@ function shadergenerator(p5, fn) { value = newValue; } } - }); + }) } } } forceTemporaryVariable() { - if ( - !(isFloatNode(this) && isVectorNode(this.parent)) || - !isVariableNode(this) - ) this.useTemp = true; + if (!(isFloatNode(this) && isVectorNode(this.parent)) || !isVariableNode(this)) + this.useTemp = true; } assertUsedInConditional(branch) { @@ -342,21 +312,14 @@ function shadergenerator(p5, fn) { } checkConditionalDependencies(context) { - context.ifs.forEach(statement => { + context.ifs.forEach((statement) => { const isUsedSatisfied = () => statement.usedInSatisfied.length >= 1; - const isDepsSatisfied = () => - statement.dependsOn.length === statement.dependsOnSatisfied.length; + const isDepsSatisfied = () => statement.dependsOn.length === statement.dependsOnSatisfied.length; if (statement.insertionPoint > -1 || !statement.usedIn.length) return; - if ( - statement.dependsOn.some(d => d.node === this) && - !statement.dependsOnSatisfied.includes(this) - ) { + if (statement.dependsOn.some(d => d.node === this) && !statement.dependsOnSatisfied.includes(this)) { statement.dependsOnSatisfied.push(this); } - if ( - statement.usedIn.includes(this) && - !statement.usedInSatisfied.includes(this) - ) { + if (statement.usedIn.includes(this) && !statement.usedInSatisfied.includes(this)) { statement.usedInSatisfied.push(this); } if (isDepsSatisfied() && isUsedSatisfied()) { @@ -375,10 +338,9 @@ function shadergenerator(p5, fn) { diff = diff > 0 ? diff : undefined; this.dependsOn.forEach(dependency => { if (dependency.isVector) { - const dependencies = dependency.originalComponents - .map((component, i) => - component === dependency.currentComponents[i] - ); + const dependencies = dependency.originalComponents.map((component, i) => + component === dependency.currentComponents[i] + ); context.updateComponents(dependency.node, diff, dependencies); } else { context.updateComponents(dependency.node, diff); @@ -387,16 +349,12 @@ function shadergenerator(p5, fn) { } else { result = this.toGLSL(context); } - this.checkConditionalDependencies(context); + this.checkConditionalDependencies(context) return result; } shouldUseTemporaryVariable() { - if ( - this.componentsChanged || - hasTemporaryVariable(this) || - this.useTemp - ) return true; + if (this.componentsChanged || hasTemporaryVariable(this) || this.useTemp) { return true; } if (this.isInternal || isVariableNode(this) || isConditionalNode(this) || this.type === 'sampler2D') { return false; } // return false; @@ -433,13 +391,13 @@ function shadergenerator(p5, fn) { enforceType(other){ if (isShaderNode(other)){ if (!isGLSLNativeType(other.type)) { - throw new TypeError (`You've tried to perform an operation on a struct of type: ${other.type}. Try accessing a member on that struct with '.'`); + throw new TypeError (`You've tried to perform an operation on a struct of type: ${other.type}. Try accessing a member on that struct with '.'`) } if (!isGLSLNativeType(other.type)) { - throw new TypeError (`You've tried to perform an operation on a struct of type: ${other.type}. Try accessing a member on that struct with '.'`); + throw new TypeError (`You've tried to perform an operation on a struct of type: ${other.type}. Try accessing a member on that struct with '.'`) } if ((isFloatType(this) || isVectorType(this)) && isIntType(other)) { - return new FloatNode(other); + return new FloatNode(other) } return other; } @@ -499,7 +457,7 @@ function shadergenerator(p5, fn) { x = x[0]; } if (_parent) { - const { parent, name } = _parent; + const { parent, name } = _parent this.name = name; this.parent = parent; } @@ -523,9 +481,7 @@ function shadergenerator(p5, fn) { class VectorNode extends BaseNode { constructor(values, type, isInternal = false) { super(isInternal, type); - this.originalValues = conformVectorParameters( - values, parseInt(type.slice(3)) - ); + this.originalValues = conformVectorParameters(values, parseInt(type.slice(3))); this.componentNames = ['x', 'y', 'z', 'w'].slice(0, this.originalValues.length); } @@ -535,9 +491,7 @@ function shadergenerator(p5, fn) { this.componentNames.forEach((componentName, i) => { const info = { name: componentName, parent: this }; - let value = isFloatNode(values[i]) ? - values[i] : - new FloatNode(values[i], true, info); + let value = isFloatNode(values[i]) ? values[i] : new FloatNode(values[i], true, info); Object.defineProperty(this, componentName, { get() { return value; @@ -547,12 +501,10 @@ function shadergenerator(p5, fn) { if (isUnaryExpressionNode(this)) { this.node.value = newValue; } else { - value = isFloatNode(newValue) ? - newValue : - new FloatNode(newValue, true, info); + value = isFloatNode(newValue) ? newValue : new FloatNode(newValue, true, info); } } - }); + }) }); this.originalValues = this.componentNames.map(name => this[name]); } @@ -572,7 +524,7 @@ function shadergenerator(p5, fn) { class FunctionCallNode extends BaseNode { constructor(name, userArgs, properties, isInternal = false) { let functionSignature; - const determineFunctionSignature = props => { + const determineFunctionSignature = (props) => { let genType; let similarity = 0; @@ -583,26 +535,25 @@ function shadergenerator(p5, fn) { if (expectedArgType === 'genType') { // We allow conversions from float -> vec if one argument is a vector. if (genType === undefined || (genType === 'float' && userType.startsWith('vec'))) { - genType = userType; + genType = userType }; expectedArgType = genType; } similarity += (userType === expectedArgType); return userType === expectedArgType || (userType === 'float' && expectedArgType.startsWith('vec')); - }); + }) - return { ...props, valid, similarity, genType }; - }; + return { ...props, valid, similarity, genType } + } if (Array.isArray(properties)) { // Check if the right number of parameters were provided - let possibleOverloads = properties - .filter(o => o.args.length === userArgs.length); + let possibleOverloads = properties.filter(o => o.args.length === userArgs.length); if (possibleOverloads.length === 0) { const argsLengthSet = new Set(); const argsLengthArr = []; - properties.forEach(p => argsLengthSet.add(p.args.length)); - argsLengthSet.forEach(len => argsLengthArr.push(`${len}`)); + properties.forEach((p) => argsLengthSet.add(p.args.length)); + argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); const argsLengthStr = argsLengthArr.join(' or '); throw new Error(`Function '${name}' has ${properties.length} variants which expect ${argsLengthStr} arguments, but ${userArgs.length} arguments were provided.`); } @@ -613,19 +564,19 @@ function shadergenerator(p5, fn) { best = current; } return best; - }; + } functionSignature = possibleOverloads.reduce(findBestOverload, null); } else { functionSignature = determineFunctionSignature(properties); } if (!functionSignature || !functionSignature.valid) { - const argsStrJoin = args => `(${args.map(arg => arg).join(', ')})`; + const argsStrJoin = (args) => `(${args.map((arg) => arg).join(', ')})`; const expectedArgsString = Array.isArray(properties) ? properties.map(prop => argsStrJoin(prop.args)).join(' or ') : argsStrJoin(properties.args); - const providedArgsString = argsStrJoin(userArgs.map(a=>getType(a))); - throw new Error(`Function '${name}' was called with wrong arguments. Most likely, you provided mixed lengths vectors as arguments.\nExpected argument types: ${expectedArgsString}\nProvided argument types: ${providedArgsString}\nAll of the arguments with expected type 'genType' should have a matching type. If one of those is different, try to find where it was created. + const providedArgsString = argsStrJoin(userArgs.map((a)=>getType(a))); + throw new Error(`Function '${name}' was called with wrong arguments. Most likely, you provided mixed lengths vectors as arguments.\nExpected argument types: ${expectedArgsString}\nProvided argument types: ${providedArgsString}\nAll of the arguments with expected type 'genType' should have a matching type. If one of those is different, try to find where it was created. `); } @@ -641,7 +592,7 @@ function shadergenerator(p5, fn) { arg = nodeConstructors[functionSignature.genType](arg); } return arg; - }); + }) if (functionSignature.returnType === 'genType') { functionSignature.returnType = functionSignature.genType; @@ -656,7 +607,7 @@ function shadergenerator(p5, fn) { deconstructArgs(context) { let argsString = this.args.map((argNode, i) => { - if (isIntType(argNode) && this.argumentTypes[i] !== 'float') { + if (isIntType(argNode) && this.argumentTypes[i] != 'float') { argNode = argNode.toFloat(); } argNode.toGLSLBase(context); @@ -718,10 +669,8 @@ function shadergenerator(p5, fn) { let { value } = val; context.declarations.push(` ${this.name} = ${value.toGLSLBase(context)};`); if (i === values.length - 1) { - const tempName = `${this.name}_${this.tempVars++}`; - snapshot = dynamicAddSwizzleTrap( - new VariableNode(tempName, this.type, true) - ); + const tempName = `${this.name}_${this.tempVars++}` + snapshot = dynamicAddSwizzleTrap(new VariableNode(tempName, this.type, true)); context.declarations.push(` ${this.type} ${tempName} = ${this.name};`); } }); @@ -732,10 +681,10 @@ function shadergenerator(p5, fn) { bridge(value) { if (!isShaderNode(value) || this.type.startsWith('vec') && getType(value) === 'float') { - value = nodeConstructors[this.type](value); + value = nodeConstructors[this.type](value) } GLOBAL_SHADER.registerVarying(this, value); - this.timesChanged += 1; + this.timesChanged += 1 } } @@ -810,55 +759,51 @@ function shadergenerator(p5, fn) { class UnaryExpressionNode extends BaseNode { constructor(node, operator, isInternal = false) { - super(isInternal, node.type); + super(isInternal, node.type) this.node = node; this.operator = operator; } toGLSL(context) { let mainStr = this.node.toGLSLBase(context); - if ( - !isVariableNode(this.node) && - !hasTemporaryVariable(this.node) && - !isPrimitiveNode(this.node) - ) { - mainStr = `(${mainStr})`; + if (!isVariableNode(this.node) && !hasTemporaryVariable(this.node) && !isPrimitiveNode(this.node)) { + mainStr = `(${mainStr})` } - return `${this.operator}${mainStr}`; + return `${this.operator}${mainStr}` } } // Conditions and logical modifiers BaseNode.prototype.equalTo = function(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '=='); - }; + } BaseNode.prototype.greaterThan = function(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '>'); - }; + } BaseNode.prototype.greaterThanEqualTo = function(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '>='); - }; + } BaseNode.prototype.lessThan = function(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '<'); - }; + } BaseNode.prototype.lessThanEqualTo = function(other) { - return binaryExpressionNodeConstructor(this, this.enforceType(other), '<='); }; + return binaryExpressionNodeConstructor(this, this.enforceType(other), '<='); } BaseNode.prototype.not = function() { - return new UnaryExpressionNode(this.condition, '!', true); - }; + return new UnaryExpressionNode(this.condition, '!', true); + } BaseNode.prototype.or = function(other) { return new binaryExpressionNodeConstructor(this, this.enforceType(other), '||', true); - }; + } BaseNode.prototype.and = function(other) { return new binaryExpressionNodeConstructor(this, this.enforceType(other), '&&', true); - }; + } function branch(callback) { const branch = new BranchNode(); @@ -897,7 +842,7 @@ function shadergenerator(p5, fn) { else(branchCallback) { this.elseBranch = branch(branchCallback); - this.elseBranch.parent = this; + this.elseBranch.parent = this return this; } @@ -924,22 +869,22 @@ function shadergenerator(p5, fn) { let codelines = [ `\n if (${this.conditionString}) {`, `\n ${this.ifBranch.toGLSL(context)}`, - '\n }' + `\n }` ]; if (this.elseIfs.length) { - this.elseIfs.forEach(elif => { + this.elseIfs.forEach((elif) => { let { condition, elseBranch } = elif; codelines.push(` else if (${condition.toGLSL(context)}) {`); codelines.push(`\n ${elseBranch.toGLSL(context)}`); - codelines.push('\n }'); - }); + codelines.push(`\n }`); + }) } if (this.elseBranch) { - codelines.push(' else {'); + codelines.push(` else {`); codelines.push(`\n ${this.elseBranch.toGLSL(context)}`); - codelines.push('\n }\n'); + codelines.push(`\n }\n`); } codelines.push('\n'); return codelines.flat().join(''); @@ -948,10 +893,10 @@ function shadergenerator(p5, fn) { fn.assign = function(node, value) { if (!BRANCH) { - throw new error('assign() is supposed to be used inside of conditional branchs. Use the "=" operator as normal otherwise.'); + throw new error('assign() is supposed to be used inside of conditional branchs. Use the "=" operator as normal otherwise.'); } BRANCH.assign(node, value); - }; + } class BranchNode { constructor() { @@ -966,10 +911,10 @@ function shadergenerator(p5, fn) { return parent; }, set(newParent) { - newParent.dependsOn.push(...this.dependsOn); + newParent.dependsOn.push(...this.dependsOn) parent = newParent; } - }); + }) } assign(node, value) { @@ -986,7 +931,7 @@ function shadergenerator(p5, fn) { return; } node.assertUsedInConditional(this); - this.dependsOn.push(makeDependencyObject(node)); + this.dependsOn.push(makeDependencyObject(node)) if (value.shouldUseTemporaryVariable()) { value.assertUsedInConditional(this); this.dependsOn.push(makeDependencyObject(value)); @@ -1015,17 +960,17 @@ function shadergenerator(p5, fn) { statement = `${node.toGLSLBase(context)} = ${result};`; } else if (isFloatNode(node) && node.name) { - statement = `${node.parent.toGLSLBase(context)}.${node.name} = ${result};`; + statement = `${node.parent.toGLSLBase(context)}.${node.name} = ${result};`; } else { node.temporaryVariable = `temp_${context.getNextID()}`; - statement = `${node.type} ${node.toGLSLBase(context)} = ${result};`; + statement = `${node.type} ${node.toGLSLBase(context)} = ${result};` } this.statements.push(statement); - }); + }) - return this.statements.join('\n '); + return this.statements.join(`\n `); } } @@ -1062,12 +1007,12 @@ function shadergenerator(p5, fn) { length += parseInt(val.type.slice(3)); } if (![2, 3, 4].includes(length)) { - throw new Error(`You have attempted to construct a vector with ${length} values. Only vec2, vec3, and vec4 types are supported.`); + throw new Error(`You have attempted to construct a vector with ${length} values. Only vec2, vec3, and vec4 types are supported.`) } - return length; + return length } - p5.dynamicNode = function (input) { + fn.dynamicNode = function (input) { if (isShaderNode(input)) { return input; } @@ -1077,13 +1022,13 @@ function shadergenerator(p5, fn) { else if (Array.isArray(input)) { return nodeConstructors.dynamicVector(input); } - }; + } // For replacing unary expressions - p5.unaryNode = function(input, sign) { - input = p5.dynamicNode(input); + fn.unaryNode = function(input, sign) { + input = dynamicNode(input); return dynamicAddSwizzleTrap(new UnaryExpressionNode(input, sign)); - }; + } function isShaderNode(node) { return (node instanceof BaseNode); @@ -1114,7 +1059,7 @@ function shadergenerator(p5, fn) { } function isConditionalNode(node) { - return (node instanceof ConditionalNode || node instanceof BranchNode); + return (node instanceof ConditionalNode || node instanceof BranchNode) } function hasTemporaryVariable(node) { @@ -1122,11 +1067,7 @@ function shadergenerator(p5, fn) { } function isPrimitiveNode(node) { - return ( - node instanceof FloatNode || - node instanceof IntNode || - node instanceof VectorNode - ); + return (node instanceof FloatNode || node instanceof IntNode || node instanceof VectorNode); } function isFunctionCallNode(node) { @@ -1134,11 +1075,11 @@ function shadergenerator(p5, fn) { } function isVectorNode(node) { - return (node instanceof VectorNode); + return (node instanceof VectorNode) } function isUnaryExpressionNode(node) { - return (node instanceof UnaryExpressionNode); + return (node instanceof UnaryExpressionNode) } // Helper function to check if a type is a user defined struct or native type @@ -1161,8 +1102,8 @@ function shadergenerator(p5, fn) { this.output = { vertexDeclarations: new Set(), fragmentDeclarations: new Set(), - uniforms: {} - }; + uniforms: {}, + } this.uniformNodes = []; this.resetGLSLContext(); this.isGenerating = false; @@ -1188,42 +1129,37 @@ function shadergenerator(p5, fn) { generateHookOverrides(originalShader) { const availableHooks = { ...originalShader.hooks.vertex, - ...originalShader.hooks.fragment - }; + ...originalShader.hooks.fragment, + } const windowOverrides = {}; - const fnOverrides = {}; - Object.keys(availableHooks).forEach(hookName => { + Object.keys(availableHooks).forEach((hookName) => { const hookTypes = originalShader.hookTypes(hookName); // These functions are where the user code is executed this[hookTypes.name] = function(userCallback) { // Create the initial nodes which are passed to the user callback // Also generate a string of the arguments for the code generation - const argNodes = []; + const argNodes = [] const argsArray = []; - hookTypes.parameters.forEach(parameter => { + hookTypes.parameters.forEach((parameter) => { // For hooks with structs as input we should pass an object populated with variable nodes if (!isGLSLNativeType(parameter.type.typeName)) { const structArg = {}; - parameter.type.properties.forEach(property => { + parameter.type.properties.forEach((property) => { structArg[property.name] = variableConstructor(`${parameter.name}.${property.name}`, property.type.typeName, true); }); argNodes.push(structArg); } else { argNodes.push( - variableConstructor( - parameter.name, - parameter.type.typeName, - true - ) + variableConstructor(parameter.name, parameter.type.typeName, true) ); } const qualifiers = parameter.type.qualifiers.length > 0 ? parameter.type.qualifiers.join(' ') : ''; - argsArray.push(`${qualifiers} ${parameter.type.typeName} ${parameter.name}`.trim()); - }); + argsArray.push(`${qualifiers} ${parameter.type.typeName} ${parameter.name}`.trim()) + }) let returnedValue = userCallback(...argNodes); const expectedReturnType = hookTypes.returnType; @@ -1231,34 +1167,26 @@ function shadergenerator(p5, fn) { // If the expected return type is a struct we need to evaluate each of its properties if (!isGLSLNativeType(expectedReturnType.typeName)) { - Object.entries(returnedValue) - .forEach(([propertyName, propertyNode]) => { - propertyNode = p5.dynamicNode(propertyNode); - toGLSLResults[propertyName] = propertyNode - .toGLSLBase(this.context); - this.context.updateComponents(propertyNode); - }); + Object.entries(returnedValue).forEach(([propertyName, propertyNode]) => { + propertyNode = dynamicNode(propertyNode); + toGLSLResults[propertyName] = propertyNode.toGLSLBase(this.context); + this.context.updateComponents(propertyNode); + }); } else { if (!isShaderNode(returnedValue)) { - returnedValue = - nodeConstructors[expectedReturnType.typeName](returnedValue); + returnedValue = nodeConstructors[expectedReturnType.typeName](returnedValue) } else if (isFloatType(returnedValue) && expectedReturnType.typeName.startsWith('vec')) { - returnedValue = - nodeConstructors[expectedReturnType.typeName](returnedValue); + returnedValue = nodeConstructors[expectedReturnType.typeName](returnedValue); } toGLSLResults['notAProperty'] = returnedValue.toGLSLBase(this.context); this.context.updateComponents(returnedValue); } - this.context.ifs.forEach(statement => { + this.context.ifs.forEach((statement) => { if (statement.usedIn.length === 0) { return; } const lines = statement.toGLSL(this.context); - this.context.declarations.splice( - statement.insertionPoint, - 0, - lines - ); - }); + this.context.declarations.splice(statement.insertionPoint, 0, lines); + }) // Build the final GLSL string. // The order of this code is a bit confusing, we need to call toGLSLBase let codeLines = [ @@ -1269,7 +1197,7 @@ function shadergenerator(p5, fn) { Object.entries(toGLSLResults).forEach(([propertyName, result]) => { const propString = expectedReturnType.properties ? `.${propertyName}` : ''; - codeLines.push(` finalReturnValue${propString} = ${result};`); + codeLines.push(` finalReturnValue${propString} = ${result};`) }); this.context.declarations = []; @@ -1278,7 +1206,7 @@ function shadergenerator(p5, fn) { const finalVaryingAssignments = []; declArray.forEach(obj => { const { node, value } = obj; - finalVaryingAssignments.push(` ${node.name} = ${value.toGLSLBase(this.context)};`); + finalVaryingAssignments.push(` ${node.name} = ${value.toGLSLBase(this.context)};`) finalVaryingAssignments.unshift(...this.context.declarations); node.timesChanged = 0; }); @@ -1288,17 +1216,13 @@ function shadergenerator(p5, fn) { codeLines.push(' return finalReturnValue;', '}'); this.output[hookName] = codeLines.join('\n'); this.resetGLSLContext(); - }; + } windowOverrides[hookTypes.name] = window[hookTypes.name]; - fnOverrides[hookTypes.name] = fn[hookTypes.name]; // Expose the Functions to global scope for users to use window[hookTypes.name] = function(userOverride) { GLOBAL_SHADER[hookTypes.name](userOverride); }; - fn[hookTypes.name] = function(userOverride) { - GLOBAL_SHADER[hookTypes.name](userOverride); - }; }); @@ -1306,9 +1230,6 @@ function shadergenerator(p5, fn) { for (const key in windowOverrides) { window[key] = windowOverrides[key]; } - for (const key in fnOverrides) { - fn[key] = fnOverrides[key]; - } }; } @@ -1322,13 +1243,13 @@ function shadergenerator(p5, fn) { } resetGLSLContext() { - this.uniformNodes.forEach(node => { + this.uniformNodes.forEach((node) => { node.usedIn = []; node.temporaryVariable = undefined; }); this.context = { id: 0, - getNextID() { return this.id++; }, + getNextID() { return this.id++ }, declarations: [], varyings: [], ifs: [], @@ -1348,22 +1269,22 @@ function shadergenerator(p5, fn) { } }); } else { - const components = node.componentNames.map(name => { - return node[name]; + const components = node.componentNames.map((name) => { + return node[name] }); const replacement = nodeConstructors[node.type](components); - const line = ` ${node.temporaryVariable} = ${replacement.toGLSLBase(this)};`; + const line = ` ${node.temporaryVariable} = ${replacement.toGLSLBase(this)};` lines.push(line); } if (_emplaceAt) { - this.declarations.splice(_emplaceAt, 0, ...lines); + this.declarations.splice(_emplaceAt, 0, ...lines) } else { this.declarations.push(...lines); } node.componentsChanged = false; } } - }; + } this.uniformNodes = []; } } @@ -1427,7 +1348,7 @@ function shadergenerator(p5, fn) { for (const set of swizzleSets) { if ([...property].every(char => set.includes(char))) { if (property.length === 1) { - return target[swizzleSets[0][set.indexOf(property[0])]]; + return target[swizzleSets[0][set.indexOf(property[0])]] } const components = [...property].map(char => { const index = set.indexOf(char); @@ -1445,16 +1366,11 @@ function shadergenerator(p5, fn) { for (const set of swizzleSets) { const propertyCharArray = [...property]; if (propertyCharArray.every(char => set.includes(char))) { - const newValues = Array.isArray(value) ? - value : - Array(property.length).fill(value); + const newValues = Array.isArray(value) ? value : Array(property.length).fill(value); propertyCharArray.forEach((char, i) => { const index = set.indexOf(char); const realProperty = swizzleSets[0][index]; - const descriptor = Object.getOwnPropertyDescriptor( - target, - realProperty - ); + const descriptor = Object.getOwnPropertyDescriptor(target, realProperty); Reflect.set(target, realProperty, newValues[i], receiver); }); return true; @@ -1462,22 +1378,22 @@ function shadergenerator(p5, fn) { } return Reflect.set(...arguments); } - }; + } } // User functions fn.If = function (condition, branch) { return new ConditionalNode(condition, branch); - }; + } fn.instanceID = function() { return variableConstructor('gl_InstanceID', 'int'); - }; + } fn.getTexture = function(...userArgs) { const props = { args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true }; return fnNodeConstructor('getTexture', userArgs, props); - }; + } // Generating uniformFloat, uniformVec, createFloat, etc functions // Maps a GLSL type to the name suffix for method names @@ -1487,7 +1403,7 @@ function shadergenerator(p5, fn) { vec2: 'Vector2', vec3: 'Vector3', vec4: 'Vector4', - sampler2D: 'Texture' + sampler2D: 'Texture', }; function dynamicAddSwizzleTrap(node, _size) { @@ -1519,14 +1435,13 @@ function shadergenerator(p5, fn) { node = dynamicAddSwizzleTrap(node); node.dependsOn = makeDependencyArray(node.args); const dependsOnConditionals = node.args.map(arg => { - const conditionals = arg.usedIn - .filter(n => isConditionalNode(n)).map(c => { - if (c instanceof BranchNode) { - return c.parent; - } else { - return c; - } - }); + const conditionals = arg.usedIn.filter(n => isConditionalNode(n)).map(c => { + if (c instanceof BranchNode) { + return c.parent; + } else { + return c; + } + }); return conditionals; }).flat(); dependsOnConditionals.forEach(conditional => conditional.usedIn.push(node)); @@ -1535,15 +1450,15 @@ function shadergenerator(p5, fn) { } const nodeConstructors = { - int: value => new IntNode(value), - float: value => new FloatNode(value), - vec2: value => dynamicAddSwizzleTrap(new VectorNode(value, 'vec2')), - vec3: value => dynamicAddSwizzleTrap(new VectorNode(value, 'vec3')), - vec4: value => dynamicAddSwizzleTrap(new VectorNode(value, 'vec4')), + int: (value) => new IntNode(value), + float: (value) => new FloatNode(value), + vec2: (value) => dynamicAddSwizzleTrap(new VectorNode(value, 'vec2')), + vec3: (value) => dynamicAddSwizzleTrap(new VectorNode(value, 'vec3')), + vec4: (value) => dynamicAddSwizzleTrap(new VectorNode(value, 'vec4')), dynamicVector: function(value) { const size = computeVectorLength(value); return this[`vec${size}`](value); - } + }, }; for (const glslType in GLSLTypesToIdentifiers) { @@ -1554,10 +1469,7 @@ function shadergenerator(p5, fn) { ShaderGenerator.prototype[uniformMethodName] = function(...args) { let [name, ...defaultValue] = args; if (glslType.startsWith('vec') && !(defaultValue[0] instanceof Function)) { - defaultValue = conformVectorParameters( - defaultValue, - parseInt(glslType.slice(3)) - ); + defaultValue = conformVectorParameters(defaultValue, parseInt(glslType.slice(3))); this.output.uniforms[`${glslType} ${name}`] = defaultValue; } else { @@ -1579,7 +1491,7 @@ function shadergenerator(p5, fn) { const varyingMethodName = `varying${typeIdentifier}`; ShaderGenerator.prototype[varyingMethodName] = function(name) { return dynamicAddSwizzleTrap(new VaryingNode(name, glslType, false)); - }; + } fn[varyingMethodName] = function (name) { return GLOBAL_SHADER[varyingMethodName](name); @@ -1610,96 +1522,96 @@ function shadergenerator(p5, fn) { // https://docs.gl/el3/abs const builtInGLSLFunctions = { //////////// Trigonometry ////////// - 'acos': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'acosh': { args: ['genType'], returnType: 'genType', isp5Function: false }, - 'asin': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'asinh': { args: ['genType'], returnType: 'genType', isp5Function: false }, + 'acos': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'acosh': { args: ['genType'], returnType: 'genType', isp5Function: false}, + 'asin': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'asinh': { args: ['genType'], returnType: 'genType', isp5Function: false}, 'atan': [ - { args: ['genType'], returnType: 'genType', isp5Function: false }, - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false } + // Single-argument atan is a normal p5 function and should work outside strands + { args: ['genType'], returnType: 'genType', isp5Function: true}, + // Two-argument atan(y, x) is GLSL-only and remains strands-only + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, ], - 'atanh': { args: ['genType'], returnType: 'genType', isp5Function: false }, - 'cos': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'cosh': { args: ['genType'], returnType: 'genType', isp5Function: false }, - 'degrees': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'radians': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'sin': { args: ['genType'], returnType: 'genType' , isp5Function: true }, - 'sinh': { args: ['genType'], returnType: 'genType', isp5Function: false }, - 'tan': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'tanh': { args: ['genType'], returnType: 'genType', isp5Function: false }, + 'atanh': { args: ['genType'], returnType: 'genType', isp5Function: false}, + 'cos': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'cosh': { args: ['genType'], returnType: 'genType', isp5Function: false}, + 'degrees': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'radians': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'sin': { args: ['genType'], returnType: 'genType' , isp5Function: true}, + 'sinh': { args: ['genType'], returnType: 'genType', isp5Function: false}, + 'tan': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'tanh': { args: ['genType'], returnType: 'genType', isp5Function: false}, ////////// Mathematics ////////// - 'abs': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'ceil': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'clamp': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false }, - 'dFdx': { args: ['genType'], returnType: 'genType', isp5Function: false }, - 'dFdy': { args: ['genType'], returnType: 'genType', isp5Function: false }, - 'exp': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'exp2': { args: ['genType'], returnType: 'genType', isp5Function: false }, - 'floor': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'fma': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false }, - 'fract': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'fwidth': { args: ['genType'], returnType: 'genType', isp5Function: false }, - 'inversesqrt': { args: ['genType'], returnType: 'genType', isp5Function: true }, + 'abs': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'ceil': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'clamp': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + 'dFdx': { args: ['genType'], returnType: 'genType', isp5Function: false}, + 'dFdy': { args: ['genType'], returnType: 'genType', isp5Function: false}, + 'exp': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'exp2': { args: ['genType'], returnType: 'genType', isp5Function: false}, + 'floor': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'fma': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + 'fract': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'fwidth': { args: ['genType'], returnType: 'genType', isp5Function: false}, + 'inversesqrt': { args: ['genType'], returnType: 'genType', isp5Function: true}, // 'isinf': {}, // 'isnan': {}, - 'log': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'log2': { args: ['genType'], returnType: 'genType', isp5Function: false }, + 'log': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'log2': { args: ['genType'], returnType: 'genType', isp5Function: false}, 'max': [ - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true }, - { args: ['genType', 'float'], returnType: 'genType', isp5Function: true } + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, ], 'min': [ - { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true }, - { args: ['genType', 'float'], returnType: 'genType', isp5Function: true } + { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, + { args: ['genType', 'float'], returnType: 'genType', isp5Function: true}, ], 'mix': [ - { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false }, - { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false } + { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}, ], // 'mod': {}, // 'modf': {}, - 'pow': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true }, - 'round': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'roundEven': { args: ['genType'], returnType: 'genType', isp5Function: false }, + 'pow': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: true}, + 'round': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'roundEven': { args: ['genType'], returnType: 'genType', isp5Function: false}, // 'sign': {}, 'smoothstep': [ - { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false }, - { args: ['float', 'float', 'genType'], returnType: 'genType', isp5Function: false } + { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + { args: ['float', 'float', 'genType'], returnType: 'genType', isp5Function: false}, ], - 'sqrt': { args: ['genType'], returnType: 'genType', isp5Function: true }, - 'step': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false }, - 'trunc': { args: ['genType'], returnType: 'genType', isp5Function: false }, + 'sqrt': { args: ['genType'], returnType: 'genType', isp5Function: true}, + 'step': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, + 'trunc': { args: ['genType'], returnType: 'genType', isp5Function: false}, ////////// Vector ////////// - 'cross': { args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true }, - 'distance': { args: ['genType', 'genType'], returnType: 'float', isp5Function: true }, - 'dot': { args: ['genType', 'genType'], returnType: 'float', isp5Function: true }, + 'cross': { args: ['vec3', 'vec3'], returnType: 'vec3', isp5Function: true}, + 'distance': { args: ['genType', 'genType'], returnType: 'float', isp5Function: true}, + 'dot': { args: ['genType', 'genType'], returnType: 'float', isp5Function: true}, // 'equal': {}, - 'faceforward': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false }, - 'length': { args: ['genType'], returnType: 'float', isp5Function: false }, - 'normalize': { args: ['genType'], returnType: 'genType', isp5Function: true }, + 'faceforward': { args: ['genType', 'genType', 'genType'], returnType: 'genType', isp5Function: false}, + 'length': { args: ['genType'], returnType: 'float', isp5Function: false}, + 'normalize': { args: ['genType'], returnType: 'genType', isp5Function: true}, // 'notEqual': {}, - 'reflect': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false }, - 'refract': { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false }, + 'reflect': { args: ['genType', 'genType'], returnType: 'genType', isp5Function: false}, + 'refract': { args: ['genType', 'genType', 'float'], returnType: 'genType', isp5Function: false}, ////////// Texture sampling ////////// - 'texture': { args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true } - }; + 'texture': {args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true}, + } Object.entries(builtInGLSLFunctions).forEach(([functionName, properties]) => { - const isp5Function = Array.isArray(properties) ? - properties[0].isp5Function : - properties.isp5Function; + const isp5Function = Array.isArray(properties) ? properties[0].isp5Function : properties.isp5Function; if (isp5Function) { const originalFn = fn[functionName]; fn[functionName] = function (...args) { if (GLOBAL_SHADER?.isGenerating) { - return fnNodeConstructor(functionName, args, properties); + return fnNodeConstructor(functionName, args, properties) } else { return originalFn.apply(this, args); } - }; + } } else { fn[functionName] = function (...args) { if (GLOBAL_SHADER?.isGenerating) { @@ -1709,9 +1621,9 @@ function shadergenerator(p5, fn) { `It looks like you've called ${functionName} outside of a shader's modify() function.` ); } - }; + } } - }); + }) // Alias GLSL's mix function as lerp in p5.strands // Bridging p5.js lerp and GLSL mix for consistency in shader expressions const originalLerp = fn.lerp; @@ -1722,34 +1634,14 @@ function shadergenerator(p5, fn) { return originalLerp.apply(this, args); // Fallback to normal p5.js lerp } }; - const originalNoise = fn.noise; - fn.noise = function (...args) { - if (!GLOBAL_SHADER?.isGenerating) { - return originalNoise.apply(this, args); // fallback to regular p5.js noise - } - - GLOBAL_SHADER.output.vertexDeclarations.add(noiseGLSL); - GLOBAL_SHADER.output.fragmentDeclarations.add(noiseGLSL); - // Handle noise(x, y) as noise(vec2) - let nodeArgs; - if (args.length === 2) { - nodeArgs = [fn.vec2(args[0], args[1])]; - } else { - nodeArgs = args; - } - - return fnNodeConstructor('noise', nodeArgs, { - args: ['vec2'], - returnType: 'float' - }); - }; - } + + export default shadergenerator; if (typeof p5 !== 'undefined') { - p5.registerAddon(shadergenerator); + p5.registerAddon(shadergenerator) } diff --git a/test/unit/spec.js b/test/unit/spec.js index fd9bbc759f..8ab9d28895 100644 --- a/test/unit/spec.js +++ b/test/unit/spec.js @@ -55,7 +55,8 @@ var spec = { // to omit some for speed if they should only be run manually. 'webgl', 'typography', - 'shape_modes' + 'shape_modes', + 'math' ] }; document.write( diff --git a/test/unit/visual/cases/math.js b/test/unit/visual/cases/math.js new file mode 100644 index 0000000000..b93d331a75 --- /dev/null +++ b/test/unit/visual/cases/math.js @@ -0,0 +1,19 @@ +import { visualTest, visualSuite } from '../../visual/visualTest.js'; + +visualSuite('math', () => { + visualTest('atan_outside_strands', async (p5, screenshot) => { + // Ensure no WebGL/strands context is used; call atan directly and draw text + p5.createCanvas(120, 60); + p5.background(255); + p5.fill(0); + const v = p5.atan(0.5); + // Draw the numeric value so visual regression will catch undefined/NaN + p5.textSize(14); + p5.textFont('monospace'); + p5.textAlign(p5.LEFT, p5.TOP); + p5.text(`atan(0.5)=${p5.nf(v, 1, 3)}`, 5, 5); + screenshot(); + }); +}); + +