p5
Version:
[](https://www.npmjs.com/package/p5)
1,486 lines (1,337 loc) • 56.6 kB
JavaScript
import { parse } from 'acorn';
import { ancestor } from 'acorn-walk';
import escodegen from 'escodegen';
/**
* @module 3D
* @submodule ShaderGenerator
* @for p5
* @requires core
*/
function shadergenerator(p5, fn) {
let GLOBAL_SHADER;
let BRANCH;
const oldModify = p5.Shader.prototype.modify;
p5.Shader.prototype.modify = function(shaderModifier, options = { parser: true, srcLocations: false }) {
if (shaderModifier instanceof Function) {
let generatorFunction;
if (options.parser) {
const sourceString = shaderModifier.toString();
const ast = parse(sourceString, {
ecmaVersion: 2021,
locations: options.srcLocations
});
ancestor(ast, ASTCallbacks, undefined, { varyings: {} });
const transpiledSource = escodegen.generate(ast);
generatorFunction = new Function(
transpiledSource
.slice(
transpiledSource.indexOf('{') + 1,
transpiledSource.lastIndexOf('}')
).replaceAll(';', '')
);
} else {
generatorFunction = shaderModifier;
}
const generator = new ShaderGenerator(generatorFunction, this, options.srcLocations);
const generatedModifyArgument = generator.generate();
return oldModify.call(this, generatedModifyArgument);
}
else {
return oldModify.call(this, shaderModifier)
}
};
// AST Transpiler Callbacks and helper functions
function replaceBinaryOperator(codeSource) {
switch (codeSource) {
case '+': return 'add';
case '-': return 'sub';
case '*': return 'mult';
case '/': return 'div';
case '%': return 'mod';
case '==':
case '===': return 'equalTo';
case '>': return 'greaterThan';
case '>=': return 'greaterThanEqualTo';
case '<': return 'lessThan';
case '&&': return 'and';
case '||': return 'or';
}
}
function ancestorIsUniform(ancestor) {
return ancestor.type === 'CallExpression'
&& ancestor.callee?.type === 'Identifier'
&& ancestor.callee?.name.startsWith('uniform');
}
const ASTCallbacks = {
UnaryExpression(node, _state, _ancestors) {
if (_ancestors.some(ancestorIsUniform)) { return; }
const signNode = {
type: 'Literal',
value: node.operator,
};
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;
const swizzleSets = [
['x', 'y', 'z', 'w'],
['r', 'g', 'b', 'a'],
['s', 't', 'p', 'q']
];
let isSwizzle = swizzleSets.some(set =>
[...property].every(char => set.includes(char))
) && node.argument.type === 'MemberExpression';
if (isSwizzle) {
node.type = 'MemberExpression';
node.object = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'unaryNode'
},
arguments: [node.argument.object, signNode],
};
node.property = {
type: 'Identifier',
name: property
};
} else {
standardReplacement(node);
}
} else {
standardReplacement(node);
}
delete node.argument;
delete node.operator;
},
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 (_state.varyings[node.name]
&& !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) {
node.type = 'ExpressionStatement';
node.expression = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: node.name
},
property: {
type: 'Identifier',
name: 'getValue'
},
},
arguments: [],
};
}
},
// The callbacks for AssignmentExpression and BinaryExpression handle
// operator overloading including +=, *= assignment expressions
ArrayExpression(node, _state, _ancestors) {
const original = JSON.parse(JSON.stringify(node));
node.type = 'CallExpression';
node.callee = {
type: 'Identifier',
name: 'dynamicNode',
};
node.arguments = [original];
},
AssignmentExpression(node, _state, _ancestors) {
if (node.operator !== '=') {
const methodName = replaceBinaryOperator(node.operator.replace('=',''));
const rightReplacementNode = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
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
},
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(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'];
if (unsafeTypes.includes(node.left.type)) {
const leftReplacementNode = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'dynamicNode',
},
arguments: [node.left]
};
node.left = leftReplacementNode;
}
// Replace the binary operator with a call expression
// in other words a call to BaseNode.mult(), .div() etc.
node.type = 'CallExpression';
node.callee = {
type: 'MemberExpression',
object: node.left,
property: {
type: 'Identifier',
name: replaceBinaryOperator(node.operator),
},
};
node.arguments = [node.right];
},
};
// Javascript Node API.
class BaseNode {
constructor(isInternal, type) {
if (new.target === BaseNode) {
throw new TypeError('Cannot construct BaseNode instances directly. This is an abstract class.');
}
this.type = type;
this.componentNames = [];
this.componentsChanged = false;
// For tracking recursion depth and creating temporary variables
this.isInternal = isInternal;
this.usedIn = [];
this.dependsOn = [];
this.srcLine = null;
this.usedInConditional = false;
// Stack Capture is used to get the original line of user code for Debug purposes
if (GLOBAL_SHADER.srcLocations === true && isInternal === false) {
try {
throw new Error('StackCapture');
} catch (e) {
const lines = e.stack.split('\n');
let userSketchLineIndex = 5;
if (isBinaryExpressionNode(this)) { userSketchLineIndex--; } this.srcLine = lines[userSketchLineIndex].trim();
}
}
}
addVectorComponents() {
if (this.type.startsWith('vec')) {
const vectorDimensions = parseInt(this.type.slice(3));
this.componentNames = ['x', 'y', 'z', 'w'].slice(0, vectorDimensions);
const proxy = this;
for (let componentName of this.componentNames) {
let value = new ComponentNode(proxy, componentName, 'float', true);
Object.defineProperty(this, componentName, {
get() {
return value;
},
set(newValue) {
this.componentsChanged = true;
if (isUnaryExpressionNode(this)) {
this.node.value = newValue;
} else {
value = newValue;
}
}
});
}
}
}
forceTemporaryVariable() {
if (!(isFloatNode(this) && isVectorNode(this.parent)) || !isVariableNode(this))
this.useTemp = true;
}
assertUsedInConditional(branch) {
this.usedInConditional = true;
this.usedIn.push(branch);
this.forceTemporaryVariable();
}
isUsedInConditional() {
return this.usedInConditional;
}
checkConditionalDependencies(context) {
context.ifs.forEach((statement) => {
const isUsedSatisfied = () => statement.usedInSatisfied.length >= 1;
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)) {
statement.dependsOnSatisfied.push(this);
}
if (statement.usedIn.includes(this) && !statement.usedInSatisfied.includes(this)) {
statement.usedInSatisfied.push(this);
}
if (isDepsSatisfied() && isUsedSatisfied()) {
statement.saveState(context, isDepsSatisfied(), isUsedSatisfied());
}
});
}
// The base node implements a version of toGLSL which determines whether the generated code should be stored in a temporary variable.
toGLSLBase(context){
let result;
if (this.shouldUseTemporaryVariable()) {
let oldLength = context.declarations.length;
result = this.getTemporaryVariable(context);
let diff = context.declarations.length - 1 - oldLength;
diff = diff > 0 ? diff : undefined;
this.dependsOn.forEach(dependency => {
if (dependency.isVector) {
const dependencies = dependency.originalComponents.map((component, i) =>
component === dependency.currentComponents[i]
);
context.updateComponents(dependency.node, diff, dependencies);
} else {
context.updateComponents(dependency.node, diff);
}
});
} else {
result = this.toGLSL(context);
}
this.checkConditionalDependencies(context);
return result;
}
shouldUseTemporaryVariable() {
if (this.componentsChanged || hasTemporaryVariable(this) || this.useTemp) { return true; }
if (this.isInternal || isVariableNode(this) || isConditionalNode(this) || this.type === 'sampler2D') { return false; }
// return false;
// Swizzles must use temporary variables as otherwise they will not be registered
let score = 0;
score += isFunctionCallNode(this) * 2;
score += isBinaryExpressionNode(this) * 2;
score += isVectorType(this) * 3;
score += this.usedIn.length;
return score >= 4;
}
getTemporaryVariable(context) {
if (!this.temporaryVariable) {
this.temporaryVariable = `temp_${context.getNextID()}`;
let line = '';
if (this.srcLine) {
line += `\n// From ${this.srcLine}\n`;
}
line += ' ' + this.type + ' ' + this.temporaryVariable + ' = ' + this.toGLSL(context) + ';';
context.declarations.push(line);
}
return this.temporaryVariable;
};
// Binary Operators
add(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '+'); }
sub(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '-'); }
mult(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '*'); }
div(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '/'); }
mod(other) { return binaryExpressionNodeConstructor(this, this.enforceType(other), '%'); }
// Check that the types of the operands are compatible.
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 '.'`)
}
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 '.'`)
}
if ((isFloatType(this) || isVectorType(this)) && isIntType(other)) {
return new FloatNode(other)
}
return other;
}
else if (typeof other === 'number') {
if (isIntType(this)) {
return new IntNode(other);
}
return new FloatNode(other);
}
else if (Array.isArray(other)) {
return nodeConstructors.dynamicVector(other);
// return nodeConstructors[`vec${other.length}`](other);
}
else {
return nodeConstructors[this.type](other);
}
}
toFloat() {
if (isFloatType(this)) {
return this;
} else if (isIntType(this)) {
return new FloatNode(this);
}
}
toGLSL(context){
throw new TypeError('Not supposed to call this function on BaseNode, which is an abstract class.');
}
}
// Primitive Types
class IntNode extends BaseNode {
constructor(x = 0, isInternal = false) {
super(isInternal, 'int');
this.x = x;
}
toGLSL(context) {
if (isShaderNode(this.x)) {
let code = this.x.toGLSLBase(context);
return isIntType(this.x.type) ? code : `int(${code})`;
}
else if (typeof this.x === 'number') {
return `${Math.floor(this.x)}`;
}
else {
return `int(${this.x})`;
}
}
}
class FloatNode extends BaseNode {
constructor(x = 0, isInternal = false, _parent = false){
super(isInternal, 'float');
if (Array.isArray(x)) {
x = x[0];
}
if (_parent) {
const { parent, name } = _parent;
this.name = name;
this.parent = parent;
}
this.x = x;
}
toGLSL(context) {
if (isShaderNode(this.x)) {
let code = this.x.toGLSLBase(context);
return isFloatType(this.x) ? code : `float(${code})`;
}
else if (typeof this.x === 'number') {
return `${this.x.toFixed(4)}`;
}
else {
return `float(${this.x})`;
}
}
}
class VectorNode extends BaseNode {
constructor(values, type, isInternal = false) {
super(isInternal, type);
this.originalValues = conformVectorParameters(values, parseInt(type.slice(3)));
this.componentNames = ['x', 'y', 'z', 'w'].slice(0, this.originalValues.length);
}
addVectorComponents() {
const values = this.originalValues;
this.componentsChanged = false;
this.componentNames.forEach((componentName, i) => {
const info = { name: componentName, parent: this };
let value = isFloatNode(values[i]) ? values[i] : new FloatNode(values[i], true, info);
Object.defineProperty(this, componentName, {
get() {
return value;
},
set(newValue) {
this.componentsChanged = true;
if (isUnaryExpressionNode(this)) {
this.node.value = newValue;
} else {
value = isFloatNode(newValue) ? newValue : new FloatNode(newValue, true, info);
}
}
});
});
this.originalValues = this.componentNames.map(name => this[name]);
}
toGLSL(context) {
if ((!this.componentsChanged || !this.defined) && !this.oldName) {
let glslArgs = this.componentNames.map((_name, i) => this.originalValues[i].toGLSLBase(context)).join(', ');
this.defined = true;
return `${this.type}(${glslArgs})`;
} else {
return this.temporaryVariable;
}
}
}
// Function Call Nodes
class FunctionCallNode extends BaseNode {
constructor(name, userArgs, properties, isInternal = false) {
let functionSignature;
const determineFunctionSignature = (props) => {
let genType;
let similarity = 0;
const valid = userArgs.every((userArg, i) => {
const userType = getType(userArg);
let expectedArgType = props.args[i];
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;
} expectedArgType = genType;
}
similarity += (userType === expectedArgType);
return userType === expectedArgType || (userType === 'float' && expectedArgType.startsWith('vec'));
});
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);
if (possibleOverloads.length === 0) {
const argsLengthSet = new Set();
const argsLengthArr = [];
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.`);
}
const findBestOverload = function (best, current) {
current = determineFunctionSignature(current);
if (!current.valid) { return best; }
if (!best || current.similarity > best.similarity) {
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 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.
`);
}
if (userArgs.length !== functionSignature.args.length) {
throw new Error(`Function '${name}' expects ${functionSignature.args.length} arguments, but ${userArgs.length} were provided.`);
}
userArgs = userArgs.map((arg, i) => {
if (!isShaderNode(arg)) {
const typeName = functionSignature.args[i] === 'genType' ? functionSignature.genType : functionSignature.args[i];
arg = nodeConstructors[typeName](arg);
} else if (isFloatType(arg) && functionSignature.args[i] === 'genType' && functionSignature.genType !== 'float') {
arg = nodeConstructors[functionSignature.genType](arg);
}
return arg;
});
if (functionSignature.returnType === 'genType') {
functionSignature.returnType = functionSignature.genType;
}
super(isInternal, functionSignature.returnType);
this.name = name;
this.args = userArgs;
this.argumentTypes = functionSignature.args;
}
deconstructArgs(context) {
let argsString = this.args.map((argNode, i) => {
if (isIntType(argNode) && this.argumentTypes[i] != 'float') {
argNode = argNode.toFloat();
}
argNode.toGLSLBase(context);
return argNode.toGLSLBase(context);
}).join(', ');
return argsString;
}
toGLSL(context) {
return `${this.name}(${this.deconstructArgs(context)})`;
}
}
// Variables and member variable nodes
class VariableNode extends BaseNode {
constructor(name, type, isInternal = false) {
super(isInternal, type);
this.name = name;
}
toGLSL(context) {
return `${this.name}`;
}
}
class ComponentNode extends BaseNode {
constructor(parent, componentName, type, isInternal = false) {
super(isInternal, type);
this.parent = parent;
this.componentName = componentName;
this.type = type;
}
toGLSL(context) {
let parentName = this.parent.toGLSLBase(context);
if (!isVariableNode(this.parent) && !hasTemporaryVariable(this.parent)) {
parentName = `(${parentName})`;
}
return `${parentName}.${this.componentName}`;
}
}
//
class VaryingNode extends VariableNode {
constructor(name, type, isInternal = false) {
super(name, type, isInternal);
this.timesChanged = 0;
this.tempVars = 0;
}
getValue() {
const context = GLOBAL_SHADER.context;
if (!context.varyings[this.name] || !this.timesChanged) {
return this;
}
let values = context.varyings[this.name].splice(0, this.timesChanged);
let snapshot;
values.forEach((val, i) => {
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));
context.declarations.push(` ${this.type} ${tempName} = ${this.name};`);
}
});
this.timesChanged = 0;
return snapshot;
}
bridge(value) {
if (!isShaderNode(value) || this.type.startsWith('vec') && getType(value) === 'float') {
value = nodeConstructors[this.type](value);
}
GLOBAL_SHADER.registerVarying(this, value);
this.timesChanged += 1;
}
}
// Binary Operator Nodes
class BinaryExpressionNode extends BaseNode {
constructor(left, right, operator, isInternal = false) {
super(isInternal, null);
this.operator = operator;
this.left = left;
this.right = right;
for (const operand of [left, right]) {
operand.usedIn.push(this);
}
this.type = this.determineType();
}
// We know that both this.left and this.right are nodes because of BaseNode.enforceType
determineType() {
if (['==', '>', '>=', '<', '<=', '||', '!', '&&'].includes(this.operator)) {
return 'bool';
}
else if (this.left.type === this.right.type) {
return this.left.type;
}
else if (isVectorType(this.left) && isFloatType(this.right)) {
return this.left.type;
}
else if (isVectorType(this.right) && isFloatType(this.left)) {
return this.right.type;
}
else if (isFloatType(this.left) && isIntType(this.right)
|| isIntType(this.left) && isFloatType(this.right)
) {
return 'float';
}
else {
throw new Error('Incompatible types for binary operator');
}
}
processOperand(operand, context) {
if (operand.temporaryVariable) { return operand.temporaryVariable; }
let code = operand.toGLSLBase(context);
if (isBinaryExpressionNode(operand) && !operand.temporaryVariable) {
code = `(${code})`;
}
if (this.type === 'float' && isIntType(operand)) {
code = `float(${code})`;
}
return code;
}
toGLSL(context) {
const a = this.processOperand(this.left, context);
const b = this.processOperand(this.right, context);
return `${a} ${this.operator} ${b}`;
}
}
class ModulusNode extends BinaryExpressionNode {
constructor(a, b, isInternal) {
super(a, b, isInternal);
}
toGLSL(context) {
// Switch on type between % or mod()
if (isVectorType(this) || isFloatType(this)) {
return `mod(${this.left.toGLSLBase(context)}, ${this.right.toGLSLBase(context)})`;
}
return `${this.processOperand(context, this.left)} % ${this.processOperand(context, this.right)}`;
}
}
class UnaryExpressionNode extends BaseNode {
constructor(node, operator, isInternal = false) {
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})`;
}
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), '<='); };
BaseNode.prototype.not = function() {
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();
callback();
BRANCH = null;
return branch;
}
class ConditionalNode {
constructor(condition, branchCallback) {
this.dependsOn = [];
this.usedIn = [];
this.dependsOnSatisfied = [];
this.usedInSatisfied = [];
this.states = [];
this.if(condition, branchCallback);
this.insertionPoint = -1;
this.elseIfs = [];
this.elseBranch = null;
GLOBAL_SHADER.context.ifs.push(this);
}
if(condition, branchCallback) {
this.condition = condition;
this.conditionString = condition.toGLSL(GLOBAL_SHADER.context);
this.ifBranch = branch(branchCallback);
this.ifBranch.parent = this;
}
elseIf(condition, branchCallback) {
let elseBranch = branch(branchCallback);
branchCallback.parent = this;
this.elseIfs.push({ condition, elseBranch });
return this;
}
else(branchCallback) {
this.elseBranch = branch(branchCallback);
this.elseBranch.parent = this;
return this;
}
thenDiscard() {
return new ConditionalDiscard(this.condition);
};
saveState(context, usedInSatisfied, dependsOnSatisfied) {
this.states.push({
line: context.declarations.length,
usedInSatisfied,
dependsOnSatisfied
});
this.insertionPoint = context.declarations.length - 1;
}
toGLSL(context) {
const oldLength = context.declarations.length;
this.dependsOn.forEach(dep => context.updateComponents(dep.node));
const newLength = context.declarations.length;
const diff = newLength - oldLength;
this.insertionPoint += diff;
let codelines = [
`\n if (${this.conditionString}) {`,
`\n ${this.ifBranch.toGLSL(context)}`,
`\n }`
];
if (this.elseIfs.length) {
this.elseIfs.forEach((elif) => {
let { condition, elseBranch } = elif;
codelines.push(` else if (${condition.toGLSL(context)}) {`);
codelines.push(`\n ${elseBranch.toGLSL(context)}`);
codelines.push(`\n }`);
});
}
if (this.elseBranch) {
codelines.push(` else {`);
codelines.push(`\n ${this.elseBranch.toGLSL(context)}`);
codelines.push(`\n }\n`);
}
codelines.push('\n');
return codelines.flat().join('');
}
}
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.');
}
BRANCH.assign(node, value);
};
class BranchNode {
constructor() {
BRANCH = this;
this.statements = [];
this.assignments = [];
this.dependsOn = [];
this.declarations = [];
let parent = null;
Object.defineProperty(this, 'parent', {
get() {
return parent;
},
set(newParent) {
newParent.dependsOn.push(...this.dependsOn);
parent = newParent;
}
});
}
assign(node, value) {
if (!isShaderNode(value) || value.type !== node.type) {
value = nodeConstructors[node.type](value);
this.declarations.push(value);
this.assignments.push({ node });
} else {
this.assignments.push({ node, value });
}
node = node.parent ? node.parent : node;
value = value.parent ? value.parent : value;
if ([node, value].some(n => this.dependsOn.some(d=>d.node===n))) {
return;
}
node.assertUsedInConditional(this);
this.dependsOn.push(makeDependencyObject(node));
if (value.shouldUseTemporaryVariable()) {
value.assertUsedInConditional(this);
this.dependsOn.push(makeDependencyObject(value));
}
}
toGLSL(context) {
let declarationsIndex = 0;
this.assignments.forEach(({ node, value }) => {
let statement;
let result;
if (!value) {
let decl = this.declarations[declarationsIndex];
declarationsIndex++;
decl.temporaryVariable = `temp_${context.getNextID()}`;
this.statements.push(
`${decl.type} ${decl.temporaryVariable} = ${decl.toGLSL(context)};`
);
result = decl.toGLSLBase(context);
} else {
result = value.toGLSLBase(context);
}
if (isVariableNode(node) || hasTemporaryVariable(node)) {
statement = `${node.toGLSLBase(context)} = ${result};`;
}
else if (isFloatNode(node) && node.name) {
statement = `${node.parent.toGLSLBase(context)}.${node.name} = ${result};`;
}
else {
node.temporaryVariable = `temp_${context.getNextID()}`;
statement = `${node.type} ${node.toGLSLBase(context)} = ${result};`;
}
this.statements.push(statement);
});
return this.statements.join(`\n `);
}
}
class ConditionalDiscard {
constructor(condition){
this.condition = condition;
}
toGLSL(context) {
context.discardConditions.push(`if (${this.condition}{discard;})`);
}
}
// Node Helper functions
function getType(node) {
if (isShaderNode(node)) { return node.type; }
else if (Array.isArray(node) && node.length > 1) { return `vec${node.length}`; }
else if (typeof node === 'number' || (Array.isArray(node) && node.length === 1)) {
return 'float';
}
return undefined;
}
function computeVectorLength(values) {
let length = 0;
if (Array.isArray(values)) {
for(let val of values) {
if (isVectorType(val)) {
length += parseInt(val.type.slice(3));
}
else length += 1;
}
}
else if (isVectorType(values)) {
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.`)
}
return length
}
fn.dynamicNode = function (input) {
if (isShaderNode(input)) {
return input;
}
else if (typeof input === 'number') {
return new FloatNode(input);
}
else if (Array.isArray(input)) {
return nodeConstructors.dynamicVector(input);
}
};
// For replacing unary expressions
fn.unaryNode = function(input, sign) {
input = dynamicNode(input);
return dynamicAddSwizzleTrap(new UnaryExpressionNode(input, sign));
};
function isShaderNode(node) {
return (node instanceof BaseNode);
}
function isIntType(node) {
return (isShaderNode(node) && (node.type === 'int'));
}
function isFloatType(node) {
return (isShaderNode(node) && (node.type === 'float'));
}
function isFloatNode(node) {
return (node instanceof FloatNode);
}
function isVectorType(node) {
return (isShaderNode(node) && (node.type === 'vec2'|| node.type === 'vec3' || node.type === 'vec4'));
}
function isBinaryExpressionNode(node) {
return (node instanceof BinaryExpressionNode);
}
function isVariableNode(node) {
return (node instanceof VariableNode || node instanceof ComponentNode);
}
function isConditionalNode(node) {
return (node instanceof ConditionalNode || node instanceof BranchNode)
}
function hasTemporaryVariable(node) {
return (node.temporaryVariable);
}
function isPrimitiveNode(node) {
return (node instanceof FloatNode || node instanceof IntNode || node instanceof VectorNode);
}
function isFunctionCallNode(node) {
return (node instanceof FunctionCallNode);
}
function isVectorNode(node) {
return (node instanceof VectorNode)
}
function isUnaryExpressionNode(node) {
return (node instanceof UnaryExpressionNode)
}
// Helper function to check if a type is a user defined struct or native type
function isGLSLNativeType(typeName) {
// Supported types for now
const glslNativeTypes = ['int', 'float', 'vec2', 'vec3', 'vec4', 'sampler2D'];
return glslNativeTypes.includes(typeName);
}
// Shader Generator
// This class is responsible for converting the nodes into an object containing GLSL code, to be used by p5.Shader.modify
class ShaderGenerator {
constructor(userCallback, originalShader, srcLocations) {
GLOBAL_SHADER = this;
this.userCallback = userCallback;
this.srcLocations = srcLocations;
this.cleanup = () => {};
this.generateHookOverrides(originalShader);
this.output = {
vertexDeclarations: new Set(),
fragmentDeclarations: new Set(),
uniforms: {},
};
this.uniformNodes = [];
this.resetGLSLContext();
this.isGenerating = false;
}
generate() {
const prevFESDisabled = p5.disableFriendlyErrors;
// We need a custom error handling system within shader generation
p5.disableFriendlyErrors = true;
this.isGenerating = true;
this.userCallback();
this.output.vertexDeclarations = [...this.output.vertexDeclarations].join('\n');
this.output.fragmentDeclarations = [...this.output.fragmentDeclarations].join('\n');
this.isGenerating = false;
this.cleanup();
p5.disableFriendlyErrors = prevFESDisabled;
return this.output;
}
// This method generates the hook overrides which the user calls in their modify function.
generateHookOverrides(originalShader) {
const availableHooks = {
...originalShader.hooks.vertex,
...originalShader.hooks.fragment,
};
const windowOverrides = {};
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 argsArray = [];
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) => {
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)
);
}
const qualifiers = parameter.type.qualifiers.length > 0 ? parameter.type.qualifiers.join(' ') : '';
argsArray.push(`${qualifiers} ${parameter.type.typeName} ${parameter.name}`.trim());
});
let returnedValue = userCallback(...argNodes);
const expectedReturnType = hookTypes.returnType;
const toGLSLResults = {};
// 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 = dynamicNode(propertyNode);
toGLSLResults[propertyName] = propertyNode.toGLSLBase(this.context);
this.context.updateComponents(propertyNode);
});
} else {
if (!isShaderNode(returnedValue)) {
returnedValue = nodeConstructors[expectedReturnType.typeName](returnedValue);
} else if (isFloatType(returnedValue) && expectedReturnType.typeName.startsWith('vec')) {
returnedValue = nodeConstructors[expectedReturnType.typeName](returnedValue);
}
toGLSLResults['notAProperty'] = returnedValue.toGLSLBase(this.context);
this.context.updateComponents(returnedValue);
}
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);
});
// Build the final GLSL string.
// The order of this code is a bit confusing, we need to call toGLSLBase
let codeLines = [
`(${argsArray.join(', ')}) {`,
...this.context.declarations,
`\n ${hookTypes.returnType.typeName} finalReturnValue;`
];
Object.entries(toGLSLResults).forEach(([propertyName, result]) => {
const propString = expectedReturnType.properties ? `.${propertyName}` : '';
codeLines.push(` finalReturnValue${propString} = ${result};`);
});
this.context.declarations = [];
for (let key in this.context.varyings) {
const declArray = this.context.varyings[key];
const finalVaryingAssignments = [];
declArray.forEach(obj => {
const { node, value } = obj;
finalVaryingAssignments.push(` ${node.name} = ${value.toGLSLBase(this.context)};`);
finalVaryingAssignments.unshift(...this.context.declarations);
node.timesChanged = 0;
});
codeLines.push(...finalVaryingAssignments);
}
codeLines.push(' return finalReturnValue;', '}');
this.output[hookName] = codeLines.join('\n');
this.resetGLSLContext();
};
windowOverrides[hookTypes.name] = window[hookTypes.name];
// Expose the Functions to global scope for users to use
window[hookTypes.name] = function(userOverride) {
GLOBAL_SHADER[hookTypes.name](userOverride);
};
});
this.cleanup = () => {
for (const key in windowOverrides) {
window[key] = windowOverrides[key];
}
};
}
registerVarying(node, value) {
if (!Array.isArray(this.context.varyings[node.name])) {
this.context.varyings[node.name] = [];
}
this.context.varyings[node.name].push({ node, value });
this.output.vertexDeclarations.add(`OUT ${node.type} ${node.name};`);
this.output.fragmentDeclarations.add(`IN ${node.type} ${node.name};`);
}
resetGLSLContext() {
this.uniformNodes.forEach((node) => {
node.usedIn = [];
node.temporaryVariable = undefined;
});
this.context = {
id: 0,
getNextID() { return this.id++ },
declarations: [],
varyings: [],
ifs: [],
updateComponents: function(node, _emplaceAt, _changedComponents) {
if (node.componentsChanged) {
if (!_changedComponents) {
_changedComponents = node.componentNames.map(() => true);
}
const lines = [];
if (isVectorNode(node)) {
node.componentNames.forEach((name, i) => {
if (!_changedComponents[i]) return;
if (node[name] !== node.originalValues[i]) {
const replacement = nodeConstructors['float'](node[name]);
const line = ` ${node.temporaryVariable}.${name} = ${replacement.toGLSLBase(this)};`;
lines.push(line);
}
});
} else {
const components = node.componentNames.map((name) => {
return node[name]
});
const replacement = nodeConstructors[node.type](components);
const line = ` ${node.temporaryVariable} = ${replacement.toGLSLBase(this)};`;
lines.push(line);
}
if (_emplaceAt) {
this.declarations.splice(_emplaceAt, 0, ...lines);
} else {
this.declarations.push(...lines);
}
node.componentsChanged = false;
}
}
};
this.uniformNodes = [];
}
}
// User function helpers
function makeDependencyObject(dep) {
if (isVectorType(dep)) {
return {
node: dep,
isVector: true,
originalComponents: [...dep.componentNames.map(name => dep[name])],
get currentComponents() {
return dep.componentNames.map(name => dep[name]);
}
};
} else {
return {
node: dep,
isVector: false
};
}
}
function makeDependencyArray(dependencies) {
return dependencies.map(dep => makeDependencyObject(dep));
}
function conformVectorParameters(value, vectorDimensions) {
// Allow arguments as arrays or otherwise. The following are all equivalent:
// ([0,0,0,0]) (0,0,0,0) (0) ([0])
if (!Array.isArray(value)) {
value = [value];
}
value = value.flat();
value = value.map(val => {
if (isVectorType(val)) {
const componentArray = val.componentNames.map(comp => val[comp]);
return componentArray;
} else {
return val;
}
}).flat();
// Populate arguments so uniformVector3(0) becomes [0,0,0]
if (value.length === 1 && !isVectorNode(value[0])) {
value = Array(vectorDimensions).fill(value[0]);
}
return value;
}
function swizzleTrap(size) {
const swizzleSets = [
['x', 'y', 'z', 'w'],
['r', 'g', 'b', 'a'],
['s', 't', 'p', 'q']
].map(s => s.slice(0, size));
return {
get(target, property, receiver) {
if (property in target) {
return Reflect.get(...arguments);
} else {
for (const set of swizzleSets) {
if ([...property].every(char => set.includes(char))) {
if (property.length === 1) {
return target[swizzleSets[0][set.indexOf(property[0])]]
}
const components = [...property].map(char => {
const index = set.indexOf(char);
const mappedChar = swizzleSets[0][index];
return target[mappedChar];
});
const type = `vec${property.length}`;
return nodeConstructors[type](components);
}
}
}
},
set(target, property, value, receiver) {
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);
propertyCharArray.forEach((char, i) => {
const index = set.indexOf(char);
const realProperty = swizzleSets[0][index];
Reflect.set(target, realProperty, newValues[i], receiver);
});
return true;
}
}
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
const GLSLTypesToIdentifiers = {
int: 'Int',
float: 'Float',
vec2: 'Vector2',
vec3: 'Vector3',
vec4: 'Vector4',
sampler2D: 'Texture',
};
function dynamicAddSwizzleTrap(node, _size) {
if (node.type.startsWith('vec') || _size) {
const size = parseInt(node.type.slice(3));
node = new Proxy(node, swizzleTrap(size));
node.addVectorComponents();
}
return node;
}
function binaryExpressionNodeConstructor(a, b, operator, isInternal) {
let node;
if (operator === '%') {
node = new ModulusNode(a, b);
} else {
node = new BinaryExpressionNode(a, b, operator, isInternal);
}
return dynamicAddSwizzleTrap(node);
}
function variableConstructor(name, type, isInternal) {
const node = new VariableNode(name, type, isInternal);
return dynamicAddSwizzleTrap(node);
}
function fnNodeConstructor(name, userArgs, properties, isInternal) {
let node = new FunctionCallNode(name, userArgs, properties, isInternal);
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;
}
});
return conditionals;
}).flat();
dependsOnConditionals.forEach(conditional => conditional.usedIn.push(node));
return node;
}
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')),
dynamicVector: function(value) {
const size = computeVectorLength(value);
return this[`vec${size}`](value);
},
};
for (const glslType in GLSLTypesToIdentifiers) {
// Generate uniform*() Methods for creating uniforms
const typeIdentifier = GLSLTypesToIdentifiers[glslType];
const uniformMethodName = `uniform${typeIdentifier}`;
ShaderGenerator.prototype[uniformMethodName] = function(...args) {
let [name, ...defaultValue] = args;
if (glslType.startsWith('vec') && !(defaultValue[0] instanceof Function)) {
defaultValue = conformVectorParameters(defaultValue, parseInt(glslType.slice(3)));
this.output.uniforms[`${glslType} ${name}`] = defaultValue;
}
else {
this.output.uniforms[`${glslType} ${name}`] = defaultValue[0];
}
const uniform = variableConstructor(name, glslType, false);
this.uniformNodes.push(uniform);
return uniform;
};
fn[uniformMethodName] = function (...args) {
return GLOBAL_SHADER[uniformMethodName](...args);
};
// We don't need a texture creation method.
if (glslType === 'sampler2D') { continue; }
const varyingMethodName = `varying${typeIdentifier}`;
ShaderGenerator.prototype[varyingMethodName] = function(name) {
return dynamicAd