diff --git a/preview/global/sketch.js b/preview/global/sketch.js index b0cd6c8045..9887bf8d76 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,124 +1,32 @@ p5.disableFriendlyErrors = true; -function windowResized() { - resizeCanvas(windowWidth, windowHeight); -} - -let starShader; -let starStrokeShader; -let stars; -let originalFrameBuffer; -let pixellizeShader; -let fresnelShader; -let bloomShader; - -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); - let base = 1.0 - dot(n, v); - let fresnel = fresnelScale * pow(base, fresnelPower) + fresnelBias; - let col = mix([0, 0, 0], [1, .5, .7], fresnel); - inputs.color = [col, 1]; - return inputs; - }); -} - -function starShaderCallback() { - const time = uniformFloat(() => millis()); - const skyRadius = uniformFloat(1000); - - function rand2(st) { - return fract(sin(dot(st, [12.9898, 78.233])) * 43758.5453123); - } - - function semiSphere() { - let id = instanceID(); - let theta = rand2([id, 0.1234]) * TWO_PI; - let phi = rand2([id, 3.321]) * PI+time/10000; - let r = skyRadius; - r *= 1.5 * sin(phi); - let x = r * sin(phi) * cos(theta); - let y = r * 1.5 * cos(phi); - let z = r * sin(phi) * sin(theta); - return [x, y, z]; - } - - getWorldInputs((inputs) => { - inputs.position += semiSphere(); - return inputs; - }); - - getObjectInputs((inputs) => { - let scale = 1 + 0.1 * sin(time * 0.002 + instanceID()); - inputs.position *= scale; +function callback() { + const time = uniformFloat(() =>millis()*0.001) + // getFinalColor((col) => { + // return vec4(1,0,0,1).rgba; + // }); + + getWorldInputs(inputs => { + inputs.color = vec4(inputs.position, 1); + strandsIf(inputs.position === vec3(1), () => 0).Else() + inputs.position = inputs.position + sin(time) * 100; return inputs; }); } -function pixellizeShaderCallback() { - const pixelSize = uniformFloat(()=> width*.75); - getColor((input, canvasContent) => { - let coord = input.texCoord; - coord = floor(coord * pixelSize) / pixelSize; - let col = texture(canvasContent, coord); - return col; - }); -} - -function bloomShaderCallback() { - const preBlur = uniformTexture(() => originalFrameBuffer); - 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; - }); -} - async function setup(){ - createCanvas(windowWidth, windowHeight, WEBGL); - stars = buildGeometry(() => sphere(30, 4, 2)) - originalFrameBuffer = createFramebuffer(); + createCanvas(windowWidth,windowHeight, WEBGL) + bloomShader = baseColorShader().modify(callback); +} - starShader = baseMaterialShader().modify(starShaderCallback); - starStrokeShader = baseStrokeShader().modify(starShaderCallback) - fresnelShader = baseColorShader().modify(fresnelShaderCallback); - bloomShader = baseFilterShader().modify(bloomShaderCallback); - pixellizeShader = baseFilterShader().modify(pixellizeShaderCallback); +function windowResized() { + resizeCanvas(windowWidth, windowHeight); } -function draw(){ - originalFrameBuffer.begin(); - background(0); +function draw() { orbitControl(); - - push() - strokeWeight(4) - stroke(255,0,0) - rotateX(PI/2 + millis() * 0.0005); - fill(255,100, 150) - strokeShader(starStrokeShader) - shader(starShader); - model(stars, 2000); - pop() - - push() - shader(fresnelShader) - noStroke() - sphere(500); - pop() - filter(pixellizeShader); - - originalFrameBuffer.end(); - - imageMode(CENTER) - image(originalFrameBuffer, 0, 0) - - filter(BLUR, 20) - filter(bloomShader); + background(0); + shader(bloomShader); + noStroke(); + sphere(300) } diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js new file mode 100644 index 0000000000..45dcf9bd94 --- /dev/null +++ b/src/strands/ir_builders.js @@ -0,0 +1,429 @@ +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 { StrandsNode } from './strands_api'; +import { strandsBuiltinFunctions } from './strands_builtins'; + +////////////////////////////////////////////// +// Builders for node graphs +////////////////////////////////////////////// +export function createScalarLiteralNode(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, components: dimension }; +} + +export function createVariableNode(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, components: dimension }; +} + +export function createSwizzleNode(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; +} + +export function createBinaryOpNode(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, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, rightArg); + rightStrandsNode = new StrandsNode(id, components, 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 = createPrimitiveConstructorNode(strandsContext, cast.toType, leftStrandsNode); + const r = createPrimitiveConstructorNode(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 = createPrimitiveConstructorNode(strandsContext, cast.toType, cast.node); + if (cast.node === leftStrandsNode) { + finalLeftNodeID = casted.id; + } else { + finalRightNodeID = casted.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, components: nodeData.dimension }; +} + +export function createMemberAccessNode(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, components: memberTypeInfo.dimension }; +} + +export function createStructInstanceNode(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.name, + identifier, + dependsOn + }) + const structID = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, structID); + + return { id: structID, components: dependsOn }; +} + +function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { + dependsOn = 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 dependsOn.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, components } = createScalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); + mappedDependencies.push(id); + calculatedDimensions += 1; + continue; + } + else { + FES.userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); + } + } + // Sometimes, the dimension is undefined + 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 createPrimitiveConstructorNode(strandsContext, typeInfo, dependsOn) { + const { cfg, dag } = strandsContext; + 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, components: mappedDependencies }; +} + +export function createStructConstructorNode(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.name} 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.name, + dependsOn + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, components: structTypeInfo.components }; +} + +export function createFunctionCallNode(strandsContext, functionName, rawUserArgs) { + const { cfg, dag } = strandsContext; + const overloads = 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, components: { dependsOn, dimension: inferredReturnType.dimension } }; +} + +export function createUnaryOpNode(strandsContext, strandsNode, opCode) { + const { dag, cfg } = strandsContext; + const dependsOn = strandsNode.id; + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode, + dependsOn, + baseType: dag.baseTypes[strandsNode.id], + dimension: dag.dimensions[strandsNode.id], + }) + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return { id, components: {dep} }; +} + +export function createStatementNode(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; +} \ No newline at end of file diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js new file mode 100644 index 0000000000..a8dabf66ed --- /dev/null +++ b/src/strands/ir_cfg.js @@ -0,0 +1,73 @@ +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); +} \ 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..8cebf62b90 --- /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.swizzles ?? 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..724e2d4ea2 --- /dev/null +++ b/src/strands/ir_types.js @@ -0,0 +1,197 @@ +///////////////////// +// 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", +}; + +export const BasePriority = { + [BaseType.FLOAT]: 3, + [BaseType.INT]: 2, + [BaseType.BOOL]: 1, + [BaseType.MAT]: 0, + [BaseType.DEFER]: -1, +}; + +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 }, +} + +export const StructType = { + Vertex: { + name: 'Vertex', + properties: [ + { name: "position", dataType: DataType.float3 }, + { name: "normal", dataType: DataType.float3 }, + { name: "texCoord", dataType: DataType.float2 }, + { name: "color", dataType: DataType.float4 }, + ] + } +} + +export function isStructType(typeName) { + return Object.keys(StructType).includes(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, 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 = {}; + +for (const { symbol, opCode } of OperatorTable) { + // SymbolToOpCode[symbol] = opCode; + OpCodeToSymbol[opCode] = symbol; +} + +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]) +); \ No newline at end of file diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js new file mode 100644 index 0000000000..1ff28e02f7 --- /dev/null +++ b/src/strands/p5.strands.js @@ -0,0 +1,93 @@ +/** +* @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) { + ctx.dag = createDirectedAcyclicGraph(); + ctx.cfg = createControlFlowGraph(); + ctx.uniforms = []; + ctx.hooks = []; + ctx.backend = backend; + ctx.active = true; + ctx.previousFES = p5.disableFriendlyErrors; + p5.disableFriendlyErrors = true; + } + + function deinitStrandsContext(ctx) { + ctx.dag = createDirectedAcyclicGraph(); + ctx.cfg = createControlFlowGraph(); + ctx.uniforms = []; + ctx.hooks = []; + p5.disableFriendlyErrors = ctx.previousFES; + ctx.active = false; + } + + const strandsContext = {}; + initStrandsContext(strandsContext); + initGlobalStrandsAPI(p5, fn, strandsContext) + + ////////////////////////////////////////////// + // Entry Point + ////////////////////////////////////////////// + const oldModify = p5.Shader.prototype.modify; + + p5.Shader.prototype.modify = function(shaderModifier, options = { parser: true, srcLocations: false }) { + if (shaderModifier instanceof Function) { + // Reset the context object every time modify is called; + const backend = glslBackend; + initStrandsContext(strandsContext, glslBackend); + createShaderHooksFunctions(strandsContext, fn, this); + + // 1. Transpile from strands DSL to JS + let strandsCallback; + if (options.parser) { + strandsCallback = transpileStrandsToJS(shaderModifier.toString(), options.srcLocations); + } 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); + console.log(hooksObject); + console.log(hooksObject['Vertex getWorldInputs']); + + // 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) +} 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..85c0f094c7 --- /dev/null +++ b/src/strands/strands_api.js @@ -0,0 +1,373 @@ +import { + createBinaryOpNode, + createFunctionCallNode, + createVariableNode, + createStatementNode, + createPrimitiveConstructorNode, + createUnaryOpNode, + createStructInstanceNode, + createStructConstructorNode, + createSwizzleNode, +} 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' + +////////////////////////////////////////////// +// User nodes +////////////////////////////////////////////// +const swizzlesSet = new Set(); + +export class StrandsNode { + constructor(id, dimension, strandsContext) { + this.id = id; + this.strandsContext = strandsContext; + this.dimension = dimension; + installSwizzlesForDimension.call(this, strandsContext, dimension) + } +} + +function generateSwizzles(chars, maxLen = 4) { + const result = []; + + function build(current) { + if (current.length > 0) result.push(current); + if (current.length === maxLen) return; + + for (let c of chars) { + build(current + c); + } + } + + build(''); + return result; +} + +function installSwizzlesForDimension(strandsContext, dimension) { + if (swizzlesSet.has(dimension)) return; + swizzlesSet.add(dimension); + + const swizzleVariants = [ + ['x', 'y', 'z', 'w'], + ['r', 'g', 'b', 'a'], + ['s', 't', 'p', 'q'] + ].map(chars => chars.slice(0, dimension)); + + const descriptors = {}; + + for (const variant of swizzleVariants) { + const swizzleStrings = generateSwizzles(variant); + for (const swizzle of swizzleStrings) { + if (swizzle.length < 1 || swizzle.length > 4) continue; + if (descriptors[swizzle]) continue; + + const hasDuplicates = new Set(swizzle).size !== swizzle.length; + + descriptors[swizzle] = { + get() { + const id = createSwizzleNode(strandsContext, this, swizzle); + return new StrandsNode(id, 0, strandsContext); + }, + ...(hasDuplicates ? {} : { + set(value) { + return assignSwizzleNode(strandsContext, this, swizzle, value); + } + }) + }; + } + } + + Object.defineProperties(this, descriptors); +} + +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, components } = createBinaryOpNode(strandsContext, this, right, opCode); + return new StrandsNode(id, components, strandsContext); + }; + } + if (arity === 'unary') { + fn[name] = function (strandsNode) { + const { id, components } = createUnaryOpNode(strandsContext, strandsNode, opCode); + return new StrandsNode(id, components, strandsContext); + } + } + } + + ////////////////////////////////////////////// + // Unique Functions + ////////////////////////////////////////////// + fn.discard = function() { + createStatementNode(strandsContext, OpCode.ControlFlow.DISCARD); + } + + fn.strandsIf = function(conditionNode, ifBody) { + return new StrandsConditional(strandsContext, conditionNode, ifBody); + } + + fn.strandsLoop = function(a, b, loopBody) { + return null; + } + + fn.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, components } = createPrimitiveConstructorNode(strandsContext, { baseType: BaseType.DEFER, dimension: null }, args.flat()); + return new StrandsNode(id, components, 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, components } = createFunctionCallNode(strandsContext, functionName, args); + return new StrandsNode(id, components, strandsContext); + } else { + return originalFn.apply(this, args); + } + } + } else { + fn[functionName] = function (...args) { + if (strandsContext.active) { + const { id, components } = createFunctionCallNode(strandsContext, functionName, args); + return new StrandsNode(id, components, strandsContext); + } else { + p5._friendlyError( + `It looks like you've called ${functionName} outside of a shader's modify() function.` + ) + } + } + } + } + + // 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, components } = createVariableNode(strandsContext, typeInfo, name); + strandsContext.uniforms.push({ name, typeInfo, defaultValue }); + return new StrandsNode(id, components, strandsContext); + }; + + const originalp5Fn = fn[typeInfo.fnName]; + fn[typeInfo.fnName] = function(...args) { + if (strandsContext.active) { + const { id, components } = createPrimitiveConstructorNode(strandsContext, typeInfo, args); + return new StrandsNode(id, components, 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) { + const paramType = param.type; + if(isStructType(paramType.typeName)) { + const structType = StructType[paramType.typeName]; + const originalInstanceInfo = createStructInstanceNode(strandsContext, structType, param.name, []); + const structNode = new StrandsNode(originalInstanceInfo.id, 0, strandsContext); + // const componentNodes = originalInstanceInfo.components.map(id => new StrandsNode(id, components)) + + for (let i = 0; i < structType.properties.length; i++) { + const componentTypeInfo = structType.properties[i]; + Object.defineProperty(structNode, componentTypeInfo.name, { + get() { + const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]) + return new StrandsNode(propNode.id, propNode.dimension, strandsContext); + // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); + // const memberAccessNode = new StrandsNode(id, components); + // return memberAccessNode; + }, + set(val) { + const oldDependsOn = dag.dependsOn[structNode.id]; + const newDependsOn = [...oldDependsOn]; + + let newValueID; + if (val instanceof StrandsNode) { + newValueID = val.id; + } + else { + let newVal = createPrimitiveConstructorNode(strandsContext, componentTypeInfo.dataType, val); + newValueID = newVal.id; + } + + newDependsOn[i] = newValueID; + const newStructInfo = createStructInstanceNode(strandsContext, structType, param.name, newDependsOn); + structNode.id = newStructInfo.id; + } + }) + } + + args.push(structNode); + } + else /*if(isNativeType(paramType.typeName))*/ { + const typeInfo = TypeInfoFromGLSLName[paramType.typeName]; + const { id, components } = createVariableNode(strandsContext, typeInfo, param.name); + const arg = new StrandsNode(id, components, strandsContext); + args.push(arg); + } + } + return args; +} + +function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { + if (!(returned instanceof StrandsNode)) { + // try { + const result = createPrimitiveConstructorNode(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 ${hookType.name} when a ${expectedType.baseType + expectedType.dimension} was expected!`); + } + else { + const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + returnedNodeID = result.id; + } + } + else if (receivedType.baseType !== expectedType.baseType) { + const result = createPrimitiveConstructorNode(strandsContext, expectedType, returned); + returnedNodeID = result.id; + } + + return returnedNodeID; +} + +// TODO: track overridden functions and restore them + +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 = strandsContext.cfg; + + for (const hookType of hookTypes) { + fn[hookType.name] = 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.typeName]; + 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.`); + } + 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 = createStructConstructorNode(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); + } + } +} \ No newline at end of file diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js new file mode 100644 index 0000000000..e931b0b880 --- /dev/null +++ b/src/strands/strands_builtins.js @@ -0,0 +1,113 @@ +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: false}, + { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, + ], + 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: ["texture2D", 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..74c99bce0d --- /dev/null +++ b/src/strands/strands_codegen.js @@ -0,0 +1,35 @@ +export function generateShaderCode(strandsContext) { + const { cfg, backend } = 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, entryBlockID, rootNodeID} of strandsContext.hooks) { + const generationContext = { + indent: 1, + codeLines: [], + write(line) { + this.codeLines.push(' '.repeat(this.indent) + line); + }, + tempNames: {}, + declarations: [], + nextTempID: 0, + }; + + for (const blockID of cfg.blockOrder) { + backend.generateBlock(blockID, strandsContext, generationContext); + } + + const firstLine = backend.hookEntry(hookType); + backend.generateReturnStatement(strandsContext, generationContext, rootNodeID); + hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); + } + + return hooksObj; +} \ No newline at end of file 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..2cc119d3f3 --- /dev/null +++ b/src/strands/strands_glslBackend.js @@ -0,0 +1,206 @@ +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, StructType } 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; + 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) { + console.log("HELLO") + 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++}`; + console.log(expr); + 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) { + const dag = strandsContext.dag; + const rootNode = getNodeDataFromID(dag, rootNodeID); + if (isStructType(rootNode.baseType)) { + const structTypeInfo = StructType[rootNode.baseType]; + 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: + return node.value.toFixed(4); + + case NodeType.VARIABLE: + return node.identifier; + + case NodeType.OPERATION: + const useParantheses = node.usedBy.length > 0; + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { + if (node.dependsOn.length === 1 && node.dimension === 1) { + 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.dependsOn.length === 1) { + 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..b7e8e35f4f --- /dev/null +++ b/src/strands/strands_transpiler.js @@ -0,0 +1,218 @@ +import { parse } from 'acorn'; +import { ancestor } from 'acorn-walk'; +import escodegen from 'escodegen'; + +function replaceBinaryOperator(codeSource) { + switch (codeSource) { + case '+': return 'add'; + case '-': return 'sub'; + case '*': return 'mult'; + case '/': return 'div'; + case '%': return 'mod'; + case '==': + case '===': return 'equalTo'; + case '>': return 'greaterThan'; + case '>=': return 'greaterThanEqualTo'; + case '<': return 'lessThan'; + case '&&': return 'and'; + case '||': return 'or'; + } +} + +function ancestorIsUniform(ancestor) { + return ancestor.type === 'CallExpression' + && ancestor.callee?.type === 'Identifier' + && ancestor.callee?.name.startsWith('uniform'); +} + +const ASTCallbacks = { + UnaryExpression(node, _state, _ancestors) { + if (_ancestors.some(ancestorIsUniform)) { return; } + + const signNode = { + type: 'Literal', + value: node.operator, + } + + const standardReplacement = (node) => { + node.type = 'CallExpression' + node.callee = { + type: 'Identifier', + name: 'unaryNode', + } + node.arguments = [node.argument, signNode] + } + + if (node.type === 'MemberExpression') { + const property = node.argument.property.name; + const swizzleSets = [ + ['x', 'y', 'z', 'w'], + ['r', 'g', 'b', 'a'], + ['s', 't', 'p', 'q'] + ]; + + let isSwizzle = swizzleSets.some(set => + [...property].every(char => set.includes(char)) + ) && node.argument.type === 'MemberExpression'; + + if (isSwizzle) { + node.type = 'MemberExpression'; + node.object = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'unaryNode' + }, + arguments: [node.argument.object, signNode], + }; + node.property = { + type: 'Identifier', + name: property + }; + } else { + standardReplacement(node); + } + } else { + standardReplacement(node); + } + delete node.argument; + delete node.operator; + }, + VariableDeclarator(node, _state, _ancestors) { + if (node.init.callee && node.init.callee.name?.startsWith('uniform')) { + const uniformNameLiteral = { + type: 'Literal', + value: node.id.name + } + node.init.arguments.unshift(uniformNameLiteral); + } + if (node.init.callee && node.init.callee.name?.startsWith('varying')) { + const varyingNameLiteral = { + type: 'Literal', + value: node.id.name + } + node.init.arguments.unshift(varyingNameLiteral); + _state.varyings[node.id.name] = varyingNameLiteral; + } + }, + Identifier(node, _state, _ancestors) { + if (_state.varyings[node.name] + && !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) { + node.type = 'ExpressionStatement'; + node.expression = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: node.name + }, + property: { + type: 'Identifier', + name: 'getValue' + }, + }, + arguments: [], + } + } + }, + // The callbacks for AssignmentExpression and BinaryExpression handle + // operator overloading including +=, *= assignment expressions + ArrayExpression(node, _state, _ancestors) { + const original = JSON.parse(JSON.stringify(node)); + node.type = 'CallExpression'; + node.callee = { + type: 'Identifier', + name: 'strandsNode', + }; + node.arguments = [original]; + }, + AssignmentExpression(node, _state, _ancestors) { + if (node.operator !== '=') { + const methodName = replaceBinaryOperator(node.operator.replace('=','')); + const rightReplacementNode = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: node.left, + property: { + type: 'Identifier', + name: methodName, + }, + }, + arguments: [node.right] + } + node.operator = '='; + node.right = rightReplacementNode; + } + if (_state.varyings[node.left.name]) { + node.type = 'ExpressionStatement'; + node.expression = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: node.left.name + }, + property: { + type: 'Identifier', + name: 'bridge', + } + }, + arguments: [node.right], + } + } + }, + BinaryExpression(node, _state, _ancestors) { + // Don't convert uniform default values to node methods, as + // they should be evaluated at runtime, not compiled. + if (_ancestors.some(ancestorIsUniform)) { return; } + // If the left hand side of an expression is one of these types, + // we should construct a node from it. + const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; + if (unsafeTypes.includes(node.left.type)) { + const leftReplacementNode = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '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(sourceString, srcLocations) { + const ast = parse(sourceString, { + ecmaVersion: 2021, + locations: srcLocations + }); + ancestor(ast, ASTCallbacks, undefined, { varyings: {} }); + const transpiledSource = escodegen.generate(ast); + const strandsCallback = new Function( + transpiledSource + .slice( + transpiledSource.indexOf('{') + 1, + transpiledSource.lastIndexOf('}') + ).replaceAll(';', '') + ); + + console.log(transpiledSource); + return strandsCallback; + } + \ No newline at end of file diff --git a/src/webgl/ShaderGenerator.js b/src/webgl/ShaderGenerator.js.temp similarity index 99% rename from src/webgl/ShaderGenerator.js rename to src/webgl/ShaderGenerator.js.temp index 58b3c7cc34..998e19cee2 100644 --- a/src/webgl/ShaderGenerator.js +++ b/src/webgl/ShaderGenerator.js.temp @@ -1116,13 +1116,12 @@ function shadergenerator(p5, fn) { 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; @@ -1422,17 +1421,6 @@ function shadergenerator(p5, fn) { 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)); @@ -1488,6 +1476,17 @@ function shadergenerator(p5, fn) { }, }; + // 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', + }; + for (const glslType in GLSLTypesToIdentifiers) { // Generate uniform*() Methods for creating uniforms const typeIdentifier = GLSLTypesToIdentifiers[glslType]; @@ -1687,6 +1686,6 @@ function shadergenerator(p5, fn) { export default shadergenerator; -if (typeof p5 !== 'undefined') { - p5.registerAddon(shadergenerator) -} +// if (typeof p5 !== 'undefined') { +// p5.registerAddon(shadergenerator) +// } 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); }