80 lines
4.7 KiB
TypeScript
80 lines
4.7 KiB
TypeScript
import { SHADER_GRAPH_SCHEMA, type ShaderGraphIR, type ShaderLinkIR, type ShaderNodeIR, type ShaderNodeType, type ShaderSocketIR } from "./shader-graph";
|
|
|
|
const input = (id: string, name: string, dataType: ShaderSocketIR["dataType"], defaultValue?: ShaderSocketIR["defaultValue"]): ShaderSocketIR => ({ id, name, direction: "INPUT", dataType, ...(defaultValue === undefined ? {} : { defaultValue }) });
|
|
const output = (id: string, name: string, dataType: ShaderSocketIR["dataType"]): ShaderSocketIR => ({ id, name, direction: "OUTPUT", dataType });
|
|
const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
|
|
|
|
const nodeSockets = (type: ShaderNodeType): ShaderSocketIR[] => {
|
|
if (type === "RGB") return [output("Color", "Color", "COLOR")];
|
|
if (type === "VALUE") return [output("Value", "Value", "VALUE")];
|
|
if (type === "MATH") return [input("Value", "Value", "VALUE", 0), input("Value_001", "Value", "VALUE", 0), output("Value", "Value", "VALUE")];
|
|
if (type === "PRINCIPLED") return [input("Base Color", "Base Color", "COLOR", [0.8, 0.8, 0.8, 1]), input("Metallic", "Metallic", "VALUE", 0), input("Roughness", "Roughness", "VALUE", 0.4), output("BSDF", "BSDF", "SHADER")];
|
|
if (type === "MATERIAL_OUTPUT") return [input("Surface", "Surface", "SHADER")];
|
|
if (type === "IMAGE_TEXTURE") return [input("Vector", "Vector", "VECTOR", [0, 0, 0]), output("Color", "Color", "COLOR")];
|
|
return [];
|
|
};
|
|
|
|
function uniqueId(base: string, ids: ReadonlySet<string>): string {
|
|
let id = base;
|
|
let suffix = 2;
|
|
while (ids.has(id)) id = `${base}-${suffix++}`;
|
|
return id;
|
|
}
|
|
|
|
/** Build a valid bounded graph for the source-backed Principled writer. */
|
|
export const shaderGraphCommands = Object.freeze({
|
|
addNode(graph: ShaderGraphIR, type: ShaderNodeType, name = type): ShaderGraphIR {
|
|
const next = clone(graph);
|
|
const id = uniqueId(`node-${type.toLowerCase()}`, new Set(next.nodes.map((node) => node.id)));
|
|
const node: ShaderNodeIR = { id, type, name: name.trim() || type, sockets: nodeSockets(type), ...(type === "MATH" ? { properties: { operation: "ADD" } } : {}) };
|
|
next.nodes.push(node);
|
|
if (type === "MATERIAL_OUTPUT") next.outputNodeId = id;
|
|
return next;
|
|
},
|
|
removeNode(graph: ShaderGraphIR, nodeId: string): ShaderGraphIR {
|
|
const next = clone(graph);
|
|
next.nodes = next.nodes.filter((node) => node.id !== nodeId);
|
|
next.links = next.links.filter((link) => link.fromNodeId !== nodeId && link.toNodeId !== nodeId);
|
|
if (next.outputNodeId === nodeId) next.outputNodeId = next.nodes.find((node) => node.type === "MATERIAL_OUTPUT")?.id;
|
|
return next;
|
|
},
|
|
connect(graph: ShaderGraphIR, link: ShaderLinkIR): ShaderGraphIR {
|
|
const next = clone(graph);
|
|
next.links = next.links.filter((item) => !(item.toNodeId === link.toNodeId && item.toSocketId === link.toSocketId));
|
|
if (!next.links.some((item) => item.fromNodeId === link.fromNodeId && item.fromSocketId === link.fromSocketId && item.toNodeId === link.toNodeId && item.toSocketId === link.toSocketId)) next.links.push(link);
|
|
return next;
|
|
},
|
|
disconnect(graph: ShaderGraphIR, link: ShaderLinkIR): ShaderGraphIR {
|
|
const next = clone(graph);
|
|
next.links = next.links.filter((item) => !(item.fromNodeId === link.fromNodeId && item.fromSocketId === link.fromSocketId && item.toNodeId === link.toNodeId && item.toSocketId === link.toSocketId));
|
|
return next;
|
|
},
|
|
setSocketValue(graph: ShaderGraphIR, nodeId: string, socketId: string, value: ShaderSocketIR["defaultValue"]): ShaderGraphIR {
|
|
const next = clone(graph);
|
|
const node = next.nodes.find((candidate) => candidate.id === nodeId);
|
|
const target = node?.sockets.find((candidate) => candidate.id === socketId && candidate.direction === "INPUT");
|
|
if (target) target.defaultValue = value;
|
|
return next;
|
|
},
|
|
setPrincipledGraph(materialId: string, baseColor: readonly number[], roughness: number, metallic: number): ShaderGraphIR {
|
|
const principled = "shader:principled";
|
|
const materialOutput = "shader:material-output";
|
|
return {
|
|
schemaVersion: SHADER_GRAPH_SCHEMA,
|
|
id: `shader-graph:${materialId}`,
|
|
materialId,
|
|
nodes: [
|
|
{ id: principled, type: "PRINCIPLED", name: "Principled BSDF", sockets: [
|
|
input("Base Color", "Base Color", "COLOR", [...baseColor].slice(0, 4)),
|
|
input("Metallic", "Metallic", "VALUE", metallic),
|
|
input("Roughness", "Roughness", "VALUE", roughness),
|
|
output("BSDF", "BSDF", "SHADER"),
|
|
] },
|
|
{ id: materialOutput, type: "MATERIAL_OUTPUT", name: "Material Output", sockets: [input("Surface", "Surface", "SHADER")] },
|
|
],
|
|
links: [{ fromNodeId: principled, fromSocketId: "BSDF", toNodeId: materialOutput, toSocketId: "Surface" }],
|
|
outputNodeId: materialOutput,
|
|
};
|
|
},
|
|
} as const);
|