Expand render graph playground editing
Amp-Thread-ID: https://ampcode.com/threads/T-01a033d4-0e44-708e-af0e-6166283416ff Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import type {
|
||||
GraphSnapshot,
|
||||
ParameterValue,
|
||||
} from "fxnode";
|
||||
import { isPhysicalRenderGraph, reconstructRenderGraph } from "./physical";
|
||||
|
||||
type PlainObject = Record<string, any>;
|
||||
type GraphSource = GraphLayoutV2 | GraphSnapshot;
|
||||
@@ -113,22 +114,38 @@ function activeChain(layout: GraphSource): any[] {
|
||||
return chain;
|
||||
}
|
||||
|
||||
function debugShader(mode: string, detailed: boolean, node: any): string {
|
||||
function passResources(pass: PlainObject): string[] {
|
||||
return [
|
||||
...(pass.bindings ?? []),
|
||||
...(pass.color ?? []),
|
||||
...(pass.vertexBuffers ?? []),
|
||||
...(pass.depth ? [pass.depth] : []),
|
||||
...(pass.indexBuffer ? [pass.indexBuffer] : []),
|
||||
]
|
||||
.map((item: PlainObject) => item?.resource)
|
||||
.filter(
|
||||
(resource: unknown): resource is string => typeof resource === "string",
|
||||
);
|
||||
}
|
||||
|
||||
function debugShader(mode: string, normalBuffer: boolean, node: any): string {
|
||||
const contrast = Number(value(node, "contrast", 1.35));
|
||||
const clay = value<readonly number[]>(node, "color", [0.36, 0.68, 0.9, 1]);
|
||||
const usesNormals = mode === "normals" || mode === "unlit";
|
||||
const usesObjectId = mode === "object-id";
|
||||
const fragment =
|
||||
mode === "normals"
|
||||
? "return vec4<f32>(normalize(input.normal) * 0.5 + 0.5, 1.0);"
|
||||
: mode === "depth"
|
||||
? `let depth = pow(clamp(1.0 - input.position.z, 0.0, 1.0), ${Math.max(0.01, contrast).toFixed(4)}); return vec4<f32>(vec3<f32>(depth), 1.0);`
|
||||
? `let depth = pow(clamp(1.0 - input.position.z, 0.0, 1.0), ${(Math.max(0.01, contrast) * 0.12).toFixed(4)}); return vec4<f32>(vec3<f32>(depth), 1.0);`
|
||||
: mode === "unlit"
|
||||
? `let base = vec3<f32>(${Number(clay[0] ?? 0.36).toFixed(5)}, ${Number(clay[1] ?? 0.68).toFixed(5)}, ${Number(clay[2] ?? 0.9).toFixed(5)}); let light = 0.32 + max(dot(normalize(input.normal), normalize(vec3<f32>(0.35, 0.8, 0.45))), 0.0) * 0.68; return vec4<f32>(base * light, 1.0);`
|
||||
: "let seed = f32(input.objectId) * 0.61803398875; let color = 0.42 + 0.58 * cos(vec3<f32>(0.0, 2.1, 4.2) + seed * 6.28318); return vec4<f32>(color, 1.0);";
|
||||
return `
|
||||
struct VertexOutput {
|
||||
@invariant @builtin(position) position: vec4<f32>,
|
||||
@location(0) normal: vec3<f32>,
|
||||
@location(1) @interpolate(flat) objectId: u32,
|
||||
${usesNormals ? "@location(0) normal: vec3<f32>," : ""}
|
||||
${usesObjectId ? `@location(${usesNormals ? 1 : 0}) @interpolate(flat) objectId: u32,` : ""}
|
||||
}
|
||||
@group(0) @binding(0) var<storage, read> positions: array<vec4<f32>>;
|
||||
@group(0) @binding(1) var<storage, read> rotors: array<vec4<f32>>;
|
||||
@@ -140,7 +157,7 @@ fn rotate(rotor: vec4<f32>, point: vec3<f32>) -> vec3<f32> {
|
||||
}
|
||||
@vertex fn vertex(
|
||||
@location(0) point: vec3<f32>,
|
||||
${detailed ? "@location(1) localNormal: vec3<f32>," : ""}
|
||||
${normalBuffer ? "@location(1) localNormal: vec3<f32>," : ""}
|
||||
@builtin(instance_index) packed: u32,
|
||||
) -> VertexOutput {
|
||||
let instance = packed & 65535u;
|
||||
@@ -152,8 +169,8 @@ fn rotate(rotor: vec4<f32>, point: vec3<f32>) -> vec3<f32> {
|
||||
dot(cameraMatrices[2], world), dot(cameraMatrices[3], world));
|
||||
if (cameraMatrices[4].w == 0.0) { output.position = world; }
|
||||
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), output.position, meshInfo[instance * 4u + 2u] != 0u);
|
||||
output.normal = normalize(rotate(rotors[instance], ${detailed ? "localNormal / scales[instance].xyz" : "vec3<f32>(0.0, 0.0, 1.0)"}));
|
||||
output.objectId = instance;
|
||||
${usesNormals ? `output.normal = normalize(rotate(rotors[instance], ${normalBuffer ? "localNormal / scales[instance].xyz" : "vec3<f32>(0.0, 0.0, 1.0)"}));` : ""}
|
||||
${usesObjectId ? "output.objectId = instance;" : ""}
|
||||
return output;
|
||||
}
|
||||
@fragment fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||
@@ -170,6 +187,10 @@ function applyDebugMode(graph: PlainObject, node: any): void {
|
||||
const byId = new Map(
|
||||
renderPipelines.map((pipeline: any) => [pipeline.id, pipeline]),
|
||||
);
|
||||
const passesById = new Map(
|
||||
graph.passes.map((pass: PlainObject) => [pass.id, pass]),
|
||||
);
|
||||
const detachedComputePasses = new Set<string>();
|
||||
const generated = new Map<string, string>();
|
||||
for (const pass of graph.passes) {
|
||||
if (
|
||||
@@ -187,19 +208,20 @@ function applyDebugMode(graph: PlainObject, node: any): void {
|
||||
const format = original.fragment?.targets?.[0]?.format;
|
||||
if (typeof format !== "string")
|
||||
throw new Error(`Forward pipeline ${pass.pipeline} has no color target`);
|
||||
const key = `${mode}:${detailed}:${format}`;
|
||||
const normalBuffer = (mode === "normals" || mode === "unlit") && detailed;
|
||||
const key = `${mode}:${normalBuffer}:${format}`;
|
||||
let id = generated.get(key);
|
||||
if (!id) {
|
||||
id = `rg-${mode}-${detailed ? "detailed" : "basic"}`;
|
||||
id = `rg-${mode}-${normalBuffer ? "detailed" : "basic"}`;
|
||||
let suffix = 1;
|
||||
while (byId.has(id))
|
||||
id = `rg-${mode}-${detailed ? "detailed" : "basic"}-${suffix++}`;
|
||||
id = `rg-${mode}-${normalBuffer ? "detailed" : "basic"}-${suffix++}`;
|
||||
const pipeline = {
|
||||
id,
|
||||
code: debugShader(mode, detailed, node),
|
||||
code: debugShader(mode, normalBuffer, node),
|
||||
vertex: {
|
||||
entry: "vertex",
|
||||
buffers: original.vertex.buffers.slice(0, detailed ? 2 : 1),
|
||||
buffers: original.vertex.buffers.slice(0, normalBuffer ? 2 : 1),
|
||||
},
|
||||
fragment: { entry: "fragment", targets: [{ format }] },
|
||||
depthStencil: original.depthStencil ?? null,
|
||||
@@ -223,9 +245,37 @@ function applyDebugMode(graph: PlainObject, node: any): void {
|
||||
resource,
|
||||
}));
|
||||
pass.vertexBuffers = (pass.vertexBuffers ?? []).filter(
|
||||
(binding: any) => binding.slot === 0 || (detailed && binding.slot === 1),
|
||||
(binding: any) =>
|
||||
binding.slot === 0 || (normalBuffer && binding.slot === 1),
|
||||
);
|
||||
const used = new Set(passResources(pass));
|
||||
pass.after = (pass.after ?? []).filter((dependency: string) => {
|
||||
const candidate = passesById.get(dependency);
|
||||
if (
|
||||
candidate?.type !== "compute" ||
|
||||
passResources(candidate).some((resource) => used.has(resource))
|
||||
)
|
||||
return true;
|
||||
detachedComputePasses.add(dependency);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
const retainedDependencies = new Set(
|
||||
graph.passes.flatMap((pass: PlainObject) => pass.after ?? []),
|
||||
);
|
||||
graph.passes = graph.passes.filter((pass: PlainObject) => {
|
||||
if (
|
||||
!detachedComputePasses.has(pass.id) ||
|
||||
retainedDependencies.has(pass.id)
|
||||
)
|
||||
return true;
|
||||
const resources = new Set(passResources(pass));
|
||||
return graph.passes.some(
|
||||
(other: PlainObject) =>
|
||||
other !== pass &&
|
||||
passResources(other).some((resource) => resources.has(resource)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type Rgb = readonly [number, number, number];
|
||||
@@ -496,23 +546,25 @@ function applyEffects(graph: PlainObject, effects: any[]): void {
|
||||
let after = Array.isArray(present.after) ? [...present.after] : [];
|
||||
for (const [index, node] of effects.entries()) {
|
||||
const suffix = `${index}-${String(node.id).replace(/[^a-z0-9-]/gi, "-")}`;
|
||||
const output = `rg-effect-${suffix}`;
|
||||
const final = index === effects.length - 1;
|
||||
const output = final ? "canvas" : `rg-effect-${suffix}`;
|
||||
const pipeline = `rg-effect-pipeline-${suffix}`;
|
||||
const pass = `rg-effect-pass-${suffix}`;
|
||||
textures.push({
|
||||
id: output,
|
||||
format: sourceTexture.format,
|
||||
size: ["canvas", "canvas", 1],
|
||||
usage: ["render", "sampled"],
|
||||
transient: true,
|
||||
});
|
||||
if (!final)
|
||||
textures.push({
|
||||
id: output,
|
||||
format: sourceTexture.format,
|
||||
size: ["canvas", "canvas", 1],
|
||||
usage: ["render", "sampled"],
|
||||
transient: true,
|
||||
});
|
||||
pipelines.push({
|
||||
id: pipeline,
|
||||
code: effectShader(node),
|
||||
vertex: { entry: "vertex", buffers: [] },
|
||||
fragment: {
|
||||
entry: "fragment",
|
||||
targets: [{ format: sourceTexture.format }],
|
||||
targets: [{ format: final ? "canvas" : sourceTexture.format }],
|
||||
},
|
||||
});
|
||||
graph.passes.push({
|
||||
@@ -530,28 +582,101 @@ function applyEffects(graph: PlainObject, effects: any[]): void {
|
||||
input = output;
|
||||
after = [pass];
|
||||
}
|
||||
sourceBinding.resource = input;
|
||||
present.after = after;
|
||||
graph.passes = graph.passes.filter((pass: PlainObject) => pass !== present);
|
||||
}
|
||||
|
||||
function routeDebugModeToCanvas(graph: PlainObject, mode: string): void {
|
||||
const present = graph.passes?.find(
|
||||
(pass: PlainObject) =>
|
||||
pass.type === "render" &&
|
||||
pass.color?.some(
|
||||
(attachment: PlainObject) => attachment.resource === "canvas",
|
||||
),
|
||||
);
|
||||
const source = present?.bindings?.find(
|
||||
(binding: PlainObject) => binding.group === 0 && binding.binding === 0,
|
||||
)?.resource;
|
||||
if (!present || typeof source !== "string")
|
||||
throw new Error("Base render graph has no sampled presentation pass");
|
||||
const producers = graph.passes.filter(
|
||||
(pass: PlainObject) =>
|
||||
pass.type === "render" &&
|
||||
pass.color?.some(
|
||||
(attachment: PlainObject) => attachment.resource === source,
|
||||
) &&
|
||||
String(pass.pipeline).startsWith(`rg-${mode}-`),
|
||||
);
|
||||
if (!producers.length)
|
||||
throw new Error(`Debug mode ${mode} has no render passes`);
|
||||
const pipelines = new Map(
|
||||
(graph.pipelines?.render ?? []).map((pipeline: PlainObject) => [
|
||||
pipeline.id,
|
||||
pipeline,
|
||||
]),
|
||||
);
|
||||
for (const pass of producers) {
|
||||
for (const attachment of pass.color)
|
||||
if (attachment.resource === source) attachment.resource = "canvas";
|
||||
const pipeline = pipelines.get(pass.pipeline);
|
||||
const targets = pipeline?.fragment?.targets;
|
||||
if (!Array.isArray(targets) || !targets.length)
|
||||
throw new Error(`Debug pipeline ${String(pass.pipeline)} has no target`);
|
||||
targets[0].format = "canvas";
|
||||
}
|
||||
graph.passes = graph.passes.filter((pass: PlainObject) => pass !== present);
|
||||
}
|
||||
|
||||
function pruneDeclarations(graph: PlainObject): void {
|
||||
const usedResources = new Set(
|
||||
(graph.passes ?? []).flatMap((pass: PlainObject) => passResources(pass)),
|
||||
);
|
||||
for (const kind of ["buffers", "textures", "samplers"])
|
||||
if (Array.isArray(graph.resources?.[kind]))
|
||||
graph.resources[kind] = graph.resources[kind].filter(
|
||||
(resource: PlainObject) => usedResources.has(resource.id),
|
||||
);
|
||||
const renderPipelines = new Set(
|
||||
(graph.passes ?? [])
|
||||
.filter((pass: PlainObject) => pass.type === "render")
|
||||
.map((pass: PlainObject) => pass.pipeline),
|
||||
);
|
||||
const computePipelines = new Set(
|
||||
(graph.passes ?? [])
|
||||
.filter((pass: PlainObject) => pass.type === "compute")
|
||||
.map((pass: PlainObject) => pass.pipeline),
|
||||
);
|
||||
if (Array.isArray(graph.pipelines?.render))
|
||||
graph.pipelines.render = graph.pipelines.render.filter(
|
||||
(pipeline: PlainObject) => renderPipelines.has(pipeline.id),
|
||||
);
|
||||
if (Array.isArray(graph.pipelines?.compute))
|
||||
graph.pipelines.compute = graph.pipelines.compute.filter(
|
||||
(pipeline: PlainObject) => computePipelines.has(pipeline.id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles an FxNode loadout over a scene-authored graph and returns the exact
|
||||
* S-expression consumed by `YawnCore.compileGraph`.
|
||||
* Resolves an FxNode loadout into the concrete graph that Yawn will compile.
|
||||
*/
|
||||
export function compileFxNodeRenderGraph(
|
||||
export function buildFxNodeRenderGraph(
|
||||
layout: GraphSource,
|
||||
baseGraph: object,
|
||||
): string {
|
||||
): object {
|
||||
if (!plainRecord(baseGraph))
|
||||
throw new TypeError("Base render graph must be plain data");
|
||||
if (isPhysicalRenderGraph(layout)) return reconstructRenderGraph(layout);
|
||||
const graph = structuredClone(baseGraph) as PlainObject;
|
||||
const chain = activeChain(layout);
|
||||
const mode = chain.findLast((node) => modeTypes.has(String(node.typeId)));
|
||||
if (mode) applyDebugMode(graph, mode);
|
||||
applyEffects(
|
||||
graph,
|
||||
chain.filter((node) => effectTypes.has(String(node.typeId))),
|
||||
);
|
||||
const effects = chain.filter((node) => effectTypes.has(String(node.typeId)));
|
||||
if (mode && mode.typeId !== "yawn.rg.mode.beauty" && effects.length === 0)
|
||||
routeDebugModeToCanvas(
|
||||
graph,
|
||||
String(mode.typeId).slice("yawn.rg.mode.".length),
|
||||
);
|
||||
else applyEffects(graph, effects);
|
||||
pruneDeclarations(graph);
|
||||
const suffix = chain
|
||||
.filter(
|
||||
(node) =>
|
||||
@@ -562,5 +687,16 @@ export function compileFxNodeRenderGraph(
|
||||
.map((stage) => (stage === "beauty" ? "standard" : stage))
|
||||
.join("-");
|
||||
graph.id = `${String(graph.id || "scene")}-fxnode-${suffix || "standard"}`;
|
||||
return encodeRenderGraph(graph);
|
||||
return graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles an FxNode loadout over a scene-authored graph and returns the exact
|
||||
* S-expression consumed by `YawnCore.compileGraph`.
|
||||
*/
|
||||
export function compileFxNodeRenderGraph(
|
||||
layout: GraphSource,
|
||||
baseGraph: object,
|
||||
): string {
|
||||
return encodeRenderGraph(buildFxNodeRenderGraph(layout, baseGraph));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user