feat: add variadic render graph logic nodes

Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab

Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-07-29 10:03:06 +00:00
co-authored by heaust
parent 0e866596c0
commit 90406ae18f
34 changed files with 1031 additions and 273 deletions
+10 -8
View File
@@ -446,10 +446,10 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
linkSources.set(link.id, linkSource);
if (!link.muted) {
incoming.set(link.toSocketId, (incoming.get(link.toSocketId) ?? 0) + 1);
nodes.get(to.node).value.inputs[to.key] = {
(nodes.get(to.node).value.inputs[to.key] ??= []).push({
node: from.node,
socket: from.key,
};
});
}
}
const ordered = [...nodes.values()].sort((a, b) =>
@@ -548,13 +548,13 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
for (const key of Object.keys(
descriptors[item.value.executor.key].inputs,
)) {
const link = raw.links.find(
const links = raw.links.filter(
(x) =>
!x.muted &&
x.toNodeId === item.value.id &&
sockets.get(x.toSocketId)?.key === key,
);
const source = linkSources.get(link?.id) ?? {
const source = linkSources.get(links[0]?.id) ?? {
kind: "input",
nodeId: item.value.id,
input: key,
@@ -562,9 +562,11 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
unconnected: true,
};
paths[`${base}.inputs.${key}`] = source;
if (link) {
paths[`${base}.inputs.${key}.node`] = source;
paths[`${base}.inputs.${key}.socket`] = {
for (const [index, link] of links.entries()) {
const linkSource = linkSources.get(link.id);
paths[`${base}.inputs.${key}[${index}]`] = linkSource;
paths[`${base}.inputs.${key}[${index}].node`] = linkSource;
paths[`${base}.inputs.${key}[${index}].socket`] = {
kind: "socket",
nodeId: link.fromNodeId,
socketId: link.fromSocketId,
@@ -576,7 +578,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
paths[`${base}.inputs`] = nodeSource;
}
const ir = {
schemaVersion: 2,
schemaVersion: 3,
graphId: GRAPH_ID,
revision,
nodes: ordered.map((item) => item.value),
+14 -13
View File
@@ -1,9 +1,9 @@
export const GRAPH_ID = "authored_gpu_culling";
export const CATALOG_VERSION = 10;
export const CATALOG_VERSION = 11;
const exact = (type) => ({ kind: "exact", types: [type] });
const i = (type, required = true, authoringType, defaultPolicy = required ? "none" : "parameter_literal") => ({
const i = (type, minimum = 1, authoringType, defaultPolicy = minimum ? "none" : "parameter_literal", maximum = 1) => ({
accepted: typeof type === "string" ? exact(type) : type,
required,
cardinality: { minimum, maximum },
...(authoringType ? { authoringType } : {}),
defaultPolicy,
});
@@ -11,18 +11,18 @@ const o = (type) => ({ type });
const expression = (inputs, outputs) => ({
version: 1,
execution: "expression",
inputs: Object.fromEntries(Object.entries(inputs).map(([name, type]) => [name, i(type, false)])),
inputs: Object.fromEntries(Object.entries(inputs).map(([name, type]) => [name, i(type, 0)])),
outputs: Object.fromEntries(Object.entries(outputs).map(([name, type]) => [name, o(type)])),
parameters: {},
});
const numbered = (prefix, count, type) =>
Object.fromEntries(Array.from({ length: count }, (_, index) => [`${prefix}${index}`, type]));
const expressionCatalog = {
and: expression({ left: "bool", right: "bool" }, { value: "bool" }),
or: expression({ left: "bool", right: "bool" }, { value: "bool" }),
and: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } },
or: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } },
not: expression({ operand: "bool" }, { value: "bool" }),
xor: expression({ left: "bool", right: "bool" }, { value: "bool" }),
xnor: expression({ left: "bool", right: "bool" }, { value: "bool" }),
xor: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } },
xnor: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } },
greater_than_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
less_than_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
equals_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
@@ -112,9 +112,9 @@ export const semanticCatalog = Object.freeze({
execution: "render",
inputs: {
mesh: i("mesh_data"),
predicate: i("bool", false),
colorTarget: i("texture", false, undefined, "compiler_texture"),
depthTarget: i("texture", false, undefined, "compiler_texture"),
predicate: i("bool", 0),
colorTarget: i("texture", 0, undefined, "compiler_texture"),
depthTarget: i("texture", 0, undefined, "compiler_texture"),
},
outputs: { color: o("texture"), depth: o("texture") },
parameters: { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] },
@@ -271,11 +271,11 @@ export const styles = {
render: { header: "#426b43" },
frame: { header: "#a75d37" },
};
const socket = (title, direction, type, value = null) => ({
const socket = (title, direction, type, value = null, capacity = 1) => ({
title,
direction,
type,
maxIncomingLinks: direction === "input" ? 1 : 0,
maxIncomingLinks: direction === "input" ? capacity : 0,
visible: true,
value,
showValue: value !== null,
@@ -429,6 +429,7 @@ export const nodeDefinitions = Object.fromEntries(
"input",
v.authoringType ?? v.accepted.types[0],
v.defaultPolicy === "parameter_literal" ? socketDefault(v.accepted.types[0], defaultForInput(key, n, v.accepted.types[0])) : null,
v.cardinality.maximum,
),
]),
),
+4 -4
View File
@@ -16,10 +16,10 @@ async function seed(root) {
for (const [index, item] of culling.nodes.entries())
await root.dispatch({ type: "node.add", nodeId: item.id, nodeType: item.executor.key,
position: { x: 40 + (index % 6) * 280, y: 120 + Math.floor(index / 6) * 260 } });
const links = culling.nodes.flatMap((item) => Object.entries(item.inputs).map(([socket, from]) =>
[from.node, from.socket, item.id, socket]));
for (const [a, as, b, bs] of links) {
const id = `${a}_${as}_${b}_${bs}`;
const links = culling.nodes.flatMap((item) => Object.entries(item.inputs).flatMap(([socket, sources]) =>
sources.map((from, index) => [from.node, from.socket, item.id, socket, index])));
for (const [a, as, b, bs, index] of links) {
const id = `${a}_${as}_${b}_${bs}_${index}`;
await root.dispatch({
type: "link.add",
link: {
+8 -9
View File
@@ -1,6 +1,6 @@
import { descriptors } from "./catalog.js";
const input = (node, socket) => ({ node, socket });
const input = (node, socket) => [{ node, socket }];
const node = (id, key, parameters = {}, inputs = {}) => ({
id, state: "enabled", executor: { key, version: descriptors[key].version }, parameters, inputs,
});
@@ -17,20 +17,19 @@ const predicates = (withCulling = false) => {
const result = [
node("type_words", "separate_u32x16", { valueDefault: Array(16).fill(0) }, { value: input("mesh", "type") }),
node("type_bits", "separate_u32_bits", { valueDefault: 0 }, { value: input("type_words", "word0") }),
node("ground_class", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit1") }),
node("visible_pbr", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit2") }),
node("ground_class", "and", {}, { inputs: [...input("type_bits", "bit0"), ...input("type_bits", "bit1")] }),
node("not_double", "not", { operandDefault: false }, { operand: input("type_bits", "bit3") }),
node("standard_class", "and", { leftDefault: true, rightDefault: true }, { left: input("visible_pbr", "value"), right: input("not_double", "value") }),
node("double_class", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit3") }),
node("standard_class", "and", {}, { inputs: [...input("type_bits", "bit0"), ...input("type_bits", "bit2"), ...input("not_double", "value")] }),
node("double_class", "and", {}, { inputs: [...input("type_bits", "bit0"), ...input("type_bits", "bit3")] }),
];
if (!withCulling) return { nodes: result, classes: { ground: "ground_class", pbr: "standard_class", pbr_double: "double_class" } };
result.push(
node("cull", "frustum_cull", { camera: "active" }, { mesh: input("mesh", "mesh"), localAabb: input("mesh", "localAabb") }),
node("not_culled", "not", { operandDefault: false }, { operand: input("cull", "isFrustumCulled") }),
...[["ground", "ground_class"], ["pbr", "standard_class"], ["pbr_double", "double_class"]].map(([name, classification]) =>
node(`${name}_final`, "and", { leftDefault: true, rightDefault: true }, { left: input(classification, "value"), right: input("not_culled", "value") })),
);
return { nodes: result, classes: { ground: "ground_final", pbr: "pbr_final", pbr_double: "pbr_double_final" } };
for (const id of ["ground_class", "standard_class", "double_class"])
result.find((item) => item.id === id).inputs.inputs.push(...input("not_culled", "value"));
return { nodes: result, classes: { ground: "ground_class", pbr: "standard_class", pbr_double: "double_class" } };
};
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1, withCulling = false) => {
const classification = predicates(withCulling);
@@ -45,7 +44,7 @@ const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1
node("pbr_double", "pipeline", { pipeline: "gltf_standard_double_sided", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr_double, "value"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
];
};
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 3, graphId, revision: 2, nodes });
const direct = (graphId, clearColor) => graph(graphId, [
node("ldr", "texture", texture("rgba8_unorm")),
...scene("ldr", clearColor),