feat(web): complete PBR shader, asset library and offscreen render chain
- generate a desktop-Blender PBR asset library and seed catalog v2 into OPFS - add React asset browser, material preview sphere and Web PBR render panel - extend shader graph/compiler to the finite 14-node set and native properties - add image colour-space editing, scene-camera still rendering and asset repair - add focused E2E/unit coverage and implementation status notes Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ const packageJson = JSON.parse(
|
||||
|
||||
test("browser runtime dependencies stay local", () => {
|
||||
const runtimePackages = Object.keys(packageJson.dependencies ?? {});
|
||||
assert.deepEqual([...runtimePackages].sort(), ["@ffmpeg/core", "@ffmpeg/ffmpeg", "@ffmpeg/util", "@webgpu/glslang", "fflate", "naga-wasm", "react", "react-dom"].sort());
|
||||
assert.deepEqual([...runtimePackages].sort(), ["@ffmpeg/core", "@ffmpeg/ffmpeg", "@ffmpeg/util", "@webgpu/glslang", "@xyflow/react", "fflate", "naga-wasm", "react", "react-accessible-treeview", "react-dom"].sort());
|
||||
assert.equal(packageJson.dependencies?.three, undefined);
|
||||
assert.equal(packageJson.dependencies?.["@sqlite.org/sqlite-wasm"], undefined);
|
||||
for (const name of runtimePackages) {
|
||||
|
||||
@@ -177,3 +177,94 @@ test("M10-08 rejects an unknown renderer backend and malformed texture identity"
|
||||
assert.equal(malformed.status, "BLOCKED");
|
||||
assert.equal(malformed.issues.at(-1).code, "SHADER_INVALID_GRAPH");
|
||||
});
|
||||
|
||||
test("M10-07 compiles Mix constants into the Principled base color", () => {
|
||||
const report = compiler.compileMaterialGraph(material({
|
||||
nodes: [
|
||||
{ id: "color-a", type: "RGB", name: "A", defaultValue: [1, 0, 0, 1] },
|
||||
{ id: "color-b", type: "RGB", name: "B", defaultValue: [0, 0, 1, 1] },
|
||||
{ id: "factor", type: "VALUE", name: "Factor", defaultValue: [0.5] },
|
||||
{ id: "mix", type: "MIX", name: "Mix", properties: { dataType: "RGBA", blendType: "MIX" } },
|
||||
{ id: "principled", type: "PRINCIPLED", name: "Principled" },
|
||||
{ id: "output", type: "OUTPUT", name: "Output" },
|
||||
],
|
||||
links: [
|
||||
{ fromNodeId: "factor", fromSocket: "Value", toNodeId: "mix", toSocket: "Factor" },
|
||||
{ fromNodeId: "color-a", fromSocket: "Color", toNodeId: "mix", toSocket: "A" },
|
||||
{ fromNodeId: "color-b", fromSocket: "Color", toNodeId: "mix", toSocket: "B" },
|
||||
{ fromNodeId: "mix", fromSocket: "Result", toNodeId: "principled", toSocket: "Base Color" },
|
||||
{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" },
|
||||
],
|
||||
}));
|
||||
assert.equal(report.status, "COMPILED");
|
||||
assert.deepEqual(report.material.baseColor.map((value) => Number(value.toFixed(4))), [0.5, 0, 0.5, 1]);
|
||||
});
|
||||
|
||||
test("M10-07 compiles Separate Color constants into scalar inputs", () => {
|
||||
const report = compiler.compileMaterialGraph(material({
|
||||
nodes: [
|
||||
{ id: "color", type: "RGB", name: "Color", defaultValue: [0.2, 0.4, 0.6, 1] },
|
||||
{ id: "separate", type: "SEPARATE_COLOR", name: "Separate", properties: { mode: "RGB" } },
|
||||
{ id: "principled", type: "PRINCIPLED", name: "Principled" },
|
||||
{ id: "output", type: "OUTPUT", name: "Output" },
|
||||
],
|
||||
links: [
|
||||
{ fromNodeId: "color", fromSocket: "Color", toNodeId: "separate", toSocket: "Color" },
|
||||
{ fromNodeId: "separate", fromSocket: "Red", toNodeId: "principled", toSocket: "Roughness" },
|
||||
{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" },
|
||||
],
|
||||
}));
|
||||
assert.equal(report.status, "COMPILED");
|
||||
assert.equal(report.material.roughness, 0.2);
|
||||
});
|
||||
|
||||
test("M10-07 compiles Mapping -> Image -> Bump with a UV transform binding", () => {
|
||||
const report = compiler.compileMaterialGraph(material({
|
||||
nodes: [
|
||||
{ id: "uv", type: "UV_MAP", name: "UV", properties: { uvMap: "UVMap" } },
|
||||
{ id: "mapping", type: "MAPPING", name: "Mapping", properties: { vectorType: "POINT" }, sockets: [
|
||||
{ id: "Location", name: "Location", direction: "INPUT", dataType: "VECTOR", value: [0.1, 0.2, 0] },
|
||||
{ id: "Scale", name: "Scale", direction: "INPUT", dataType: "VECTOR", value: [2, 3, 1] },
|
||||
{ id: "Rotation", name: "Rotation", direction: "INPUT", dataType: "VECTOR", value: [0.5, 0, 0] },
|
||||
] },
|
||||
{ id: "bump-image", type: "IMAGE_TEXTURE", name: "Bump Image", imageId: "image:Bump" },
|
||||
{ id: "bump", type: "BUMP", name: "Bump" },
|
||||
{ id: "principled", type: "PRINCIPLED", name: "Principled" },
|
||||
{ id: "output", type: "OUTPUT", name: "Output" },
|
||||
],
|
||||
links: [
|
||||
{ fromNodeId: "uv", fromSocket: "UV", toNodeId: "mapping", toSocket: "Vector" },
|
||||
{ fromNodeId: "mapping", fromSocket: "Vector", toNodeId: "bump-image", toSocket: "Vector" },
|
||||
{ fromNodeId: "bump-image", fromSocket: "Color", toNodeId: "bump", toSocket: "Height" },
|
||||
{ fromNodeId: "bump", fromSocket: "Normal", toNodeId: "principled", toSocket: "Normal" },
|
||||
{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" },
|
||||
],
|
||||
}), { imageIds: new Set(["image:Bump"]) });
|
||||
assert.equal(report.status, "COMPILED");
|
||||
assert.deepEqual(report.textureBindings, [{
|
||||
imageId: "image:Bump",
|
||||
usage: "BUMP",
|
||||
uvMap: "UVMap",
|
||||
uvTransform: { offset: [0.1, 0.2], rotation: 0.5, scale: [2, 3] },
|
||||
}]);
|
||||
});
|
||||
|
||||
test("M10-07 keeps texture-fed Mix blocked with a visible issue", () => {
|
||||
const report = compiler.compileMaterialGraph(material({
|
||||
nodes: [
|
||||
{ id: "image", type: "IMAGE_TEXTURE", name: "Image", imageId: "image:Color" },
|
||||
{ id: "color", type: "RGB", name: "Color", defaultValue: [0, 0, 0, 1] },
|
||||
{ id: "mix", type: "MIX", name: "Mix", properties: { dataType: "RGBA", blendType: "MIX" } },
|
||||
{ id: "principled", type: "PRINCIPLED", name: "Principled" },
|
||||
{ id: "output", type: "OUTPUT", name: "Output" },
|
||||
],
|
||||
links: [
|
||||
{ fromNodeId: "image", fromSocket: "Color", toNodeId: "mix", toSocket: "A" },
|
||||
{ fromNodeId: "color", fromSocket: "Color", toNodeId: "mix", toSocket: "B" },
|
||||
{ fromNodeId: "mix", fromSocket: "Result", toNodeId: "principled", toSocket: "Base Color" },
|
||||
{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" },
|
||||
],
|
||||
}), { imageIds: new Set(["image:Color"]) });
|
||||
assert.equal(report.status, "BLOCKED");
|
||||
assert.ok(report.issues.some((entry) => entry.message.includes("Mix color inputs")));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user