Add FxNode render graph playground
Add persistent render graph loadouts, grouped node authoring, code-appending apply behavior, explicit playground lighting, and resilient worker startup. 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:
@@ -193,15 +193,18 @@ test("Node, ArcRotateCamera, and light handles update their shared rows", async
|
||||
color: [0.2, 0.4, 0.6],
|
||||
intensity: 7,
|
||||
range: 8,
|
||||
radius: 2,
|
||||
});
|
||||
await light.ready;
|
||||
close(
|
||||
scene.array("pointLights").row(light.id),
|
||||
[0.2, 0.4, 0.6, 7, 8, 1, 0, 0],
|
||||
[0.2, 0.4, 0.6, 7, 8, 1, 2, 0],
|
||||
);
|
||||
light.intensity = 9;
|
||||
light.range = 10;
|
||||
light.radius = 3;
|
||||
close(scene.array("pointLights").row(light.id).subarray(3, 5), [9, 10]);
|
||||
assert.equal(scene.array("pointLights").row(light.id)[6], 3);
|
||||
});
|
||||
|
||||
test("ArcRotateCamera pans one projected world pixel per pointer pixel", async () => {
|
||||
|
||||
@@ -20,16 +20,18 @@ if (typeof globalThis.ImageBitmap === "undefined")
|
||||
value: class ImageBitmap {},
|
||||
});
|
||||
|
||||
test("empty scenes clear without drawing placeholder geometry", async () => {
|
||||
test("empty scenes use neutral authored defaults without placeholder geometry", async () => {
|
||||
const scene = await createScene();
|
||||
const emptyPass = latestGraph(scene).passes.find(
|
||||
(pass: any) => pass.id === "forward-empty",
|
||||
);
|
||||
|
||||
assert.equal(emptyPass.draw.vertices, 0);
|
||||
assert.deepEqual(emptyPass.color[0].clear, [0, 0, 0, 1]);
|
||||
assert.deepEqual(Array.from(scene.array("sceneAccent").row(0)), [1, 1, 1, 1]);
|
||||
});
|
||||
|
||||
test("opaque PBR graph fuses the background and compacts sparse lights", async () => {
|
||||
test("opaque PBR graph has no implicit environment and compacts sparse lights", async () => {
|
||||
const scene = await createScene();
|
||||
const material = new PBRMaterial(scene);
|
||||
await material.ready;
|
||||
@@ -46,19 +48,35 @@ test("opaque PBR graph fuses the background and compacts sparse lights", async (
|
||||
|
||||
assert.equal(pipeline.primitive.frontFace, "ccw");
|
||||
assert.equal(
|
||||
graph.passes.some((pass: any) => pass.id === "studio-background"),
|
||||
graph.passes.some((pass: any) => pass.id.includes("background")),
|
||||
false,
|
||||
);
|
||||
assert.match(present.code, /fn studioBackground\(/u);
|
||||
assert.deepEqual(forward.color[0].clear, [0, 0, 0, 0]);
|
||||
assert.doesNotMatch(JSON.stringify(graph), /studio/iu);
|
||||
assert.deepEqual(forward.color[0].clear, [0, 0, 0, 1]);
|
||||
assert.match(pipeline.code, /@interpolate\(flat\) material: u32/u);
|
||||
assert.match(pipeline.code, /materials\[input\.material \* 3u\]/u);
|
||||
assert.doesNotMatch(pipeline.code, /defaultLight|studioEnvironment/u);
|
||||
assert.doesNotMatch(pipeline.code, /0\.4, 0\.7, 0\.6/u);
|
||||
assert.match(
|
||||
pipeline.code,
|
||||
/let ambient = vec3<f32>\(bitcast<f32>\(clusters\[1012\]\)/u,
|
||||
);
|
||||
assert.match(pipeline.code, /fn directContribution/u);
|
||||
assert.match(pipeline.code, /fn directionalContribution/u);
|
||||
assert.equal(
|
||||
forward.bindings.find((binding: any) => binding.binding === 10)?.resource,
|
||||
"directional-lights",
|
||||
);
|
||||
|
||||
assert.match(clusters.code, /clusters\[11u \+ pointCount\] = i/u);
|
||||
assert.match(clusters.code, /clusters\[512u \+ spotCount\] = i/u);
|
||||
assert.match(pipeline.code, /let pointCount = clusters\[10\]/u);
|
||||
assert.match(pipeline.code, /pointCount <= 500u/u);
|
||||
assert.match(pipeline.code, /arrayLength\(&pointLights\) \/ 2u/u);
|
||||
assert.match(
|
||||
pipeline.code,
|
||||
/let sourceRoughness = clamp\(roughness \+ max\(state\.z/u,
|
||||
);
|
||||
assert.match(pipeline.code, /let spotCount = clusters\[511\]/u);
|
||||
assert.match(pipeline.code, /spotCount <= 500u/u);
|
||||
});
|
||||
@@ -103,9 +121,14 @@ test("transmission renders directly into a composited layer with five taps", asy
|
||||
const presentPass = graph.passes.find((pass: any) => pass.id === "present");
|
||||
|
||||
assert.equal(
|
||||
graph.passes.some((pass: any) => pass.id === "studio-background"),
|
||||
graph.passes.some((pass: any) => pass.id === "scene-color-clear"),
|
||||
true,
|
||||
);
|
||||
const sceneColorClear = graph.passes.find(
|
||||
(pass: any) => pass.id === "scene-color-clear",
|
||||
);
|
||||
assert.equal(sceneColorClear.draw.vertices, 0);
|
||||
assert.deepEqual(sceneColorClear.color[0].clear, [0, 0, 0, 1]);
|
||||
assert.equal(
|
||||
graph.passes.some((pass: any) => pass.id === "copy-scene-color"),
|
||||
false,
|
||||
@@ -124,6 +147,27 @@ test("transmission renders directly into a composited layer with five taps", asy
|
||||
assert.match(pipeline.code, /transmitted \*= pow/u);
|
||||
});
|
||||
|
||||
test("clear color is explicit scene data used by empty and forward graphs", async () => {
|
||||
const clearColor = [0.03, 0.06, 0.09, 1] as const;
|
||||
const scene = await createScene({ clearColor });
|
||||
const empty = latestGraph(scene).passes.find(
|
||||
(pass: any) => pass.id === "forward-empty",
|
||||
);
|
||||
assert.deepEqual(empty.color[0].clear, clearColor);
|
||||
|
||||
const material = new PBRMaterial(scene);
|
||||
await material.ready;
|
||||
const mesh = detailedMesh(scene, material);
|
||||
await mesh.ready;
|
||||
|
||||
const graph = latestGraph(scene);
|
||||
assert.deepEqual(forwardPasses(graph)[0].color[0].clear, clearColor);
|
||||
assert.deepEqual(
|
||||
graph.passes.find((pass: any) => pass.id === "present").color[0].clear,
|
||||
clearColor,
|
||||
);
|
||||
});
|
||||
|
||||
test("post-processing keeps the conservative transmission copy", async () => {
|
||||
const scene = await createScene();
|
||||
const material = new PBRMaterial(scene, { transmission: 1 });
|
||||
@@ -273,3 +317,35 @@ test("batchGraphUpdates compiles once for multiple mesh registrations", async ()
|
||||
assert.equal(testCore(scene).graphs.length - before, 1);
|
||||
assert.equal(forwardPasses(latestGraph(scene)).length, 2);
|
||||
});
|
||||
|
||||
test("render graph snapshots expose isolated scene-owned declarations", async () => {
|
||||
const scene = await createScene();
|
||||
const source: any = await scene.getRenderGraph();
|
||||
|
||||
assert.equal(source.id, "scene");
|
||||
assert.ok(
|
||||
source.resources.buffers.some((buffer: any) => buffer.id === "clusters"),
|
||||
);
|
||||
source.id = "mutated";
|
||||
source.resources.buffers.length = 0;
|
||||
|
||||
const next: any = await scene.getRenderGraph();
|
||||
assert.equal(next.id, "scene");
|
||||
assert.ok(
|
||||
next.resources.buffers.some((buffer: any) => buffer.id === "clusters"),
|
||||
);
|
||||
});
|
||||
|
||||
test("serialized render graphs compile into explicitly selectable loadouts", async () => {
|
||||
const scene = await createScene();
|
||||
const core = testCore(scene);
|
||||
const serialized = core.serializedGraphs.at(-1)!;
|
||||
const before = core.graphs.length;
|
||||
|
||||
const loadoutId = await scene.compileRenderGraph(serialized);
|
||||
await scene.setLoadout(loadoutId);
|
||||
|
||||
assert.equal(core.graphs.length, before + 1);
|
||||
assert.equal(loadoutId, `loadout-${before + 1}`);
|
||||
assert.equal(core.loadouts.at(-1), loadoutId);
|
||||
});
|
||||
|
||||
@@ -13,8 +13,10 @@ export const triangle = {
|
||||
indices: [0, 1, 2],
|
||||
};
|
||||
|
||||
export async function createScene() {
|
||||
const scene = new Scene({} as HTMLCanvasElement);
|
||||
export async function createScene(
|
||||
options: ConstructorParameters<typeof Scene>[1] = {},
|
||||
) {
|
||||
const scene = new Scene({} as HTMLCanvasElement, options);
|
||||
await scene.ready;
|
||||
return scene;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user