diff --git a/preview/global/sketch.js b/preview/global/sketch.js index b0cd6c8045..990b750d27 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,14 +1,8 @@ -p5.disableFriendlyErrors = true; - -function windowResized() { - resizeCanvas(windowWidth, windowHeight); -} - -let starShader; -let starStrokeShader; +let instancedShader; +let instancedStrokeShader; let stars; -let originalFrameBuffer; -let pixellizeShader; +let originalImage; +let pixelateShader; let fresnelShader; let bloomShader; @@ -16,6 +10,7 @@ function fresnelShaderCallback() { const fresnelPower = uniformFloat(2); const fresnelBias = uniformFloat(-0.1); const fresnelScale = uniformFloat(2); + getCameraInputs((inputs) => { let n = normalize(inputs.normal); let v = normalize(-inputs.position); @@ -29,7 +24,7 @@ function fresnelShaderCallback() { function starShaderCallback() { const time = uniformFloat(() => millis()); - const skyRadius = uniformFloat(1000); + const skyRadius = uniformFloat(250); function rand2(st) { return fract(sin(dot(st, [12.9898, 78.233])) * 43758.5453123); @@ -37,10 +32,11 @@ function starShaderCallback() { function semiSphere() { let id = instanceID(); - let theta = rand2([id, 0.1234]) * TWO_PI; - let phi = rand2([id, 3.321]) * PI+time/10000; + let theta = rand2([id, 0.1234]) * TWO_PI + time / 100000; + let phi = rand2([id, 3.321]) * PI + time / 50000; + let r = skyRadius; - r *= 1.5 * sin(phi); + r *= sin(phi); let x = r * sin(phi) * cos(theta); let y = r * 1.5 * cos(phi); let z = r * sin(phi) * sin(theta); @@ -53,72 +49,80 @@ function starShaderCallback() { }); getObjectInputs((inputs) => { - let scale = 1 + 0.1 * sin(time * 0.002 + instanceID()); - inputs.position *= scale; + let size = 1 + 0.5 * sin(time * 0.002 + instanceID()); + inputs.position *= size; return inputs; }); } -function pixellizeShaderCallback() { - const pixelSize = uniformFloat(()=> width*.75); - getColor((input, canvasContent) => { - let coord = input.texCoord; +function pixelateShaderCallback() { + const pixelCountX = uniformFloat(()=> 280); + + getColor((inputs, canvasContent) => { + const aspectRatio = inputs.canvasSize.x / inputs.canvasSize.y; + const pixelSize = [pixelCountX, pixelCountX / aspectRatio]; + + let coord = inputs.texCoord; coord = floor(coord * pixelSize) / pixelSize; - let col = texture(canvasContent, coord); - return col; + + let col = getTexture(canvasContent, coord); + return col//[coord, 0, 1]; }); } function bloomShaderCallback() { - const preBlur = uniformTexture(() => originalFrameBuffer); + const preBlur = uniformTexture(() => originalImage); + getColor((input, canvasContent) => { - const blurredCol = texture(canvasContent, input.texCoord); - const originalCol = texture(preBlur, input.texCoord); - const brightPass = max(originalCol, 0.3) * 1.5; - const bloom = originalCol + blurredCol * brightPass; - return bloom; + const blurredCol = getTexture(canvasContent, input.texCoord); + const originalCol = getTexture(preBlur, input.texCoord); + + const intensity = max(originalCol, 0.1) * 12.2; + const bloom = originalCol + blurredCol * intensity; + return [bloom.rgb, 1]; }); } async function setup(){ - createCanvas(windowWidth, windowHeight, WEBGL); - stars = buildGeometry(() => sphere(30, 4, 2)) - originalFrameBuffer = createFramebuffer(); + createCanvas(800, 600, WEBGL); + pixelDensity(1); + stars = buildGeometry(() => sphere(8, 4, 2)) + originalImage = createFramebuffer(); starShader = baseMaterialShader().modify(starShaderCallback); starStrokeShader = baseStrokeShader().modify(starShaderCallback) fresnelShader = baseColorShader().modify(fresnelShaderCallback); bloomShader = baseFilterShader().modify(bloomShaderCallback); - pixellizeShader = baseFilterShader().modify(pixellizeShaderCallback); + pixelateShader = baseFilterShader().modify(pixelateShaderCallback); } function draw(){ - originalFrameBuffer.begin(); + originalImage.begin(); background(0); orbitControl(); push() - strokeWeight(4) + strokeWeight(2) stroke(255,0,0) rotateX(PI/2 + millis() * 0.0005); fill(255,100, 150) strokeShader(starStrokeShader) shader(starShader); - model(stars, 2000); + model(stars, 1000); pop() push() shader(fresnelShader) noStroke() - sphere(500); + sphere(90); + filter(pixelateShader); pop() - filter(pixellizeShader); - originalFrameBuffer.end(); + originalImage.end(); imageMode(CENTER) - image(originalFrameBuffer, 0, 0) + image(originalImage, 0, 0) - filter(BLUR, 20) + filter(BLUR, 15) filter(bloomShader); -} +} \ No newline at end of file diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js new file mode 100644 index 0000000000..dcecc7d0f9 --- /dev/null +++ b/src/strands/ir_builders.js @@ -0,0 +1,554 @@ +import * as DAG from './ir_dag' +import * as CFG from './ir_cfg' +import * as FES from './strands_FES' +import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, typeEquals, } from './ir_types'; +import { createStrandsNode, StrandsNode } from './strands_api'; +import { strandsBuiltinFunctions } from './strands_builtins'; + +////////////////////////////////////////////// +// Builders for node graphs +////////////////////////////////////////////// +export function scalarLiteralNode(strandsContext, typeInfo, value) { + const { cfg, dag } = strandsContext + let { dimension, baseType } = typeInfo; + if (dimension !== 1) { + FES.internalError('Created a scalar literal node with dimension > 1.') + } + const nodeData = DAG.createNodeData({ + nodeType: NodeType.LITERAL, + dimension, + baseType, + value + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, dimension }; +} + +export function variableNode(strandsContext, typeInfo, identifier) { + const { cfg, dag } = strandsContext; + const { dimension, baseType } = typeInfo; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + dimension, + baseType, + identifier + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, dimension }; +} + +export function unaryOpNode(strandsContext, nodeOrValue, opCode) { + const { dag, cfg } = strandsContext; + let dependsOn; + let node; + if (nodeOrValue instanceof StrandsNode) { + node = nodeOrValue; + } else { + const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, nodeOrValue); + node = createStrandsNode(id, dimension, strandsContext); + } + dependsOn = [node.id]; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode, + dependsOn, + baseType: dag.baseTypes[node.id], + dimension: node.dimension + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, dimension: node.dimension }; +} + +export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { + const { dag, cfg } = strandsContext; + // Construct a node for right if its just an array or number etc. + let rightStrandsNode; + if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) { + rightStrandsNode = rightArg[0]; + } else { + const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, rightArg); + rightStrandsNode = createStrandsNode(id, dimension, strandsContext); + } + let finalLeftNodeID = leftStrandsNode.id; + let finalRightNodeID = rightStrandsNode.id; + + // Check if we have to cast either node + const leftType = DAG.extractNodeTypeInfo(dag, leftStrandsNode.id); + const rightType = DAG.extractNodeTypeInfo(dag, rightStrandsNode.id); + const cast = { node: null, toType: leftType }; + const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; + if (bothDeferred) { + cast.toType.baseType = BaseType.FLOAT; + if (leftType.dimension === rightType.dimension) { + cast.toType.dimension = leftType.dimension; + } + else if (leftType.dimension === 1 && rightType.dimension > 1) { + cast.toType.dimension = rightType.dimension; + } + else if (rightType.dimension === 1 && leftType.dimension > 1) { + cast.toType.dimension = leftType.dimension; + } + else { + FES.userError("type error", `You have tried to perform a binary operation:\n`+ + `${leftType.baseType+leftType.dimension} ${OpCodeToSymbol[opCode]} ${rightType.baseType+rightType.dimension}\n` + + `It's only possible to operate on two nodes with the same dimension, or a scalar value and a vector.` + ); + } + const l = primitiveConstructorNode(strandsContext, cast.toType, leftStrandsNode); + const r = primitiveConstructorNode(strandsContext, cast.toType, rightStrandsNode); + finalLeftNodeID = l.id; + finalRightNodeID = r.id; + } + else if (leftType.baseType !== rightType.baseType || + leftType.dimension !== rightType.dimension) { + + if (leftType.dimension === 1 && rightType.dimension > 1) { + cast.node = leftStrandsNode; + cast.toType = rightType; + } + else if (rightType.dimension === 1 && leftType.dimension > 1) { + cast.node = rightStrandsNode; + cast.toType = leftType; + } + else if (leftType.priority > rightType.priority) { + // e.g. op(float vector, int vector): cast priority is float > int > bool + cast.node = rightStrandsNode; + cast.toType = leftType; + } + else if (rightType.priority > leftType.priority) { + cast.node = leftStrandsNode; + cast.toType = rightType; + } + else { + FES.userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); + } + + const casted = primitiveConstructorNode(strandsContext, cast.toType, cast.node); + + if (cast.node === leftStrandsNode) { + leftStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext); + finalLeftNodeID = leftStrandsNode.id; + } else { + rightStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext); + finalRightNodeID = rightStrandsNode.id; + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode, + dependsOn: [finalLeftNodeID, finalRightNodeID], + baseType: cast.toType.baseType, + dimension: cast.toType.dimension, + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, dimension: nodeData.dimension }; +} + +export function memberAccessNode(strandsContext, parentNode, componentNode, memberTypeInfo) { + const { dag, cfg } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Binary.MEMBER_ACCESS, + dimension: memberTypeInfo.dimension, + baseType: memberTypeInfo.baseType, + dependsOn: [parentNode.id, componentNode.id], + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, dimension: memberTypeInfo.dimension }; +} + +export function structInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { + const { cfg, dag, } = strandsContext; + + if (dependsOn.length === 0) { + for (const prop of structTypeInfo.properties) { + const typeInfo = prop.dataType; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + baseType: typeInfo.baseType, + dimension: typeInfo.dimension, + identifier: `${identifier}.${prop.name}`, + }); + const componentID = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, componentID); + dependsOn.push(componentID); + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.VARIABLE, + dimension: structTypeInfo.properties.length, + baseType: structTypeInfo.typeName, + identifier, + dependsOn + }) + const structID = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, structID); + + return { id: structID, dimension: 0, components: dependsOn }; +} + +function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { + const inputs = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; + const mappedDependencies = []; + let { dimension, baseType } = typeInfo; + + const dag = strandsContext.dag; + let calculatedDimensions = 0; + let originalNodeID = null; + for (const dep of inputs.flat(Infinity)) { + if (dep instanceof StrandsNode) { + const node = DAG.getNodeDataFromID(dag, dep.id); + originalNodeID = dep.id; + baseType = node.baseType; + + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { + for (const inner of node.dependsOn) { + mappedDependencies.push(inner); + } + } else { + mappedDependencies.push(dep.id); + } + + calculatedDimensions += node.dimension; + continue; + } + else if (typeof dep === 'number') { + const { id, dimension } = scalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + mappedDependencies.push(id); + calculatedDimensions += dimension; + continue; + } + else { + FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); + } + } + if (dimension === null) { + dimension = calculatedDimensions; + } else if (dimension > calculatedDimensions && calculatedDimensions === 1) { + calculatedDimensions = dimension; + } else if(calculatedDimensions !== 1 && calculatedDimensions !== dimension) { + FES.userError('type error', `You've tried to construct a ${baseType + dimension} with ${calculatedDimensions} components`); + } + const inferredTypeInfo = { + dimension, + baseType, + priority: BasePriority[baseType], + } + return { originalNodeID, mappedDependencies, inferredTypeInfo }; +} + +export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray) { + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.CONSTRUCTOR, + dimension: typeInfo.dimension, + baseType: typeInfo.baseType, + dependsOn: strandsNodesArray + }); + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + return id; +} + +export function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) { + const cfg = strandsContext.cfg; + const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn); + + const finalType = { + baseType: typeInfo.baseType, + dimension: inferredTypeInfo.dimension + }; + + const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); + + if (typeInfo.baseType !== BaseType.DEFER) { + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + } + + return { id, dimension: finalType.dimension, components: mappedDependencies }; +} + +export function structConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { + const { cfg, dag } = strandsContext; + const { identifer, properties } = structTypeInfo; + + if (!(rawUserArgs.length === properties.length)) { + FES.userError('type error', + `You've tried to construct a ${structTypeInfo.typeName} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` + + `The properties it expects are:\n` + + `${properties.map(prop => prop.name + ' ' + prop.DataType.baseType + prop.DataType.dimension)}` + ); + } + + const dependsOn = []; + for (let i = 0; i < properties.length; i++) { + const expectedProperty = properties[i]; + const { originalNodeID, mappedDependencies } = mapPrimitiveDepsToIDs(strandsContext, expectedProperty.dataType, rawUserArgs[i]); + if (originalNodeID) { + dependsOn.push(originalNodeID); + } + else { + dependsOn.push( + constructTypeFromIDs(strandsContext, expectedProperty.dataType, mappedDependencies) + ); + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.CONSTRUCTOR, + dimension: properties.length, + baseType: structTypeInfo.typeName , + dependsOn + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, dimension: properties.length, components: structTypeInfo.components }; +} + +export function functionCallNode( + strandsContext, + functionName, + rawUserArgs, + { overloads: rawOverloads } = {}, +) { + const { cfg, dag } = strandsContext; + const overloads = rawOverloads || strandsBuiltinFunctions[functionName]; + + const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDepsToIDs(strandsContext, DataType.defer, rawUserArg)); + const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); + if (matchingArgsCounts.length === 0) { + const argsLengthSet = new Set(); + const argsLengthArr = []; + overloads.forEach((overload) => argsLengthSet.add(overload.params.length)); + argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); + const argsLengthStr = argsLengthArr.join(', or '); + FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`); + } + + const isGeneric = (T) => T.dimension === null; + let bestOverload = null; + let bestScore = 0; + let inferredReturnType = null; + let inferredDimension = null; + + for (const overload of matchingArgsCounts) { + let isValid = true; + let similarity = 0; + + for (let i = 0; i < preprocessedArgs.length; i++) { + const preArg = preprocessedArgs[i]; + const argType = preArg.inferredTypeInfo; + const expectedType = overload.params[i]; + let dimension = expectedType.dimension; + + if (isGeneric(expectedType)) { + if (inferredDimension === null || inferredDimension === 1) { + inferredDimension = argType.dimension; + } + + if (inferredDimension !== argType.dimension && + !(argType.dimension === 1 && inferredDimension >= 1) + ) { + isValid = false; + } + dimension = inferredDimension; + } + else { + if (argType.dimension > dimension) { + isValid = false; + } + } + + if (argType.baseType === expectedType.baseType) { + similarity += 2; + } + else if(expectedType.priority > argType.priority) { + similarity += 1; + } + + } + + if (isValid && (!bestOverload || similarity > bestScore)) { + bestOverload = overload; + bestScore = similarity; + inferredReturnType = {...overload.returnType }; + if (isGeneric(inferredReturnType)) { + inferredReturnType.dimension = inferredDimension; + } + } + } + + if (bestOverload === null) { + FES.userError('parameter validation', `No matching overload for ${functionName} was found!`); + } + + let dependsOn = []; + for (let i = 0; i < bestOverload.params.length; i++) { + const arg = preprocessedArgs[i]; + const paramType = { ...bestOverload.params[i] }; + if (isGeneric(paramType)) { + paramType.dimension = inferredDimension; + } + if (arg.originalNodeID && typeEquals(arg.inferredTypeInfo, paramType)) { + dependsOn.push(arg.originalNodeID); + } + else { + const castedArgID = constructTypeFromIDs(strandsContext, paramType, arg.mappedDependencies); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, castedArgID); + dependsOn.push(castedArgID); + } + } + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.FUNCTION_CALL, + identifier: functionName, + dependsOn, + baseType: inferredReturnType.baseType, + dimension: inferredReturnType.dimension + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, dimension: inferredReturnType.dimension }; +} + +export function statementNode(strandsContext, opCode) { + const { dag, cfg } = strandsContext; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + opCode + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return id; +} + +export function swizzleNode(strandsContext, parentNode, swizzle) { + const { dag, cfg } = strandsContext; + const baseType = dag.baseTypes[parentNode.id]; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + baseType, + dimension: swizzle.length, + opCode: OpCode.Unary.SWIZZLE, + dependsOn: [parentNode.id], + swizzle, + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, dimension: swizzle.length }; +} + +export function swizzleTrap(id, dimension, strandsContext, onRebind) { + const swizzleSets = [ + ['x', 'y', 'z', 'w'], + ['r', 'g', 'b', 'a'], + ['s', 't', 'p', 'q'] + ].map(s => s.slice(0, dimension)); + const trap = { + 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))) { + const swizzle = [...property].map(char => { + const index = set.indexOf(char); + return swizzleSets[0][index]; + }).join(''); + const node = swizzleNode(strandsContext, target, swizzle); + return createStrandsNode(node.id, node.dimension, strandsContext); + } + } + } + }, + set(target, property, value, receiver) { + for (const swizzleSet of swizzleSets) { + const chars = [...property]; + const valid = + chars.every(c => swizzleSet.includes(c)) && + new Set(chars).size === chars.length && + target.dimension >= chars.length; + + if (!valid) continue; + + const dim = target.dimension; + + // lanes are the underlying values of the target vector + // e.g. lane 0 holds the value aliased by 'x', 'r', and 's' + // the lanes array is in the 'correct' order + const lanes = new Array(dim); + for (let i = 0; i < dim; i++) { + const { id, dimension } = swizzleNode(strandsContext, target, 'xyzw'[i]); + lanes[i] = createStrandsNode(id, dimension, strandsContext); + } + + // The scalars array contains the individual components of the users values. + // This may not be the most efficient way, as we swizzle each component individually, + // so that .xyz becomes .x, .y, .z + let scalars = []; + if (value instanceof StrandsNode) { + if (value.dimension === 1) { + scalars = Array(chars.length).fill(value); + } else if (value.dimension === chars.length) { + for (let k = 0; k < chars.length; k++) { + const { id, dimension } = swizzleNode(strandsContext, value, 'xyzw'[k]); + scalars.push(createStrandsNode(id, dimension, strandsContext)); + } + } else { + FES.userError('type error', `Swizzle assignment: RHS vector does not match LHS vector (need ${chars.length}, got ${value.dimension}).`); + } + } else if (Array.isArray(value)) { + const flat = value.flat(Infinity); + if (flat.length === 1) { + scalars = Array(chars.length).fill(flat[0]); + } else if (flat.length === chars.length) { + scalars = flat; + } else { + FES.userError('type error', `Swizzle assignment: RHS length ${flat.length} does not match ${chars.length}.`); + } + } else if (typeof value === 'number') { + scalars = Array(chars.length).fill(value); + } else { + FES.userError('type error', `Unsupported RHS for swizzle assignment: ${value}`); + } + + // The canonical index refers to the actual value's position in the vector lanes + // i.e. we are finding (3,2,1) from .zyx + // We set the correct value in the lanes array + for (let j = 0; j < chars.length; j++) { + const canonicalIndex = swizzleSet.indexOf(chars[j]); + lanes[canonicalIndex] = scalars[j]; + } + + const orig = DAG.getNodeDataFromID(strandsContext.dag, target.id); + const baseType = orig?.baseType ?? BaseType.FLOAT; + const { id: newID } = primitiveConstructorNode( + strandsContext, + { baseType, dimension: dim }, + lanes + ); + + target.id = newID; + + // If we swizzle assign on a struct component i.e. + // inputs.position.rg = [1, 2] + // The onRebind callback will update the structs components so that it refers to the new values, + // and make a new ID for the struct with these new values + if (typeof onRebind === 'function') { + onRebind(newID); + } + return true; + } + return Reflect.set(...arguments); + } + }; + return trap; +} diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js new file mode 100644 index 0000000000..7bdcf09382 --- /dev/null +++ b/src/strands/ir_cfg.js @@ -0,0 +1,92 @@ +import { BlockTypeToName } from "./ir_types"; +import * as FES from './strands_FES' + +// Todo: remove edges to simplify. Block order is always ordered already. + +export function createControlFlowGraph() { + return { + // graph structure + blockTypes: [], + incomingEdges: [], + outgoingEdges: [], + blockInstructions: [], + // runtime data for constructing graph + nextID: 0, + blockStack: [], + blockOrder: [], + blockConditions: {}, + currentBlock: -1, + }; +} + +export function pushBlock(graph, blockID) { + graph.blockStack.push(blockID); + graph.blockOrder.push(blockID); + graph.currentBlock = blockID; +} + +export function popBlock(graph) { + graph.blockStack.pop(); + const len = graph.blockStack.length; + graph.currentBlock = graph.blockStack[len-1]; +} + +export function createBasicBlock(graph, blockType) { + const id = graph.nextID++; + graph.blockTypes[id] = blockType; + graph.incomingEdges[id] = []; + graph.outgoingEdges[id] = []; + graph.blockInstructions[id]= []; + return id; +} + +export function addEdge(graph, from, to) { + graph.outgoingEdges[from].push(to); + graph.incomingEdges[to].push(from); +} + +export function recordInBasicBlock(graph, blockID, nodeID) { + if (nodeID === undefined) { + FES.internalError('undefined nodeID in `recordInBasicBlock()`'); + } + if (blockID === undefined) { + FES.internalError('undefined blockID in `recordInBasicBlock()'); + } + graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || []; + graph.blockInstructions[blockID].push(nodeID); +} + +export function getBlockDataFromID(graph, id) { + return { + id, + blockType: graph.blockTypes[id], + incomingEdges: graph.incomingEdges[id], + outgoingEdges: graph.outgoingEdges[id], + blockInstructions: graph.blockInstructions[id], + } +} + +export function printBlockData(graph, id) { + const block = getBlockDataFromID(graph, id); + block.blockType = BlockTypeToName[block.blockType]; + console.log(block); +} + +export function sortCFG(adjacencyList, start) { + const visited = new Set(); + const postOrder = []; + + function dfs(v) { + if (visited.has(v)) { + return; + } + visited.add(v); + for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { + dfs(w); + } + postOrder.push(v); + } + + dfs(start); + return postOrder.reverse(); +} \ No newline at end of file diff --git a/src/strands/ir_dag.js b/src/strands/ir_dag.js new file mode 100644 index 0000000000..7633d534c6 --- /dev/null +++ b/src/strands/ir_dag.js @@ -0,0 +1,130 @@ +import { NodeTypeRequiredFields, NodeTypeToName, BasePriority, StatementType } from './ir_types'; +import * as FES from './strands_FES'; + +///////////////////////////////// +// Public functions for strands runtime +///////////////////////////////// + +export function createDirectedAcyclicGraph() { + const graph = { + nextID: 0, + cache: new Map(), + nodeTypes: [], + baseTypes: [], + dimensions: [], + opCodes: [], + values: [], + identifiers: [], + phiBlocks: [], + dependsOn: [], + usedBy: [], + statementTypes: [], + swizzles: [], + }; + + return graph; +} + +export function getOrCreateNode(graph, node) { + // const key = getNodeKey(node); + // const existing = graph.cache.get(key); + + // if (existing !== undefined) { + // return existing; + // } else { + const id = createNode(graph, node); + // graph.cache.set(key, id); + return id; + // } +} + +export function createNodeData(data = {}) { + const node = { + nodeType: data.nodeType ?? null, + baseType: data.baseType ?? null, + dimension: data.dimension ?? null, + opCode: data.opCode ?? null, + value: data.value ?? null, + identifier: data.identifier ?? null, + statementType: data.statementType ?? null, + swizzle: data.swizzle ?? null, + dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], + usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], + phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], + }; + validateNode(node); + return node; +} + +export function getNodeDataFromID(graph, id) { + return { + id, + nodeType: graph.nodeTypes[id], + opCode: graph.opCodes[id], + value: graph.values[id], + identifier: graph.identifiers[id], + dependsOn: graph.dependsOn[id], + usedBy: graph.usedBy[id], + phiBlocks: graph.phiBlocks[id], + dimension: graph.dimensions[id], + baseType: graph.baseTypes[id], + statementType: graph.statementTypes[id], + swizzle: graph.swizzles[id], + } +} + +export function extractNodeTypeInfo(dag, nodeID) { + return { + baseType: dag.baseTypes[nodeID], + dimension: dag.dimensions[nodeID], + priority: BasePriority[dag.baseTypes[nodeID]], + }; +} + +///////////////////////////////// +// Private functions +///////////////////////////////// +function createNode(graph, node) { + const id = graph.nextID++; + graph.nodeTypes[id] = node.nodeType; + graph.opCodes[id] = node.opCode; + graph.values[id] = node.value; + graph.identifiers[id] = node.identifier; + graph.dependsOn[id] = node.dependsOn.slice(); + graph.usedBy[id] = node.usedBy; + graph.phiBlocks[id] = node.phiBlocks.slice(); + graph.baseTypes[id] = node.baseType + graph.dimensions[id] = node.dimension; + graph.statementTypes[id] = node.statementType; + graph.swizzles[id] = node.swizzle; + + for (const dep of node.dependsOn) { + if (!Array.isArray(graph.usedBy[dep])) { + graph.usedBy[dep] = []; + } + graph.usedBy[dep].push(id); + } + return id; +} + +function getNodeKey(node) { + const key = JSON.stringify(node); + return key; +} + +function validateNode(node){ + const nodeType = node.nodeType; + const requiredFields = NodeTypeRequiredFields[nodeType]; + if (requiredFields.length === 2) { + FES.internalError(`Required fields for node type '${NodeTypeToName[nodeType]}' not defined. Please add them to the utils.js file in p5.strands!`) + } + const missingFields = []; + for (const field of requiredFields) { + if (node[field] === null) { + missingFields.push(field); + } + } + if (missingFields.length > 0) { + FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); + } +} \ No newline at end of file diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js new file mode 100644 index 0000000000..67829d6b03 --- /dev/null +++ b/src/strands/ir_types.js @@ -0,0 +1,211 @@ +///////////////////// +// Enums for nodes // +///////////////////// +export const NodeType = { + OPERATION: 'operation', + LITERAL: 'literal', + VARIABLE: 'variable', + CONSTANT: 'constant', + STRUCT: 'struct', + PHI: 'phi', + STATEMENT: 'statement', +}; + +export const NodeTypeToName = Object.fromEntries( + Object.entries(NodeType).map(([key, val]) => [val, key]) +); + +export const NodeTypeRequiredFields = { + [NodeType.OPERATION]: ["opCode", "dependsOn", "dimension", "baseType"], + [NodeType.LITERAL]: ["value", "dimension", "baseType"], + [NodeType.VARIABLE]: ["identifier", "dimension", "baseType"], + [NodeType.CONSTANT]: ["value", "dimension", "baseType"], + [NodeType.STRUCT]: [""], + [NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"], + [NodeType.STATEMENT]: ["opCode"] +}; + +export const StatementType = { + DISCARD: 'discard', +}; + +export const BaseType = { + FLOAT: "float", + INT: "int", + BOOL: "bool", + MAT: "mat", + DEFER: "defer", + SAMPLER2D: "sampler2D", +}; + +export const BasePriority = { + [BaseType.FLOAT]: 3, + [BaseType.INT]: 2, + [BaseType.BOOL]: 1, + [BaseType.MAT]: 0, + [BaseType.DEFER]: -1, + [BaseType.SAMPLER2D]: -10, +}; + +export const DataType = { + float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, }, + float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, }, + float3: { fnName: "vec3", baseType: BaseType.FLOAT, dimension:3, priority: 3, }, + float4: { fnName: "vec4", baseType: BaseType.FLOAT, dimension:4, priority: 3, }, + int1: { fnName: "int", baseType: BaseType.INT, dimension:1, priority: 2, }, + int2: { fnName: "ivec2", baseType: BaseType.INT, dimension:2, priority: 2, }, + int3: { fnName: "ivec3", baseType: BaseType.INT, dimension:3, priority: 2, }, + int4: { fnName: "ivec4", baseType: BaseType.INT, dimension:4, priority: 2, }, + bool1: { fnName: "bool", baseType: BaseType.BOOL, dimension:1, priority: 1, }, + bool2: { fnName: "bvec2", baseType: BaseType.BOOL, dimension:2, priority: 1, }, + bool3: { fnName: "bvec3", baseType: BaseType.BOOL, dimension:3, priority: 1, }, + bool4: { fnName: "bvec4", baseType: BaseType.BOOL, dimension:4, priority: 1, }, + mat2: { fnName: "mat2x2", baseType: BaseType.MAT, dimension:2, priority: 0, }, + mat3: { fnName: "mat3x3", baseType: BaseType.MAT, dimension:3, priority: 0, }, + mat4: { fnName: "mat4x4", baseType: BaseType.MAT, dimension:4, priority: 0, }, + defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, + sampler2D: { fnName: "texture", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 }, +} +export const structType = function (hookType) { + let T = hookType.type === undefined ? hookType : hookType.type; + const structType = { + name: hookType.name, + properties: [], + typeName: T.typeName, + }; + // TODO: handle struct properties that are themselves structs + for (const prop of T.properties) { + const propType = TypeInfoFromGLSLName[prop.type.typeName]; + structType.properties.push( + {name: prop.name, dataType: propType } + ); + } + return structType; +}; + +export function isStructType(typeName) { + return !isNativeType(typeName); +} + +export function isNativeType(typeName) { + return Object.keys(DataType).includes(typeName); +} + +export const GenType = { + FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 }, + INT: { baseType: BaseType.INT, dimension: null, priority: 2 }, + BOOL: { baseType: BaseType.BOOL, dimension: null, priority: 1 }, +} + +export function typeEquals(nodeA, nodeB) { + return (nodeA.dimension === nodeB.dimension) && (nodeA.baseType === nodeB.baseType); +} + +export const TypeInfoFromGLSLName = Object.fromEntries( + Object.values(DataType) + .filter(info => info.fnName !== null) + .map(info => [info.fnName === 'texture' ? 'sampler2D' : info.fnName, info]) +); + +export const OpCode = { + Binary: { + ADD: 0, + SUBTRACT: 1, + MULTIPLY: 2, + DIVIDE: 3, + MODULO: 4, + EQUAL: 5, + NOT_EQUAL: 6, + GREATER_THAN: 7, + GREATER_EQUAL: 8, + LESS_THAN: 9, + LESS_EQUAL: 10, + LOGICAL_AND: 11, + LOGICAL_OR: 12, + MEMBER_ACCESS: 13, + }, + Unary: { + LOGICAL_NOT: 100, + NEGATE: 101, + PLUS: 102, + SWIZZLE: 103, + }, + Nary: { + FUNCTION_CALL: 200, + CONSTRUCTOR: 201, + }, + ControlFlow: { + RETURN: 300, + JUMP: 301, + BRANCH_IF_FALSE: 302, + DISCARD: 303, + } +}; + +export const OperatorTable = [ + { arity: "unary", name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT }, + { arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE }, + { arity: "unary", name: "plus", symbol: "+", opCode: OpCode.Unary.PLUS }, + { arity: "binary", name: "add", symbol: "+", opCode: OpCode.Binary.ADD }, + { arity: "binary", name: "sub", symbol: "-", opCode: OpCode.Binary.SUBTRACT }, + { arity: "binary", name: "mult", symbol: "*", opCode: OpCode.Binary.MULTIPLY }, + { arity: "binary", name: "div", symbol: "/", opCode: OpCode.Binary.DIVIDE }, + { arity: "binary", name: "mod", symbol: "%", opCode: OpCode.Binary.MODULO }, + { arity: "binary", name: "equalTo", symbol: "==", opCode: OpCode.Binary.EQUAL }, + { arity: "binary", name: "notEqual", symbol: "!=", opCode: OpCode.Binary.NOT_EQUAL }, + { arity: "binary", name: "greaterThan", symbol: ">", opCode: OpCode.Binary.GREATER_THAN }, + { arity: "binary", name: "greaterEqual", symbol: ">=", opCode: OpCode.Binary.GREATER_EQUAL }, + { arity: "binary", name: "lessThan", symbol: "<", opCode: OpCode.Binary.LESS_THAN }, + { arity: "binary", name: "lessEqual", symbol: "<=", opCode: OpCode.Binary.LESS_EQUAL }, + { arity: "binary", name: "and", symbol: "&&", opCode: OpCode.Binary.LOGICAL_AND }, + { arity: "binary", name: "or", symbol: "||", opCode: OpCode.Binary.LOGICAL_OR }, +]; + +export const ConstantFolding = { + [OpCode.Binary.ADD]: (a, b) => a + b, + [OpCode.Binary.SUBTRACT]: (a, b) => a - b, + [OpCode.Binary.MULTIPLY]: (a, b) => a * b, + [OpCode.Binary.DIVIDE]: (a, b) => a / b, + [OpCode.Binary.MODULO]: (a, b) => a % b, + [OpCode.Binary.EQUAL]: (a, b) => a == b, + [OpCode.Binary.NOT_EQUAL]: (a, b) => a != b, + [OpCode.Binary.GREATER_THAN]: (a, b) => a > b, + [OpCode.Binary.GREATER_EQUAL]: (a, b) => a >= b, + [OpCode.Binary.LESS_THAN]: (a, b) => a < b, + [OpCode.Binary.LESS_EQUAL]: (a, b) => a <= b, + [OpCode.Binary.LOGICAL_AND]: (a, b) => a && b, + [OpCode.Binary.LOGICAL_OR]: (a, b) => a || b, +}; + +// export const SymbolToOpCode = {}; +export const OpCodeToSymbol = {}; +export const UnarySymbolToName = {}; +export const BinarySymbolToName = {}; + +for (const { symbol, opCode, name, arity } of OperatorTable) { + // SymbolToOpCode[symbol] = opCode; + OpCodeToSymbol[opCode] = symbol; + if (arity === 'unary') { + UnarySymbolToName[symbol] = name; + } + if (arity === 'binary') { + BinarySymbolToName[symbol] = name; + } +} + +export const BlockType = { + GLOBAL: 'global', + FUNCTION: 'function', + IF_COND: 'if_cond', + IF_BODY: 'if_body', + ELIF_BODY: 'elif_body', + ELIF_COND: 'elif_cond', + ELSE_BODY: 'else_body', + FOR: 'for', + MERGE: 'merge', + DEFAULT: 'default', +} + +export const BlockTypeToName = Object.fromEntries( + Object.entries(BlockType).map(([key, val]) => [val, key]) +); diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js new file mode 100644 index 0000000000..384b6068d7 --- /dev/null +++ b/src/strands/p5.strands.js @@ -0,0 +1,614 @@ +/** +* @module 3D +* @submodule strands +* @for p5 +* @requires core +*/ +import { glslBackend } from './strands_glslBackend'; + +import { transpileStrandsToJS } from './strands_transpiler'; +import { BlockType } from './ir_types'; + +import { createDirectedAcyclicGraph } from './ir_dag' +import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './ir_cfg'; +import { generateShaderCode } from './strands_codegen'; +import { initGlobalStrandsAPI, createShaderHooksFunctions } from './strands_api'; + +function strands(p5, fn) { + ////////////////////////////////////////////// + // Global Runtime + ////////////////////////////////////////////// + function initStrandsContext(ctx, backend, { active = false } = {}) { + ctx.dag = createDirectedAcyclicGraph(); + ctx.cfg = createControlFlowGraph(); + ctx.uniforms = []; + ctx.vertexDeclarations = new Set(); + ctx.fragmentDeclarations = new Set(); + ctx.hooks = []; + ctx.backend = backend; + ctx.active = active; + ctx.previousFES = p5.disableFriendlyErrors; + ctx.windowOverrides = {}; + ctx.fnOverrides = {}; + if (active) { + p5.disableFriendlyErrors = true; + } + } + + function deinitStrandsContext(ctx) { + ctx.dag = createDirectedAcyclicGraph(); + ctx.cfg = createControlFlowGraph(); + ctx.uniforms = []; + ctx.vertexDeclarations = new Set(); + ctx.fragmentDeclarations = new Set(); + ctx.hooks = []; + ctx.active = false; + p5.disableFriendlyErrors = ctx.previousFES; + for (const key in ctx.windowOverrides) { + window[key] = ctx.windowOverrides[key]; + } + for (const key in ctx.fnOverrides) { + fn[key] = ctx.fnOverrides[key]; + } + } + + const strandsContext = {}; + initStrandsContext(strandsContext); + initGlobalStrandsAPI(p5, fn, strandsContext) + + ////////////////////////////////////////////// + // Entry Point + ////////////////////////////////////////////// + const oldModify = p5.Shader.prototype.modify; + + p5.Shader.prototype.modify = function(shaderModifier, scope = {}) { + if (shaderModifier instanceof Function) { + // Reset the context object every time modify is called; + // const backend = glslBackend; + initStrandsContext(strandsContext, glslBackend, { active: true }); + createShaderHooksFunctions(strandsContext, fn, this); + // TODO: expose this, is internal for debugging for now. + const options = { parser: true, srcLocations: false }; + + // 1. Transpile from strands DSL to JS + let strandsCallback; + 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()})`; + strandsCallback = transpileStrandsToJS(p5, sourceString, options.srcLocations, scope); + } else { + strandsCallback = shaderModifier; + } + + // 2. Build the IR from JavaScript API + const globalScope = createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); + pushBlock(strandsContext.cfg, globalScope); + strandsCallback(); + popBlock(strandsContext.cfg); + + // 3. Generate shader code hooks object from the IR + // ....... + const hooksObject = generateShaderCode(strandsContext); + + // Reset the strands runtime context + deinitStrandsContext(strandsContext); + + // Call modify with the generated hooks object + return oldModify.call(this, hooksObject); + } + else { + return oldModify.call(this, shaderModifier) + } + } +} + +export default strands; + +if (typeof p5 !== 'undefined') { + p5.registerAddon(strands) +} + +/* ------------------------------------------------------------- */ +/** + * @method getWorldInputs + * @description + * Registers a callback to modify the world-space properties of each vertex in a shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. + * + * The callback receives a vertex object with the following properties: + * - `position`: a three-component vector representing the original position of the vertex. + * - `normal`: a three-component vector representing the direction the surface is facing. + * - `texCoord`: a two-component vector representing the texture coordinates. + * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). + * + * This hook is available in: + * - baseMaterialShader() + * - baseNormalShader() + * - baseColorShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives a vertex object containing position (vec3), normal (vec3), texCoord (vec2), and color (vec4) properties. The function should return the modified vertex object. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify(() => { + * let t = uniformFloat(() => millis()); + * getWorldInputs(inputs => { + * // Move the vertex up and down in a wave in world space + * // In world space, moving the object (e.g., with translate()) will affect these coordinates +* // The sphere is ~50 units tall here, so 20 gives a noticeable wave + * inputs.position.y += 20 * sin(t * 0.001 + inputs.position.x * 0.05); + * return inputs; + * }); + * }); + * } + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ */ + +/** + * @method combineColors + * @description + * Registers a callback to customize how color components are combined in the fragment shader. This hook can be used inside baseMaterialShader().modify() and similar shader modify() calls to control the final color output of a material. The callback receives an object with the following properties: + * + * - `baseColor`: a three-component vector representing the base color (red, green, blue). + * - `diffuse`: a single number representing the diffuse reflection. + * - `ambientColor`: a three-component vector representing the ambient color. + * - `ambient`: a single number representing the ambient reflection. + * - `specularColor`: a three-component vector representing the specular color. + * - `specular`: a single number representing the specular reflection. + * - `emissive`: a three-component vector representing the emissive color. + * - `opacity`: a single number representing the opacity. + * + * The callback should return a vector with four components (red, green, blue, alpha) for the final color. + * + * This hook is available in: + * - baseMaterialShader() + * + * @param {Function} callback + * A callback function which receives the object described above and returns a vector with four components for the final color. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify(() => { + * combineColors(components => { + * // Custom color combination: add a green tint using vector properties + * return [ + * components.baseColor * components.diffuse + + * components.ambientColor * components.ambient + + * components.specularColor * components.specular + + * components.emissive + + * [0, 0.2, 0], // Green tint for visibility + * components.opacity + * ]; + * }); + * }); + * } + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('white'); + * sphere(50); + * } + * + *
+ */ + +/** + * @method beforeVertex + * @private + * @description + * Registers a callback to run custom code at the very start of the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to set up variables or perform calculations that affect every vertex before processing begins. The callback receives no arguments. + * + * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which is called before each vertex is processed. + */ + +/** + * @method afterVertex + * @private + * @description + * Registers a callback to run custom code at the very end of the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to perform cleanup or final calculations after all vertex processing is done. The callback receives no arguments. + * + * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which is called after each vertex is processed. + */ + +/** + * @method beforeFragment + * @private + * @description + * Registers a callback to run custom code at the very start of the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to set up variables or perform calculations that affect every pixel before color calculations begin. The callback receives no arguments. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which is called before each fragment is processed. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * beforeFragment(() => { + * // Set a value for use in getFinalColor + * this.brightness = 0.5 + 0.5 * sin(millis() * 0.001); + * }); + * getFinalColor(color => { + * // Use the value set in beforeFragment to tint the color + * color.r *= this.brightness; // Tint red channel + * return color; + * }); + * }); + * } + * function draw() { + * background(220); + * shader(myShader); + * noStroke(); + * fill('teal'); + * box(100); + * } + * + *
+ */ + +/** + * @method getPixelInputs + * @description + * Registers a callback to modify the properties of each fragment (pixel) before the final color is calculated in the fragment shader. This hook can be used inside baseMaterialShader().modify() and similar shader modify() calls to adjust per-pixel data before lighting/mixing. + * + * The callback receives an `Inputs` object. Available fields depend on the shader: + * + * - In baseMaterialShader(): + * - `normal`: a three-component vector representing the surface normal. + * - `texCoord`: a two-component vector representing the texture coordinates (u, v). + * - `ambientLight`: a three-component vector representing the ambient light color. + * - `ambientMaterial`: a three-component vector representing the material's ambient color. + * - `specularMaterial`: a three-component vector representing the material's specular color. + * - `emissiveMaterial`: a three-component vector representing the material's emissive color. + * - `color`: a four-component vector representing the base color (red, green, blue, alpha). + * - `shininess`: a number controlling specular highlights. + * - `metalness`: a number controlling the metalness factor. + * + * - In baseStrokeShader(): + * - `color`: a four-component vector representing the stroke color (red, green, blue, alpha). + * - `tangent`: a two-component vector representing the stroke tangent. + * - `center`: a two-component vector representing the cap/join center. + * - `position`: a two-component vector representing the current fragment position. + * - `strokeWeight`: a number representing the stroke weight in pixels. + * + * Return the modified object to update the fragment. + * + * This hook is available in: + * - baseMaterialShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives the fragment inputs object and should return it after making any changes. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify(() => { + * let t = uniformFloat(() => millis()); + * getPixelInputs(inputs => { + * // Animate alpha (transparency) based on x position + * inputs.color.a = 0.5 + 0.5 * sin(inputs.texCoord.x * 10.0 + t * 0.002); + * return inputs; + * }); + * }); + * } + * function draw() { + * background(240); + * shader(myShader); + * lights(); + * noStroke(); + * fill('purple'); + * circle(0, 0, 100); + * } + * + *
+ */ + +/** + * @method shouldDiscard + * @private + * @description + * Registers a callback to decide whether to discard (skip drawing) a fragment (pixel) in the fragment shader. This hook can be used inside baseStrokeShader().modify() and similar shader modify() calls to create effects like round points or custom masking. The callback receives a boolean: + * - `willDiscard`: true if the fragment would be discarded by default + * + * Return true to discard the fragment, or false to keep it. + * + * This hook is available in: + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives a boolean and should return a boolean. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseStrokeShader().modify({ + * 'bool shouldDiscard': '(bool outside) { return outside; }' + * }); + * } + * function draw() { + * background(255); + * strokeShader(myShader); + * strokeWeight(30); + * line(-width/3, 0, width/3, 0); + * } + * + *
+ */ + +/** + * @method getFinalColor + * @description + * Registers a callback to change the final color of each pixel after all lighting and mixing is done in the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to adjust the color before it appears on the screen. The callback receives a four component vector representing red, green, blue, and alpha. + * + * Return a new color array to change the output color. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives the color array and should return a color array. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * getFinalColor(color => { + * // Add a blue tint to the output color + * color.b += 0.4; + * return color; + * }); + * }); + * } + * function draw() { + * background(230); + * shader(myShader); + * noStroke(); + * fill('green'); + * circle(0, 0, 100); + * } + * + *
+ */ + +/** + * @method afterFragment + * @private + * @description + * Registers a callback to run custom code at the very end of the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to perform cleanup or final per-pixel effects after all color calculations are done. The callback receives no arguments. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which is called after each fragment is processed. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * getFinalColor(color => { + * // Add a purple tint to the color + * color.b += 0.2; + * return color; + * }); + * afterFragment(() => { + * // This hook runs after the final color is set for each fragment. + * // You could use this for debugging or advanced effects. + * }); + * }); + * } + * function draw() { + * background(240); + * shader(myShader); + * noStroke(); + * fill('purple'); + * sphere(60); + * } + * + *
+ */ + +/** + * @method getColor + * @description + * Registers a callback to set the final color for each pixel in a filter shader. This hook can be used inside baseFilterShader().modify() and similar shader modify() calls to control the output color for each pixel. The callback receives the following arguments: + * - `inputs`: an object with the following properties: + * - `texCoord`: a two-component vector representing the texture coordinates (u, v). + * - `canvasSize`: a two-component vector representing the canvas size in pixels (width, height). + * - `texelSize`: a two-component vector representing the size of a single texel in texture space. + * - `canvasContent`: a texture containing the sketch's contents before the filter is applied. + * + * Return a four-component vector `[r, g, b, a]` for the pixel. + * + * This hook is available in: + * - baseFilterShader() + * + * @param {Function} callback + * A callback function which receives the inputs object and canvasContent, and should return a color array. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseFilterShader().modify(() => { + * getColor((inputs, canvasContent) => { + * // Warp the texture coordinates for a wavy effect + * let warped = [inputs.texCoord.x, inputs.texCoord.y + 0.1 * sin(inputs.texCoord.x * 10.0)]; + * return getTexture(canvasContent, warped); + * }); + * }); + * } + * function draw() { + * background(180); + * // Draw something to the canvas + * fill('yellow'); + * circle(0, 0, 150); + * filter(myShader); + * } + * + *
+ */ + +/** + * @method getObjectInputs + * @description + * Registers a callback to modify the properties of each vertex before any transformations are applied in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to move, color, or otherwise modify the raw model data. The callback receives an object with the following properties: + * + * - `position`: a three-component vector representing the original position of the vertex. + * - `normal`: a three-component vector representing the direction the surface is facing. + * - `texCoord`: a two-component vector representing the texture coordinates. + * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). + * + * Return the modified object to update the vertex. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives the vertex object and should return it after making any changes. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * let t = uniformFloat(() => millis()); + * getObjectInputs(inputs => { + * // Create a sine wave along the x axis in object space + * inputs.position.y += sin(t * 0.001 + inputs.position.x); + * return inputs; + * }); + * }); + * } + * function draw() { + * background(220); + * shader(myShader); + * noStroke(); + * fill('orange'); + * sphere(50); + * } + * + *
+ */ + +/** + * @method getCameraInputs + * @description + * Registers a callback to adjust vertex properties after the model has been transformed by the camera, but before projection, in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to create effects that depend on the camera's view. The callback receives an object with the following properties: + * + * - `position`: a three-component vector representing the position after camera transformation. + * - `normal`: a three-component vector representing the normal after camera transformation. + * - `texCoord`: a two-component vector representing the texture coordinates. + * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). + * + * Return the modified object to update the vertex. + * + * This hook is available in: + * - baseColorShader() + * - baseMaterialShader() + * - baseNormalShader() + * - baseStrokeShader() + * + * @param {Function} callback + * A callback function which receives the vertex object and should return it after making any changes. + * + * @example + *
+ * + * let myShader; + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().modify(() => { + * getCameraInputs(inputs => { + * // Move vertices in camera space based on their x position + * let t = uniformFloat(() => millis()); + * inputs.position.y += 30 * sin(inputs.position.x * 0.05 + t * 0.001); + * // Tint all vertices blue + * inputs.color.b = 1; + * return inputs; + * }); + * }); + * } + * function draw() { + * background(200); + * shader(myShader); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ */ diff --git a/src/strands/strands_FES.js b/src/strands/strands_FES.js new file mode 100644 index 0000000000..3af0aca90b --- /dev/null +++ b/src/strands/strands_FES.js @@ -0,0 +1,9 @@ +export function internalError(errorMessage) { + const prefixedMessage = `[p5.strands internal error]: ${errorMessage}` + throw new Error(prefixedMessage); +} + +export function userError(errorType, errorMessage) { + const prefixedMessage = `[p5.strands ${errorType}]: ${errorMessage}`; + throw new Error(prefixedMessage); +} \ No newline at end of file diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js new file mode 100644 index 0000000000..0bb0ce9866 --- /dev/null +++ b/src/strands/strands_api.js @@ -0,0 +1,369 @@ +import * as build from './ir_builders' +import { + OperatorTable, + BlockType, + DataType, + BaseType, + structType, + TypeInfoFromGLSLName, + isStructType, + OpCode, + // isNativeType +} from './ir_types' +import { strandsBuiltinFunctions } from './strands_builtins' +import { StrandsConditional } from './strands_conditionals' +import * as CFG from './ir_cfg' +import * as FES from './strands_FES' +import { getNodeDataFromID } from './ir_dag' +import noiseGLSL from '../webgl/shaders/functions/noiseGLSL.glsl'; + +////////////////////////////////////////////// +// User nodes +////////////////////////////////////////////// +export class StrandsNode { + constructor(id, dimension, strandsContext) { + this.id = id; + this.strandsContext = strandsContext; + this.dimension = dimension; + } +} + +export function createStrandsNode(id, dimension, strandsContext, onRebind) { + return new Proxy( + new StrandsNode(id, dimension, strandsContext), + build.swizzleTrap(id, dimension, strandsContext, onRebind) + ); +} + +export function initGlobalStrandsAPI(p5, fn, strandsContext) { + // We augment the strands node with operations programatically + // this means methods like .add, .sub, etc can be chained + for (const { name, arity, opCode } of OperatorTable) { + if (arity === 'binary') { + StrandsNode.prototype[name] = function (...right) { + const { id, dimension } = build.binaryOpNode(strandsContext, this, right, opCode); + return createStrandsNode(id, dimension, strandsContext); + }; + } + if (arity === 'unary') { + p5[name] = function (nodeOrValue) { + const { id, dimension } = build.unaryOpNode(strandsContext, nodeOrValue, opCode); + return createStrandsNode(id, dimension, strandsContext); + } + } + } + + ////////////////////////////////////////////// + // Unique Functions + ////////////////////////////////////////////// + fn.discard = function() { + build.statementNode(strandsContext, OpCode.ControlFlow.DISCARD); + } + + fn.instanceID = function() { + const node = build.variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, 'gl_InstanceID'); + return createStrandsNode(node.id, node.dimension, strandsContext); + } + + fn.strandsIf = function(conditionNode, ifBody) { + return new StrandsConditional(strandsContext, conditionNode, ifBody); + } + + fn.strandsLoop = function(a, b, loopBody) { + return null; + } + + p5.strandsNode = function(...args) { + if (args.length === 1 && args[0] instanceof StrandsNode) { + return args[0]; + } + if (args.length > 4) { + FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") + } + const { id, dimension } = build.primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, args.flat()); + return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext); + } + + ////////////////////////////////////////////// + // Builtins, uniforms, variable constructors + ////////////////////////////////////////////// + for (const [functionName, overrides] of Object.entries(strandsBuiltinFunctions)) { + const isp5Function = overrides[0].isp5Function; + + if (isp5Function) { + const originalFn = fn[functionName]; + fn[functionName] = function(...args) { + if (strandsContext.active) { + const { id, dimension } = build.functionCallNode(strandsContext, functionName, args); + return createStrandsNode(id, dimension, strandsContext); + } else { + return originalFn.apply(this, args); + } + } + } else { + fn[functionName] = function (...args) { + if (strandsContext.active) { + const { id, dimension } = build.functionCallNode(strandsContext, functionName, args); + return createStrandsNode(id, dimension, strandsContext); + } else { + p5._friendlyError( + `It looks like you've called ${functionName} outside of a shader's modify() function.` + ) + } + } + } + } + + // Add GLSL noise. TODO: Replace this with a backend-agnostic implementation + const originalNoise = fn.noise; + fn.noise = function (...args) { + if (!strandsContext.active) { + return originalNoise.apply(this, args); // fallback to regular p5.js noise + } + + strandsContext.vertexDeclarations.add(noiseGLSL); + strandsContext.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; + } + + const { id, dimension } = build.functionCallNode(strandsContext, 'noise', nodeArgs, { + overloads: [{ + params: [DataType.float2], + returnType: DataType.float1, + }] + }); + return createStrandsNode(id, dimension, strandsContext); + }; + + // Next is type constructors and uniform functions + for (const type in DataType) { + if (type === BaseType.DEFER) { + continue; + } + const typeInfo = DataType[type]; + + let pascalTypeName; + if (/^[ib]vec/.test(typeInfo.fnName)) { + pascalTypeName = typeInfo.fnName + .slice(0, 2).toUpperCase() + + typeInfo.fnName + .slice(2) + .toLowerCase(); + } else { + pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase() + + typeInfo.fnName.slice(1).toLowerCase(); + } + fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { + const { id, dimension } = build.variableNode(strandsContext, typeInfo, name); + strandsContext.uniforms.push({ name, typeInfo, defaultValue }); + return createStrandsNode(id, dimension, strandsContext); + }; + if (pascalTypeName.startsWith('Vec')) { + // For compatibility, also alias uniformVec2 as uniformVector2, what we initially + // documented these as + fn[`uniform${pascalTypeName.replace('Vec', 'Vector')}`] = fn[`uniform${pascalTypeName}`]; + } + + const originalp5Fn = fn[typeInfo.fnName]; + fn[typeInfo.fnName] = function(...args) { + if (strandsContext.active) { + const { id, dimension } = build.primitiveConstructorNode(strandsContext, typeInfo, args); + return createStrandsNode(id, dimension, strandsContext); + } else if (originalp5Fn) { + return originalp5Fn.apply(this, args); + } else { + p5._friendlyError( + `It looks like you've called ${typeInfo.fnName} outside of a shader's modify() function.` + ); + } + } + } +} + +////////////////////////////////////////////// +// Per-Hook functions +////////////////////////////////////////////// +function createHookArguments(strandsContext, parameters){ + const args = []; + const dag = strandsContext.dag; + + for (const param of parameters) { + if(isStructType(param.type.typeName)) { + const structTypeInfo = structType(param); + const { id, dimension } = build.structInstanceNode(strandsContext, structTypeInfo, param.name, []); + const structNode = createStrandsNode(id, dimension, strandsContext); + for (let i = 0; i < structTypeInfo.properties.length; i++) { + const propertyType = structTypeInfo.properties[i]; + Object.defineProperty(structNode, propertyType.name, { + get() { + const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]) + const onRebind = (newFieldID) => { + const oldDeps = dag.dependsOn[structNode.id]; + const newDeps = oldDeps.slice(); + newDeps[i] = newFieldID; + const rebuilt = build.structInstanceNode(strandsContext, structTypeInfo, param.name, newDeps); + structNode.id = rebuilt.id; + }; + // TODO: implement member access operations + // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); + // const memberAccessNode = new StrandsNode(id, components); + // return memberAccessNode; + return createStrandsNode(propNode.id, propNode.dimension, strandsContext, onRebind); + }, + set(val) { + const oldDependsOn = dag.dependsOn[structNode.id]; + const newDependsOn = [...oldDependsOn]; + + let newValueID; + if (val instanceof StrandsNode) { + newValueID = val.id; + } + else { + let newVal = build.primitiveConstructorNode(strandsContext, propertyType.dataType, val); + newValueID = newVal.id; + } + + newDependsOn[i] = newValueID; + const newStructInfo = build.structInstanceNode(strandsContext, structTypeInfo, param.name, newDependsOn); + structNode.id = newStructInfo.id; + } + }) + } + + args.push(structNode); + } + else /*if(isNativeType(paramType.typeName))*/ { + const typeInfo = TypeInfoFromGLSLName[param.type.typeName]; + const { id, dimension } = build.variableNode(strandsContext, typeInfo, param.name); + const arg = createStrandsNode(id, dimension, strandsContext); + args.push(arg); + } + } + return args; +} + +function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { + if (!(returned instanceof StrandsNode)) { + // try { + const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); + return result.id; + // } catch (e) { + // FES.userError('type error', + // `There was a type mismatch for a value returned from ${hookName}.\n` + + // `The value in question was supposed to be:\n` + + // `${expectedType.baseType + expectedType.dimension}\n` + + // `But you returned:\n` + + // `${returned}` + // ); + // } + } + + const dag = strandsContext.dag; + let returnedNodeID = returned.id; + const receivedType = { + baseType: dag.baseTypes[returnedNodeID], + dimension: dag.dimensions[returnedNodeID], + } + if (receivedType.dimension !== expectedType.dimension) { + if (receivedType.dimension !== 1) { + FES.userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookName} when a ${expectedType.baseType + expectedType.dimension} was expected!`); + } + else { + const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); + returnedNodeID = result.id; + } + } + else if (receivedType.baseType !== expectedType.baseType) { + const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); + returnedNodeID = result.id; + } + + return returnedNodeID; +} + +export function createShaderHooksFunctions(strandsContext, fn, shader) { + const availableHooks = { + ...shader.hooks.vertex, + ...shader.hooks.fragment, + } + const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); + const { cfg, dag } = strandsContext; + + for (const hookType of hookTypes) { + const hookImplementation = function(hookUserCallback) { + const entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); + CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); + CFG.pushBlock(cfg, entryBlockID); + + const args = createHookArguments(strandsContext, hookType.parameters); + const userReturned = hookUserCallback(...args); + const expectedReturnType = hookType.returnType; + + let rootNodeID = null; + + if(isStructType(expectedReturnType.typeName)) { + const expectedStructType = structType(expectedReturnType); + if (userReturned instanceof StrandsNode) { + const returnedNode = getNodeDataFromID(strandsContext.dag, userReturned.id); + + if (returnedNode.baseType !== expectedStructType.typeName) { + FES.userError("type error", `You have returned a ${userReturned.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`); + } + + const newDeps = returnedNode.dependsOn.slice(); + + for (let i = 0; i < expectedStructType.properties.length; i++) { + const expectedType = expectedStructType.properties[i].dataType; + const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[userReturned.id], strandsContext); + newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name); + } + + dag.dependsOn[userReturned.id] = newDeps; + rootNodeID = userReturned.id; + } + else { + const expectedProperties = expectedStructType.properties; + const newStructDependencies = []; + for (let i = 0; i < expectedProperties.length; i++) { + const expectedProp = expectedProperties[i]; + const propName = expectedProp.name; + const receivedValue = userReturned[propName]; + if (receivedValue === undefined) { + FES.userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` + + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + + `Received: { ${Object.keys(userReturned).join(', ')} }\n` + + `All of the properties are required!`); + } + const expectedTypeInfo = expectedProp.dataType; + const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); + newStructDependencies.push(returnedPropID); + } + const newStruct = build.structConstructorNode(strandsContext, expectedStructType, newStructDependencies); + rootNodeID = newStruct.id; + } + + } + else /*if(isNativeType(expectedReturnType.typeName))*/ { + const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName]; + rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); + } + + strandsContext.hooks.push({ + hookType, + entryBlockID, + rootNodeID, + }); + CFG.popBlock(cfg); + } + strandsContext.windowOverrides[hookType.name] = window[hookType.name]; + strandsContext.fnOverrides[hookType.name] = fn[hookType.name]; + + window[hookType.name] = hookImplementation; + fn[hookType.name] = hookImplementation; + } +} diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js new file mode 100644 index 0000000000..3eb76c8ff6 --- /dev/null +++ b/src/strands/strands_builtins.js @@ -0,0 +1,114 @@ +import { GenType, DataType } from "./ir_types" + +// GLSL Built in functions +// https://docs.gl/el3/abs +const builtInGLSLFunctions = { + //////////// Trigonometry ////////// + acos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + acosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + asin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + asinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + atan: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + ], + atanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + cos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + cosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + degrees: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + radians: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + sin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT , isp5Function: true}], + sinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + tan: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + tanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + + ////////// Mathematics ////////// + abs: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT], returnType: GenType.INT, isp5Function: true} + ], + ceil: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + clamp: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT,DataType.float1,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.INT, GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: false}, + { params: [GenType.INT, DataType.int1, DataType.int1], returnType: GenType.INT, isp5Function: false}, + ], + dFdx: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + dFdy: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + exp: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + exp2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + floor: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + fma: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + fract: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + fwidth: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + inversesqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + // "isinf": [{}], + // "isnan": [{}], + log: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + log2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + max: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true}, + { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true}, + ], + min: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true}, + { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true}, + ], + mix: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.FLOAT, GenType.FLOAT, GenType.BOOL], returnType: GenType.FLOAT, isp5Function: false}, + ], + mod: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, + { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, + ], + // "modf": [{}], + pow: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + round: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + roundEven: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + sign: [ + { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [GenType.INT], returnType: GenType.INT, isp5Function: false}, + ], + smoothstep: [ + { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + { params: [ DataType.float1,DataType.float1, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + ], + sqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + step: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + trunc: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + + ////////// Vector ////////// + cross: [{ params: [DataType.float3, DataType.float3], returnType: DataType.float3, isp5Function: true}], + distance: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}], + dot: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}], + equal: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false}, + ], + faceforward: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + length: [{ params: [GenType.FLOAT], returnType:DataType.float1, isp5Function: false}], + normalize: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], + notEqual: [ + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false}, + { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false}, + ], + reflect: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], + refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}], + + ////////// Texture sampling ////////// + texture: [{params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4, isp5Function: true}], + getTexture: [{params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4, isp5Function: true}] +} + +export const strandsBuiltinFunctions = { + ...builtInGLSLFunctions, +} \ No newline at end of file diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js new file mode 100644 index 0000000000..911ab8376b --- /dev/null +++ b/src/strands/strands_codegen.js @@ -0,0 +1,51 @@ +import { sortCFG } from "./ir_cfg"; +import { structType, TypeInfoFromGLSLName } from './ir_types'; + +export function generateShaderCode(strandsContext) { + const { + cfg, + backend, + vertexDeclarations, + fragmentDeclarations + } = strandsContext; + + const hooksObj = { + uniforms: {}, + }; + + for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) { + const declaration = backend.generateUniformDeclaration(name, typeInfo); + hooksObj.uniforms[declaration] = defaultValue; + } + + for (const { hookType, rootNodeID, entryBlockID } of strandsContext.hooks) { + const generationContext = { + indent: 1, + codeLines: [], + write(line) { + this.codeLines.push(' '.repeat(this.indent) + line); + }, + tempNames: {}, + declarations: [], + nextTempID: 0, + }; + + const blocks = sortCFG(cfg.outgoingEdges, entryBlockID); + for (const blockID of blocks) { + backend.generateBlock(blockID, strandsContext, generationContext); + } + + const firstLine = backend.hookEntry(hookType); + let returnType = hookType.returnType.properties + ? structType(hookType.returnType) + : TypeInfoFromGLSLName[hookType.returnType.typeName]; + backend.generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType); + hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); + console.log(hooksObj[`${hookType.returnType.typeName} ${hookType.name}`]); + } + + hooksObj.vertexDeclarations = [...vertexDeclarations].join('\n'); + hooksObj.fragmentDeclarations = [...fragmentDeclarations].join('\n'); + + return hooksObj; +} diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js new file mode 100644 index 0000000000..1ce888cc91 --- /dev/null +++ b/src/strands/strands_conditionals.js @@ -0,0 +1,71 @@ +import * as CFG from './ir_cfg' +import { BlockType } from './ir_types'; + +export class StrandsConditional { + constructor(strandsContext, condition, branchCallback) { + // Condition must be a node... + this.branches = [{ + condition, + branchCallback, + blockType: BlockType.IF_BODY + }]; + this.ctx = strandsContext; + } + + ElseIf(condition, branchCallback) { + this.branches.push({ + condition, + branchCallback, + blockType: BlockType.ELIF_BODY + }); + return this; + } + + Else(branchCallback = () => ({})) { + this.branches.push({ + condition: null, + branchCallback, + blockType: BlockType.ELSE_BODY + }); + return buildConditional(this.ctx, this); + } +} + +function buildConditional(strandsContext, conditional) { + const cfg = strandsContext.cfg; + const branches = conditional.branches; + + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + const results = []; + + let previousBlock = cfg.currentBlock; + + for (let i = 0; i < branches.length; i++) { + const { condition, branchCallback, blockType } = branches[i]; + + if (condition !== null) { + const conditionBlock = CFG.createBasicBlock(cfg, BlockType.IF_COND); + CFG.addEdge(cfg, previousBlock, conditionBlock); + CFG.pushBlock(cfg, conditionBlock); + cfg.blockConditions[conditionBlock] = condition.id; + previousBlock = conditionBlock; + CFG.popBlock(cfg); + } + + const branchBlock = CFG.createBasicBlock(cfg, blockType); + CFG.addEdge(cfg, previousBlock, branchBlock); + + CFG.pushBlock(cfg, branchBlock); + const branchResults = branchCallback(); + results.push(branchResults); + if (cfg.currentBlock !== branchBlock) { + CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); + CFG.popBlock(); + } + CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); + CFG.popBlock(cfg); + } + CFG.pushBlock(cfg, mergeBlock); + + return results; +} \ No newline at end of file diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js new file mode 100644 index 0000000000..70552bad1b --- /dev/null +++ b/src/strands/strands_glslBackend.js @@ -0,0 +1,213 @@ +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType } from "./ir_types"; +import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; +import * as FES from './strands_FES' + +function shouldCreateTemp(dag, nodeID) { + const nodeType = dag.nodeTypes[nodeID]; + if (nodeType !== NodeType.OPERATION) return false; + if (dag.baseTypes[nodeID] === BaseType.SAMPLER2D) return false; + const uses = dag.usedBy[nodeID] || []; + return uses.length > 1; +} + +const TypeNames = { + 'float1': 'float', + 'float2': 'vec2', + 'float3': 'vec3', + 'float4': 'vec4', + 'int1': 'int', + 'int2': 'ivec2', + 'int3': 'ivec3', + 'int4': 'ivec4', + 'bool1': 'bool', + 'bool2': 'bvec2', + 'bool3': 'bvec3', + 'bool4': 'bvec4', + 'mat2': 'mat2x2', + 'mat3': 'mat3x3', + 'mat4': 'mat4x4', +} + +const cfgHandlers = { + [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { + const { dag, cfg } = strandsContext; + + const instructions = cfg.blockInstructions[blockID] || []; + for (const nodeID of instructions) { + const nodeType = dag.nodeTypes[nodeID]; + if (shouldCreateTemp(dag, nodeID)) { + const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID); + generationContext.write(declaration); + } + if (nodeType === NodeType.STATEMENT) { + glslBackend.generateStatement(generationContext, dag, nodeID); + } + } + }, + + [BlockType.IF_COND](blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + const conditionID = cfg.blockConditions[blockID]; + const condExpr = glslBackend.generateExpression(generationContext, dag, conditionID); + generationContext.write(`if (${condExpr}) {`) + generationContext.indent++; + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + generationContext.indent--; + generationContext.write(`}`) + return; + }, + + [BlockType.IF_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.ELIF_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) { + + }, + + [BlockType.MERGE](blockID, strandsContext, generationContext) { + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, + + [BlockType.FUNCTION](blockID, strandsContext, generationContext) { + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, +} + + +export const glslBackend = { + hookEntry(hookType) { + const firstLine = `(${hookType.parameters.flatMap((param) => { + return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${param.name}`; + }).join(', ')}) {`; + return firstLine; + }, + + getTypeName(baseType, dimension) { + const primitiveTypeName = TypeNames[baseType + dimension] + if (!primitiveTypeName) { + return baseType; + } + return primitiveTypeName; + }, + + generateUniformDeclaration(name, typeInfo) { + return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; + }, + + generateStatement(generationContext, dag, nodeID) { + const node = getNodeDataFromID(dag, nodeID); + if (node.statementType === OpCode.ControlFlow.DISCARD) { + generationContext.write('discard;'); + } + }, + + generateDeclaration(generationContext, dag, nodeID) { + const expr = this.generateExpression(generationContext, dag, nodeID); + const tmp = `T${generationContext.nextTempID++}`; + generationContext.tempNames[nodeID] = tmp; + + const T = extractNodeTypeInfo(dag, nodeID); + const typeName = this.getTypeName(T.baseType, T.dimension); + return `${typeName} ${tmp} = ${expr};`; + }, + + generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { + const dag = strandsContext.dag; + const rootNode = getNodeDataFromID(dag, rootNodeID); + if (isStructType(rootNode.baseType)) { + const structTypeInfo = returnType; + for (let i = 0; i < structTypeInfo.properties.length; i++) { + const prop = structTypeInfo.properties[i]; + const val = this.generateExpression(generationContext, dag, rootNode.dependsOn[i]); + if (prop.name !== val) { + generationContext.write( + `${rootNode.identifier}.${prop.name} = ${val};` + ) + } + } + } + generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`); + }, + + generateExpression(generationContext, dag, nodeID) { + const node = getNodeDataFromID(dag, nodeID); + if (generationContext.tempNames?.[nodeID]) { + return generationContext.tempNames[nodeID]; + } + switch (node.nodeType) { + case NodeType.LITERAL: + if (node.baseType === BaseType.FLOAT) { + return node.value.toFixed(4); + } + else { + return node.value; + } + + case NodeType.VARIABLE: + return node.identifier; + + case NodeType.OPERATION: + const useParantheses = node.usedBy.length > 0; + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { + // TODO: differentiate casts and constructors for more efficient codegen. + // if (node.dependsOn.length === 1 && node.dimension === 1) { + // return this.generateExpression(generationContext, dag, node.dependsOn[0]); + // } + if (node.baseType === BaseType.SAMPLER2D) { + return this.generateExpression(generationContext, dag, node.dependsOn[0]); + } + const T = this.getTypeName(node.baseType, node.dimension); + const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); + return `${T}(${deps.join(', ')})`; + } + if (node.opCode === OpCode.Nary.FUNCTION_CALL) { + const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); + return `${node.identifier}(${functionArgs.join(', ')})`; + } + if (node.opCode === OpCode.Binary.MEMBER_ACCESS) { + const [lID, rID] = node.dependsOn; + const lName = this.generateExpression(generationContext, dag, lID); + const rName = this.generateExpression(generationContext, dag, rID); + return `${lName}.${rName}`; + } + if (node.opCode === OpCode.Unary.SWIZZLE) { + const parentID = node.dependsOn[0]; + const parentExpr = this.generateExpression(generationContext, dag, parentID); + return `${parentExpr}.${node.swizzle}`; + } + if (node.dependsOn.length === 2) { + const [lID, rID] = node.dependsOn; + const left = this.generateExpression(generationContext, dag, lID); + const right = this.generateExpression(generationContext, dag, rID); + const opSym = OpCodeToSymbol[node.opCode]; + if (useParantheses) { + return `(${left} ${opSym} ${right})`; + } else { + return `${left} ${opSym} ${right}`; + } + } + if (node.opCode === OpCode.Unary.LOGICAL_NOT + || node.opCode === OpCode.Unary.NEGATE + || node.opCode === OpCode.Unary.PLUS + ) { + const [i] = node.dependsOn; + const val = this.generateExpression(generationContext, dag, i); + const sym = OpCodeToSymbol[node.opCode]; + return `${sym}${val}`; + } + default: + FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`) + } + }, + + generateBlock(blockID, strandsContext, generationContext) { + const type = strandsContext.cfg.blockTypes[blockID]; + const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; + handler.call(cfgHandlers, blockID, strandsContext, generationContext); + } +} diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js new file mode 100644 index 0000000000..dd39a21c87 --- /dev/null +++ b/src/strands/strands_transpiler.js @@ -0,0 +1,234 @@ +import { parse } from 'acorn'; +import { ancestor } from 'acorn-walk'; +import escodegen from 'escodegen'; +import { UnarySymbolToName } from './ir_types'; + +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 nodeIsUniform(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') + ) + ); +} + +const ASTCallbacks = { + UnaryExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + + + const unaryFnName = UnarySymbolToName[node.operator]; + const standardReplacement = (node) => { + node.type = 'CallExpression' + node.callee = { + type: 'Identifier', + name: `__p5.${unaryFnName}`, + } + node.arguments = [node.argument] + } + + 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: `__p5.${unaryFnName}` + }, + arguments: [node.argument.object], + }; + node.property = { + type: 'Identifier', + name: property + }; + } else { + standardReplacement(node); + } + } else { + standardReplacement(node); + } + delete node.argument; + delete node.operator; + }, + VariableDeclarator(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + if (nodeIsUniform(node.init)) { + 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; } + 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) { + if (ancestors.some(nodeIsUniform)) { return; } + const original = JSON.parse(JSON.stringify(node)); + node.type = 'CallExpression'; + node.callee = { + type: 'Identifier', + name: '__p5.strandsNode', + }; + node.arguments = [original]; + }, + AssignmentExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + 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(nodeIsUniform)) { 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: '__p5.strandsNode', + }, + 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]; + }, + } + + export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { + const ast = parse(sourceString, { + ecmaVersion: 2021, + locations: srcLocations + }); + ancestor(ast, ASTCallbacks, undefined, { varyings: {} }); + const transpiledSource = escodegen.generate(ast); + const scopeKeys = Object.keys(scope); + const internalStrandsCallback = 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, + transpiledSource + .slice( + transpiledSource.indexOf('{') + 1, + transpiledSource.lastIndexOf('}') + ).replaceAll(';', '') + ); + return () => internalStrandsCallback(p5, ...scopeKeys.map(key => scope[key])); + } + \ No newline at end of file diff --git a/src/webgl/ShaderGenerator.js b/src/webgl/ShaderGenerator.js deleted file mode 100644 index 506a880537..0000000000 --- a/src/webgl/ShaderGenerator.js +++ /dev/null @@ -1,2261 +0,0 @@ -/** -* @module 3D -* @submodule Material -* @for p5 -* @requires core -*/ -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; - - p5.Shader.prototype.modify = function(shaderModifier, scope = {}) { - 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, { - 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, - transpiledSource - .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 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 nodeIsUniform(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') - ) - ); - } - - const ASTCallbacks = { - UnaryExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - - const signNode = { - type: 'Literal', - value: node.operator - }; - - const standardReplacement = node => { - node.type = 'CallExpression'; - node.callee = { - type: 'Identifier', - name: '__p5.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: '__p5.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 (ancestors.some(nodeIsUniform)) { return; } - if (nodeIsUniform(node.init)) { - 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; } - 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) { - if (ancestors.some(nodeIsUniform)) { return; } - const original = JSON.parse(JSON.stringify(node)); - node.type = 'CallExpression'; - node.callee = { - type: 'Identifier', - name: '__p5.dynamicNode' - }; - node.arguments = [original]; - }, - AssignmentExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - 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(nodeIsUniform)) { 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: '__p5.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; - } - - p5.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 - p5.unaryNode = function(input, sign) { - input = p5.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 = {}; - const fnOverrides = {}; - - 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 = p5.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]; - 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); - }; - }); - - - this.cleanup = () => { - for (const key in windowOverrides) { - window[key] = windowOverrides[key]; - } - for (const key in fnOverrides) { - fn[key] = fnOverrides[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]; - const descriptor = Object.getOwnPropertyDescriptor( - target, - realProperty - ); - 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 = _size ? _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 dynamicAddSwizzleTrap(new VaryingNode(name, glslType, false)); - }; - - fn[varyingMethodName] = function (name) { - return GLOBAL_SHADER[varyingMethodName](name); - }; - - // Generate the creation methods for creating variables in shaders - const originalFn = fn[glslType]; - fn[glslType] = function (...value) { - if (GLOBAL_SHADER?.isGenerating) { - if (glslType.startsWith('vec')) { - value = conformVectorParameters(value, parseInt(glslType.slice(3))); - } else { - value = value[0]; - } - return nodeConstructors[glslType](value); - } else if (originalFn) { - return originalFn.apply(this, value); - } else { - p5._friendlyError( - `It looks like you've called ${glslType} outside of a shader's modify() function.` - ); - } - }; - } - - // GLSL Built in functions - // Add a whole lot of these functions. - // 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 }, - 'atan': [ - // 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 }, - - ////////// 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 }, - // 'isinf': {}, - // 'isnan': {}, - '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 } - ], - 'min': [ - { 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 } - ], - // 'mod': {}, - // 'modf': {}, - '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 } - ], - '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 }, - // 'equal': {}, - '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 }, - - ////////// Texture sampling ////////// - 'texture': { args: ['sampler2D', 'vec2'], returnType: 'vec4', isp5Function: true } - }; - - Object.entries(builtInGLSLFunctions).forEach(([functionName, properties]) => { - 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); - } else { - return originalFn.apply(this, args); - } - }; - } else { - fn[functionName] = function (...args) { - if (GLOBAL_SHADER?.isGenerating) { - return new fnNodeConstructor(functionName, args, properties); - } else { - p5._friendlyError( - `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; - fn.lerp = function (...args) { - if (GLOBAL_SHADER?.isGenerating) { - return this.mix(...args); // Use mix inside p5.strands - } else { - 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); -} - - - -/* ------------------------------------------------------------- */ -/** - * @method getWorldInputs - * @description - * Registers a callback to modify the world-space properties of each vertex in a shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. - * - * The callback receives a vertex object with the following properties: - * - `position`: a three-component vector representing the original position of the vertex. - * - `normal`: a three-component vector representing the direction the surface is facing. - * - `texCoord`: a two-component vector representing the texture coordinates. - * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). - * - * This hook is available in: - * - baseMaterialShader() - * - baseNormalShader() - * - baseColorShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives a vertex object containing position (vec3), normal (vec3), texCoord (vec2), and color (vec4) properties. The function should return the modified vertex object. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { - * let t = uniformFloat(() => millis()); - * getWorldInputs(inputs => { - * // Move the vertex up and down in a wave in world space - * // In world space, moving the object (e.g., with translate()) will affect these coordinates -* // The sphere is ~50 units tall here, so 20 gives a noticeable wave - * inputs.position.y += 20 * sin(t * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); - * }); - * } - * function draw() { - * background(255); - * shader(myShader); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); - * } - * - *
- */ - -/** - * @method combineColors - * @description - * Registers a callback to customize how color components are combined in the fragment shader. This hook can be used inside baseMaterialShader().modify() and similar shader modify() calls to control the final color output of a material. The callback receives an object with the following properties: - * - * - `baseColor`: a three-component vector representing the base color (red, green, blue). - * - `diffuse`: a single number representing the diffuse reflection. - * - `ambientColor`: a three-component vector representing the ambient color. - * - `ambient`: a single number representing the ambient reflection. - * - `specularColor`: a three-component vector representing the specular color. - * - `specular`: a single number representing the specular reflection. - * - `emissive`: a three-component vector representing the emissive color. - * - `opacity`: a single number representing the opacity. - * - * The callback should return a vector with four components (red, green, blue, alpha) for the final color. - * - * This hook is available in: - * - baseMaterialShader() - * - * @param {Function} callback - * A callback function which receives the object described above and returns a vector with four components for the final color. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { - * combineColors(components => { - * // Custom color combination: add a green tint using vector properties - * return [ - * components.baseColor * components.diffuse + - * components.ambientColor * components.ambient + - * components.specularColor * components.specular + - * components.emissive + - * [0, 0.2, 0], // Green tint for visibility - * components.opacity - * ]; - * }); - * }); - * } - * function draw() { - * background(255); - * shader(myShader); - * lights(); - * noStroke(); - * fill('white'); - * sphere(50); - * } - * - *
- */ - -/** - * @method beforeVertex - * @private - * @description - * Registers a callback to run custom code at the very start of the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to set up variables or perform calculations that affect every vertex before processing begins. The callback receives no arguments. - * - * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported. - * - * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which is called before each vertex is processed. - */ - -/** - * @method afterVertex - * @private - * @description - * Registers a callback to run custom code at the very end of the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to perform cleanup or final calculations after all vertex processing is done. The callback receives no arguments. - * - * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported. - * - * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which is called after each vertex is processed. - */ - -/** - * @method beforeFragment - * @private - * @description - * Registers a callback to run custom code at the very start of the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to set up variables or perform calculations that affect every pixel before color calculations begin. The callback receives no arguments. - * - * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which is called before each fragment is processed. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify(() => { - * beforeFragment(() => { - * // Set a value for use in getFinalColor - * this.brightness = 0.5 + 0.5 * sin(millis() * 0.001); - * }); - * getFinalColor(color => { - * // Use the value set in beforeFragment to tint the color - * color.r *= this.brightness; // Tint red channel - * return color; - * }); - * }); - * } - * function draw() { - * background(220); - * shader(myShader); - * noStroke(); - * fill('teal'); - * box(100); - * } - * - *
- */ - -/** - * @method getPixelInputs - * @description - * Registers a callback to modify the properties of each fragment (pixel) before the final color is calculated in the fragment shader. This hook can be used inside baseMaterialShader().modify() and similar shader modify() calls to adjust per-pixel data before lighting/mixing. - * - * The callback receives an `Inputs` object. Available fields depend on the shader: - * - * - In baseMaterialShader(): - * - `normal`: a three-component vector representing the surface normal. - * - `texCoord`: a two-component vector representing the texture coordinates (u, v). - * - `ambientLight`: a three-component vector representing the ambient light color. - * - `ambientMaterial`: a three-component vector representing the material's ambient color. - * - `specularMaterial`: a three-component vector representing the material's specular color. - * - `emissiveMaterial`: a three-component vector representing the material's emissive color. - * - `color`: a four-component vector representing the base color (red, green, blue, alpha). - * - `shininess`: a number controlling specular highlights. - * - `metalness`: a number controlling the metalness factor. - * - * - In baseStrokeShader(): - * - `color`: a four-component vector representing the stroke color (red, green, blue, alpha). - * - `tangent`: a two-component vector representing the stroke tangent. - * - `center`: a two-component vector representing the cap/join center. - * - `position`: a two-component vector representing the current fragment position. - * - `strokeWeight`: a number representing the stroke weight in pixels. - * - * Return the modified object to update the fragment. - * - * This hook is available in: - * - baseMaterialShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives the fragment inputs object and should return it after making any changes. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { - * let t = uniformFloat(() => millis()); - * getPixelInputs(inputs => { - * // Animate alpha (transparency) based on x position - * inputs.color.a = 0.5 + 0.5 * sin(inputs.texCoord.x * 10.0 + t * 0.002); - * return inputs; - * }); - * }); - * } - * function draw() { - * background(240); - * shader(myShader); - * lights(); - * noStroke(); - * fill('purple'); - * circle(0, 0, 100); - * } - * - *
- */ - -/** - * @method shouldDiscard - * @private - * @description - * Registers a callback to decide whether to discard (skip drawing) a fragment (pixel) in the fragment shader. This hook can be used inside baseStrokeShader().modify() and similar shader modify() calls to create effects like round points or custom masking. The callback receives a boolean: - * - `willDiscard`: true if the fragment would be discarded by default - * - * Return true to discard the fragment, or false to keep it. - * - * This hook is available in: - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives a boolean and should return a boolean. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseStrokeShader().modify({ - * 'bool shouldDiscard': '(bool outside) { return outside; }' - * }); - * } - * function draw() { - * background(255); - * strokeShader(myShader); - * strokeWeight(30); - * line(-width/3, 0, width/3, 0); - * } - * - *
- */ - -/** - * @method getFinalColor - * @description - * Registers a callback to change the final color of each pixel after all lighting and mixing is done in the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to adjust the color before it appears on the screen. The callback receives a four component vector representing red, green, blue, and alpha. - * - * Return a new color array to change the output color. - * - * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives the color array and should return a color array. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify(() => { - * getFinalColor(color => { - * // Add a blue tint to the output color - * color.b += 0.4; - * return color; - * }); - * }); - * } - * function draw() { - * background(230); - * shader(myShader); - * noStroke(); - * fill('green'); - * circle(0, 0, 100); - * } - * - *
- */ - -/** - * @method afterFragment - * @private - * @description - * Registers a callback to run custom code at the very end of the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to perform cleanup or final per-pixel effects after all color calculations are done. The callback receives no arguments. - * - * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which is called after each fragment is processed. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify(() => { - * getFinalColor(color => { - * // Add a purple tint to the color - * color.b += 0.2; - * return color; - * }); - * afterFragment(() => { - * // This hook runs after the final color is set for each fragment. - * // You could use this for debugging or advanced effects. - * }); - * }); - * } - * function draw() { - * background(240); - * shader(myShader); - * noStroke(); - * fill('purple'); - * sphere(60); - * } - * - *
- */ - -/** - * @method getColor - * @description - * Registers a callback to set the final color for each pixel in a filter shader. This hook can be used inside baseFilterShader().modify() and similar shader modify() calls to control the output color for each pixel. The callback receives the following arguments: - * - `inputs`: an object with the following properties: - * - `texCoord`: a two-component vector representing the texture coordinates (u, v). - * - `canvasSize`: a two-component vector representing the canvas size in pixels (width, height). - * - `texelSize`: a two-component vector representing the size of a single texel in texture space. - * - `canvasContent`: a texture containing the sketch's contents before the filter is applied. - * - * Return a four-component vector `[r, g, b, a]` for the pixel. - * - * This hook is available in: - * - baseFilterShader() - * - * @param {Function} callback - * A callback function which receives the inputs object and canvasContent, and should return a color array. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseFilterShader().modify(() => { - * getColor((inputs, canvasContent) => { - * // Warp the texture coordinates for a wavy effect - * let warped = [inputs.texCoord.x, inputs.texCoord.y + 0.1 * sin(inputs.texCoord.x * 10.0)]; - * return getTexture(canvasContent, warped); - * }); - * }); - * } - * function draw() { - * background(180); - * // Draw something to the canvas - * fill('yellow'); - * circle(0, 0, 150); - * filter(myShader); - * } - * - *
- */ - -/** - * @method getObjectInputs - * @description - * Registers a callback to modify the properties of each vertex before any transformations are applied in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to move, color, or otherwise modify the raw model data. The callback receives an object with the following properties: - * - * - `position`: a three-component vector representing the original position of the vertex. - * - `normal`: a three-component vector representing the direction the surface is facing. - * - `texCoord`: a two-component vector representing the texture coordinates. - * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). - * - * Return the modified object to update the vertex. - * - * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives the vertex object and should return it after making any changes. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify(() => { - * let t = uniformFloat(() => millis()); - * getObjectInputs(inputs => { - * // Create a sine wave along the x axis in object space - * inputs.position.y += sin(t * 0.001 + inputs.position.x); - * return inputs; - * }); - * }); - * } - * function draw() { - * background(220); - * shader(myShader); - * noStroke(); - * fill('orange'); - * sphere(50); - * } - * - *
- */ - -/** - * @method getCameraInputs - * @description - * Registers a callback to adjust vertex properties after the model has been transformed by the camera, but before projection, in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to create effects that depend on the camera's view. The callback receives an object with the following properties: - * - * - `position`: a three-component vector representing the position after camera transformation. - * - `normal`: a three-component vector representing the normal after camera transformation. - * - `texCoord`: a two-component vector representing the texture coordinates. - * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). - * - * Return the modified object to update the vertex. - * - * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives the vertex object and should return it after making any changes. - * - * @example - *
- * - * let myShader; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify(() => { - * getCameraInputs(inputs => { - * // Move vertices in camera space based on their x position - * let t = uniformFloat(() => millis()); - * inputs.position.y += 30 * sin(inputs.position.x * 0.05 + t * 0.001); - * // Tint all vertices blue - * inputs.color.b = 1; - * return inputs; - * }); - * }); - * } - * function draw() { - * background(200); - * shader(myShader); - * noStroke(); - * fill('red'); - * sphere(50); - * } - * - *
- */ \ No newline at end of file diff --git a/src/webgl/index.js b/src/webgl/index.js index 7ba587b132..52292100e8 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -14,7 +14,7 @@ import shader from './p5.Shader'; import camera from './p5.Camera'; import texture from './p5.Texture'; import rendererGL from './p5.RendererGL'; -import shadergenerator from './ShaderGenerator'; +import strands from '../strands/p5.strands'; export default function(p5){ rendererGL(p5, p5.prototype); @@ -33,5 +33,5 @@ export default function(p5){ dataArray(p5, p5.prototype); shader(p5, p5.prototype); texture(p5, p5.prototype); - shadergenerator(p5, p5.prototype); + strands(p5, p5.prototype); } diff --git a/test/unit/math/atan.js b/test/unit/math/atan.js deleted file mode 100644 index 19a14e4664..0000000000 --- a/test/unit/math/atan.js +++ /dev/null @@ -1,33 +0,0 @@ -import trigonometry from '../../../src/math/trigonometry.js'; -import { assert } from 'chai'; - -suite('atan', function() { - const mockP5 = { - RADIANS: 'radians', - DEGREES: 'degrees', - _validateParameters: () => {} - }; - const mockP5Prototype = {}; - - beforeEach(function() { - mockP5Prototype._angleMode = mockP5.RADIANS; - mockP5Prototype.angleMode = function(mode) { - this._angleMode = mode; - }; - trigonometry(mockP5, mockP5Prototype); - }); - - test('should return the correct value for atan(0.5) in radians', function() { - mockP5Prototype.angleMode(mockP5.RADIANS); - const expected = 0.4636476090008061; // pre-calculated value - const actual = mockP5Prototype.atan(0.5); - assert.closeTo(actual, expected, 1e-10); - }); - - test('should return the correct value for atan(0.5) in degrees', function() { - mockP5Prototype.angleMode(mockP5.DEGREES); - const expected = 26.56505117707799; // pre-calculated value - const actual = mockP5Prototype.atan(0.5); - assert.closeTo(actual, expected, 1e-10); - }); -}); diff --git a/test/unit/math/trigonometry.js b/test/unit/math/trigonometry.js index b8c2c86e2a..62b8bbd532 100644 --- a/test/unit/math/trigonometry.js +++ b/test/unit/math/trigonometry.js @@ -7,11 +7,11 @@ suite('Trigonometry', function() { let y = 1; let ratio = 0.5; - const mockP5 = { - _validateParameters: vi.fn() - }; const mockP5Prototype = { }; + const mockP5 = Object.create(mockP5Prototype, { + _validateParameters: vi.fn() + }); beforeEach(async function() { trigonometry(mockP5, mockP5Prototype); @@ -47,7 +47,7 @@ suite('Trigonometry', function() { }); }; - suite.todo('p5.prototype.angleMode', function() { + suite('p5.prototype.angleMode', function() { test('should set constant to DEGREES', function() { mockP5Prototype.angleMode(mockP5.DEGREES); assert.equal(mockP5Prototype.angleMode(), mockP5.DEGREES); @@ -102,31 +102,31 @@ suite('Trigonometry', function() { }); }); - suite.todo('p5.prototype.asin', function() { + suite('p5.prototype.asin', function() { ahandleDegreesAndRadians('asin'); }); - suite.todo('p5.prototype.atan', function() { + suite('p5.prototype.atan', function() { ahandleDegreesAndRadians('atan'); }); - suite.todo('p5.prototype.acos', function() { + suite('p5.prototype.acos', function() { ahandleDegreesAndRadians('acos'); }); - suite.todo('p5.prototype.sin', function() { + suite('p5.prototype.sin', function() { handleDegreesAndRadians('sin'); }); - suite.todo('p5.prototype.cos', function() { + suite('p5.prototype.cos', function() { handleDegreesAndRadians('cos'); }); - suite.todo('p5.prototype.tan', function() { + suite('p5.prototype.tan', function() { handleDegreesAndRadians('tan'); }); - suite.todo('p5.prototype.atan2', function() { + suite('p5.prototype.atan2', function() { test('should handle degrees', function() { mockP5Prototype.angleMode(mockP5.DEGREES); assert.equal(