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); });