feat: add color grading graph 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-28 14:18:04 +00:00
co-authored by heaust
parent 67cf3a6555
commit 972fadc635
14 changed files with 648 additions and 16 deletions
+4 -4
View File
@@ -10,13 +10,13 @@ import {
spawnRequestedNode,
} from "../static/render-graph/node-spawn.js";
test("add-node model contains all 13 catalog types in application groups", () => {
assert.equal(addNodeItems.length, 13);
test("add-node model contains all 17 catalog types in application groups", () => {
assert.equal(addNodeItems.length, 17);
assert.deepEqual(
[...new Set(addNodeItems.map((item) => item.group))],
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
);
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 13);
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 17);
assert.deepEqual(
searchAddNodeItems("tone render").map((item) => item.typeId),
["tone_map"],
@@ -40,7 +40,7 @@ test("allocator avoids existing and session-reserved IDs and is bounded", () =>
);
});
test("all 13 types spawn with exact position, current version and generated ID", async () => {
test("all 17 types spawn with exact position, current version and generated ID", async () => {
let revision = 5,
expectedType;
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
+2 -2
View File
@@ -36,8 +36,8 @@ test("production render graph composition passes fxnode's public validator", asy
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
);
assert.equal(fxNodeComposition.schemaVersion, 2);
assert.equal(fxNodeComposition.version, 4);
assert.equal(Object.keys(fxNodeComposition.nodes).length, 13);
assert.equal(fxNodeComposition.version, 5);
assert.equal(Object.keys(fxNodeComposition.nodes).length, 17);
assert.ok(
Object.values(fxNodeComposition.nodes).every(
(definition) => definition.migrations.length === 0,
+25 -1
View File
@@ -111,6 +111,10 @@ test("catalog exhaustively mirrors all current contracts", () => {
"pipeline",
"fullscreen_copy",
"tone_map",
"color_balance",
"exposure_contrast",
"saturation",
"channel_mixer",
"bloom_extract",
"bloom_blur",
"bloom_composite",
@@ -130,7 +134,7 @@ test("catalog exhaustively mirrors all current contracts", () => {
Object.keys(contract.parameters).sort(),
key,
);
assert.equal(CATALOG_VERSION, 4);
assert.equal(CATALOG_VERSION, 5);
assert.deepEqual(nodeDefinitions.pipeline.parameters, {
pipeline: {
type: "string",
@@ -175,6 +179,26 @@ test("catalog exhaustively mirrors all current contracts", () => {
value: true,
});
assert.equal(nodeDefinitions.mesh_query.sockets.isVisible.showValue, true);
assert.deepEqual(nodeDefinitions.color_balance.ui.slice(0, 4), [
{ kind: "parameter", parameter: "mode" },
{ kind: "widget", widget: "grading-wheels", bindings: [
{ title: "Lift", scalar: "lift", color: "liftColor" },
{ title: "Gamma", scalar: "gamma", color: "gammaColor" },
{ title: "Gain", scalar: "gain", color: "gainColor" },
], visibleWhen: { parameter: "mode", equals: "lift_gamma_gain" } },
{ kind: "widget", widget: "grading-wheels", bindings: [
{ title: "Offset", scalar: "offset", color: "offsetColor" },
{ title: "Power", scalar: "power", color: "powerColor" },
{ title: "Slope", scalar: "slope", color: "slopeColor" },
], visibleWhen: { parameter: "mode", equals: "offset_power_slope" } },
{ kind: "parameter", parameter: "factor" },
]);
for (const name of ["liftColor", "gammaColor", "gainColor", "offsetColor", "powerColor", "slopeColor"])
assert.deepEqual(nodeDefinitions.color_balance.parameters[name].default, { kind: "color", value: [1, 1, 1, 1] });
assert.deepEqual(nodeDefinitions.channel_mixer.parameters.redOutput, {
type: "vector", default: { kind: "vector", value: [1, 0, 0] }, minimum: -2, maximum: 2,
});
});
test("adapter validates and exactly lowers canonical pipeline controls and blur direction", () => {
+20 -1
View File
@@ -9,6 +9,7 @@ const order = [
"hdr",
"culling",
"tone",
"grading",
"edges",
"bloom",
"combined",
@@ -72,6 +73,12 @@ const sequences = {
["tone", "tone_map"],
["frame_out", "frame_out"],
],
grading: [
["balance_hdr", "texture"], ["exposure_hdr", "texture"], ["saturation_hdr", "texture"], ["mixer_hdr", "texture"], ["ldr", "texture"],
["hdr", "texture"], ["depth", "texture"], ["mesh", "mesh"], ["query", "mesh_query"], ["registry", "pipeline_registry"],
["ground", "pipeline"], ["pbr", "pipeline"], ["pbr_double", "pipeline"], ["balance", "color_balance"], ["exposure", "exposure_contrast"],
["saturation", "saturation"], ["mixer", "channel_mixer"], ["tone", "tone_map"], ["frame_out", "frame_out"],
],
edges: [
["ldr", "texture"],
["edge_hdr", "texture"],
@@ -143,6 +150,7 @@ test("presets have the exact canonical pipeline identities, schemas, and node se
"preset_hdr_fullscreen",
"preset_gpu_culling",
"preset_tone",
"preset_grading",
"preset_edges",
"preset_bloom",
"preset_combined",
@@ -175,6 +183,16 @@ test("presets have the exact canonical pipeline identities, schemas, and node se
),
);
}
const grading = presets.grading;
assert.deepEqual(grading.nodes.find((n) => n.id === "balance").parameters, {
mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1],
gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1],
offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1],
slope: 1, slopeColor: [1,1,1,1],
});
assert.deepEqual(grading.nodes.find((n) => n.id === "exposure").parameters, { exposureStops: 0, contrast: 1, pivot: .18, factor: 1 });
assert.deepEqual(grading.nodes.find((n) => n.id === "saturation").parameters, { saturation: 1, factor: 1 });
assert.deepEqual(grading.nodes.find((n) => n.id === "mixer").parameters, { redOutput: [1,0,0], greenOutput: [0,1,0], blueOutput: [0,0,1], factor: 1 });
});
test("presets preserve common mesh, texture, query, pipeline, culling and post wiring", () => {
@@ -264,12 +282,13 @@ test("presets preserve common mesh, texture, query, pipeline, culling and post w
node: "cull",
socket: "isFrustumCulled",
});
for (const name of ["tone", "edges", "bloom", "combined"])
for (const name of ["tone", "grading", "edges", "bloom", "combined"])
assert.ok(!presets[name].nodes.some((node) => node.id === "copy"));
const finalSource = {
hdr: "pbr_double",
culling: "pbr_double",
tone: "tone",
grading: "tone",
edges: "tone",
bloom: "tone",
combined: "tone",