Files
yawn/addons/handles/test/scene-graph.test.ts
T

276 lines
9.0 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { Mesh } from "../src/Mesh";
import { FXAA } from "../src/PostProcesses/FXAA";
import { PBRMaterial } from "../src/materials/PBRMaterial";
import { Texture } from "../src/materials/Texture";
import {
createScene,
detailedMesh,
forwardPasses,
latestGraph,
pbrPipelines,
renderPipeline,
testCore,
triangle,
} from "./scene-helpers";
if (typeof globalThis.ImageBitmap === "undefined")
Object.defineProperty(globalThis, "ImageBitmap", {
value: class ImageBitmap {},
});
test("empty scenes clear without drawing 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);
});
test("opaque PBR graph fuses the background and compacts sparse lights", async () => {
const scene = await createScene();
const material = new PBRMaterial(scene);
await material.ready;
const mesh = detailedMesh(scene, material);
await mesh.ready;
const graph = latestGraph(scene);
const pipeline = pbrPipelines(graph)[0];
const forward = forwardPasses(graph)[0];
const present = renderPipeline(graph, "present");
const clusters = graph.pipelines.compute.find(
(candidate: any) => candidate.id === "cluster-lights",
);
assert.equal(pipeline.primitive.frontFace, "ccw");
assert.equal(
graph.passes.some((pass: any) => pass.id === "studio-background"),
false,
);
assert.match(present.code, /fn studioBackground\(/u);
assert.deepEqual(forward.color[0].clear, [0, 0, 0, 0]);
assert.match(pipeline.code, /@interpolate\(flat\) material: u32/u);
assert.match(pipeline.code, /materials\[input\.material \* 3u\]/u);
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 spotCount = clusters\[511\]/u);
assert.match(pipeline.code, /spotCount <= 500u/u);
});
test("materials with identical shader features share one pipeline", async () => {
const scene = await createScene();
const firstMaterial = new PBRMaterial(scene, { roughness: 0.2 });
const secondMaterial = new PBRMaterial(scene, { roughness: 0.8 });
await Promise.all([firstMaterial.ready, secondMaterial.ready]);
const firstMesh = detailedMesh(scene, firstMaterial);
const secondMesh = detailedMesh(scene, secondMaterial);
await Promise.all([firstMesh.ready, secondMesh.ready]);
const graph = latestGraph(scene);
const passes = forwardPasses(graph);
assert.equal(pbrPipelines(graph).length, 1);
assert.equal(passes.length, 2);
assert.deepEqual(
passes.map((pass: any) => pass.draw.firstInstance & 0xffff),
[firstMesh.id, secondMesh.id],
);
assert.deepEqual(
passes.map((pass: any) => (pass.draw.firstInstance >>> 16) - 1),
[firstMaterial.id, secondMaterial.id],
);
});
test("transmission renders directly into a composited layer with five taps", async () => {
const scene = await createScene();
const material = new PBRMaterial(scene, {
transmission: 0.9,
thickness: 0.25,
attenuationDistance: 2,
});
await material.ready;
const mesh = detailedMesh(scene, material);
await mesh.ready;
const graph = latestGraph(scene);
const pipeline = pbrPipelines(graph)[0];
const present = renderPipeline(graph, "present");
const presentPass = graph.passes.find((pass: any) => pass.id === "present");
assert.equal(
graph.passes.some((pass: any) => pass.id === "studio-background"),
true,
);
assert.equal(
graph.passes.some((pass: any) => pass.id === "copy-scene-color"),
false,
);
assert.equal(
presentPass.bindings.some(
(binding: any) => binding.resource === "scene-color",
),
true,
);
assert.match(present.code, /textureSample\(scene, sourceSampler/u);
assert.equal(
pipeline.code.match(/textureSampleLevel\(sceneColor/g)?.length,
5,
);
assert.match(pipeline.code, /transmitted \*= pow/u);
});
test("post-processing keeps the conservative transmission copy", async () => {
const scene = await createScene();
const material = new PBRMaterial(scene, { transmission: 1 });
await material.ready;
const mesh = detailedMesh(scene, material);
await mesh.ready;
const effect = new FXAA(scene);
await effect.ready;
const graph = latestGraph(scene);
const presentPass = graph.passes.find((pass: any) => pass.id === "present");
assert.equal(
graph.passes.some((pass: any) => pass.id === "copy-scene-color"),
true,
);
assert.equal(
presentPass.bindings.some(
(binding: any) => binding.resource === "scene-color",
),
false,
);
assert.equal(
graph.passes.some((pass: any) => pass.pipeline.startsWith("post-fxaa-")),
true,
);
});
test("PBR generation eliminates inactive features and specializes active ones", async () => {
const scene = await createScene();
const textures = Array.from(
{ length: 13 },
() => new Texture(scene, { size: [1, 1, 1] }),
);
await Promise.all(textures.map((texture) => texture.ready));
const textureOptions = {
baseColorTexture: textures[0],
metallicRoughnessTexture: textures[1],
normalTexture: textures[2],
emissiveTexture: textures[3],
occlusionTexture: textures[4],
transmissionTexture: textures[5],
thicknessTexture: textures[6],
clearcoatTexture: textures[7],
iridescenceTexture: textures[8],
iridescenceThicknessTexture: textures[9],
anisotropyTexture: textures[10],
specularTexture: textures[11],
specularColorTexture: textures[12],
};
const active = new PBRMaterial(scene, {
...textureOptions,
transmission: 1,
thickness: 0.5,
attenuationDistance: 2,
clearcoat: 0.7,
sheenColor: [0.2, 0.3, 0.4],
iridescence: 0.8,
anisotropy: 0.6,
textureInfos: Array.from({ length: 13 }, (_, slot) => ({
texCoord: slot === 1 ? 1 : 0,
})),
});
const unlit = new PBRMaterial(scene, { ...textureOptions, flags: 8 });
await Promise.all([active.ready, unlit.ready]);
const activeMesh = detailedMesh(scene, active);
const unlitMesh = detailedMesh(scene, unlit);
await Promise.all([activeMesh.ready, unlitMesh.ready]);
const pipelines = pbrPipelines(latestGraph(scene));
const activeCode = pipelines.find((pipeline: any) =>
pipeline.code.includes("transmissionAmount"),
).code;
const unlitCode = pipelines.find((pipeline: any) =>
pipeline.code.includes("color = base.rgb + emissive"),
).code;
for (const binding of [
"baseTexture",
"materialTexture",
"normalTexture",
"emissiveTexture",
"occlusionTexture",
"transmissionTexture",
"thicknessTexture",
"clearcoatTexture",
"iridescenceTexture",
"iridescenceThicknessTexture",
"anisotropyTexture",
"specularTexture",
"specularColorTexture",
])
assert.match(activeCode, new RegExp(`var ${binding}: texture_2d`));
assert.match(
activeCode,
/textureSample\(baseTexture, materialSampler, input\.uv\)/u,
);
assert.match(
activeCode,
/textureSample\(materialTexture, materialSampler, input\.uv2\)/u,
);
assert.match(activeCode, /let anisotropyAmount/u);
assert.match(activeCode, /let iridescenceAmount/u);
assert.match(activeCode, /let clearcoatFresnel/u);
assert.match(activeCode, /let sheen =/u);
assert.match(unlitCode, /var baseTexture: texture_2d/u);
assert.match(unlitCode, /var emissiveTexture: texture_2d/u);
assert.doesNotMatch(unlitCode, /var normalTexture: texture_2d/u);
assert.doesNotMatch(unlitCode, /var clearcoatTexture: texture_2d/u);
assert.doesNotMatch(unlitCode, /let clearcoatFresnel/u);
});
test("alpha blending uses source-over and disables depth writes", async () => {
const scene = await createScene();
const material = new PBRMaterial(scene, { flags: 2 });
await material.ready;
const mesh = detailedMesh(scene, material);
await mesh.ready;
const pipeline = pbrPipelines(latestGraph(scene))[0];
assert.deepEqual(pipeline.fragment.targets[0].blend, {
color: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
alpha: {
srcFactor: "one",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
});
assert.equal(pipeline.depthStencil.depth_write_enabled, false);
});
test("batchGraphUpdates compiles once for multiple mesh registrations", async () => {
const scene = await createScene();
const material = new PBRMaterial(scene);
await material.ready;
const before = testCore(scene).graphs.length;
await scene.batchGraphUpdates(async () => {
const first = new Mesh(scene, { material, vertexData: triangle });
const second = new Mesh(scene, { material, vertexData: triangle });
await Promise.all([first.ready, second.ready]);
});
assert.equal(testCore(scene).graphs.length - before, 1);
assert.equal(forwardPasses(latestGraph(scene)).length, 2);
});