Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/graph/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AstNode, FunctionNode, Program } from '@shaderfrog/glsl-parser/ast';
import {
Engine,
EngineContext,
EngineNodeType,
extendNodeContext,
extendNodesContext,
} from '../engine';
Expand Down Expand Up @@ -79,6 +80,21 @@ const computeNodeContext = async (
graph: Graph,
node: SourceNode
): Promise<NodeContext | NodeErrors> => {
// Three engine nodes have no GLSL source — return a minimal empty context.
// Their compilation is handled via onBeforeCompile on the material (threngine.ts).
if (
(node as CodeNode).engine &&
[EngineNodeType.physical, EngineNodeType.phong, EngineNodeType.toon].includes(
node.type as EngineNodeType
)
) {
return {
ast: { type: 'program', program: [], scopes: [] } as Program,
inputFillers: {},
inputs: node.inputs,
};
}

// THIS DUPLICATES OTHER LINE
const parser = {
...(coreParsers[node.type] || coreParsers[NodeType.SOURCE]),
Expand Down
86 changes: 83 additions & 3 deletions src/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
renameFunctions,
} from '@shaderfrog/glsl-parser/parser/utils';
import { Program } from '@shaderfrog/glsl-parser/ast';
import { Engine, EngineContext } from '../engine';
import { Engine, EngineContext, EngineNodeType } from '../engine';
import {
NodeContext,
NodeContexts,
Expand All @@ -15,6 +15,7 @@ import {
shaderSectionsCons,
findShaderSections,
mergeShaderSections,
filterSections,
ShaderSections,
shaderSectionsToProgram,
} from './shader-sections';
Expand Down Expand Up @@ -538,6 +539,17 @@ export const compileNode = (
continuation = mergeShaderSections(continuation, inputSections);
compiledIds = { ...compiledIds, ...childIds };

// Three engine nodes have no AST to fill — user GLSL accumulates in
// continuation above and is injected via onBeforeCompile at material time.
if (
(codeNode as CodeNode).engine &&
[EngineNodeType.physical, EngineNodeType.phong, EngineNodeType.toon].includes(
node.type as EngineNodeType
)
) {
return;
}

// Continue on if there's no context I don't know what case causes this
if (!nodeContext) {
return;
Expand Down Expand Up @@ -615,13 +627,24 @@ export const compileNode = (
return [sections, filler, { ...compiledIds, [node.id]: node }];
};

export type ThreeNodeFiller = {
fillerName: string; // e.g. 'filler_map', 'filler_position'
fromNodeId: string; // upstream user node feeding this filler
};

export type ThreeNodeInfo = {
nodeType: EngineNodeType;
connectedFillers: ThreeNodeFiller[];
};

export type CompileGraphResult = {
fragment: ShaderSections;
vertex: ShaderSections;
outputFrag: GraphNode;
outputVert: GraphNode;
orphanNodes: GraphNode[];
activeNodeIds: Set<string>;
threeNode?: ThreeNodeInfo;
};

export const compileGraph = (
Expand Down Expand Up @@ -679,6 +702,46 @@ export const compileGraph = (
outputVert
);

// Find connected Three engine nodes and their active fillers for onBeforeCompile
const threeEngineNodeTypes = [
EngineNodeType.physical,
EngineNodeType.phong,
EngineNodeType.toon,
];
const threeEngineNodes = graph.nodes.filter(
(n): n is SourceNode =>
(n as CodeNode).engine === true &&
threeEngineNodeTypes.includes(n.type as EngineNodeType)
);

let threeNode: ThreeNodeInfo | undefined;
if (threeEngineNodes.length > 0) {
const nodeType = threeEngineNodes[0].type as EngineNodeType;
const connectedFillers: ThreeNodeFiller[] = [];

for (const engineNode of threeEngineNodes) {
for (const edge of graph.edges.filter((e) => e.to === engineNode.id)) {
const input = engineNode.inputs.find((i) => i.id === edge.input);
if (!input) continue;

if (input.type === 'filler') {
// namedAttributeStrategy inputs (e.g. filler_position)
connectedFillers.push({ fillerName: input.id, fromNodeId: edge.from });
} else if (input.type === 'property' && input.bakeable) {
// Bakeable property inputs (e.g. map → filler_map)
const prop = (engineNode as CodeNode).config.properties?.find(
(p) => p.property === input.property
);
if (prop?.fillerName) {
connectedFillers.push({ fillerName: prop.fillerName, fromNodeId: edge.from });
}
}
}
}

threeNode = { nodeType, connectedFillers };
}

// Every compileNode returns the AST so far, as well as the filler for the
// next node with inputs. On the final step, we discard the filler
return {
Expand All @@ -692,6 +755,7 @@ export const compileGraph = (
...Object.keys(fragmentIds),
...orphanNodes.map((node) => node.id),
]),
threeNode,
};
};

Expand Down Expand Up @@ -764,11 +828,27 @@ export const compileSource = async (

const compileResult = compileGraph(updatedContext, engine, graph);

// For Three engine node graphs, filter out the output node's void main()
// so fragmentResult/vertexResult contain only user-authored GLSL functions.
// createMaterial injects these into Three's shader via onBeforeCompile.
const fragSections = compileResult.threeNode
? filterSections(
(s) => s.nodeId !== compileResult.outputFrag.id,
compileResult.fragment
)
: compileResult.fragment;
const vertSections = compileResult.threeNode
? filterSections(
(s) => s.nodeId !== compileResult.outputVert.id,
compileResult.vertex
)
: compileResult.vertex;

const fragmentResult = generate(
shaderSectionsToProgram(compileResult.fragment, engine.mergeOptions).program
shaderSectionsToProgram(fragSections, engine.mergeOptions).program
);
const vertexResult = generate(
shaderSectionsToProgram(compileResult.vertex, engine.mergeOptions).program
shaderSectionsToProgram(vertSections, engine.mergeOptions).program
);

const dataInputs = filterGraphNodes(
Expand Down
Loading
Loading