import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { Accessor, Document, NodeIO } from "@gltf-transform/core"; import { EXTMeshGPUInstancing } from "@gltf-transform/extensions"; import { mappingMetrics, nodeInstanceTransforms, sourceImageAliasKey, transformInstancedPoint, triangleArea, } from "./audit-environment-quality.mjs"; test("source image aliases preserve unresolved or used alpha and normalize only known-unused alpha", () => { const source = new Uint8Array([ 255, 64, 32, 0, 10, 20, 30, 255, ]); const aliasWithDifferentAlpha = new Uint8Array([ 255, 64, 32, 200, 10, 20, 30, 1, ]); const opaqueA = sourceImageAliasKey({ width: 2, height: 1, rgba: source, alphaMode: "OPAQUE", alphaUsed: false }); const opaqueB = sourceImageAliasKey({ width: 2, height: 1, rgba: aliasWithDifferentAlpha, alphaMode: "OPAQUE", alphaUsed: false }); assert.equal(opaqueA, opaqueB, "opaque texture aliases are compared using RGB and dimensions"); assert.notEqual(sourceImageAliasKey({ width: 2, height: 1, rgba: source, alphaMode: "OPAQUE" }), sourceImageAliasKey({ width: 2, height: 1, rgba: aliasWithDifferentAlpha, alphaMode: "OPAQUE" }), "varying source alpha stays part of identity when native use is unresolved"); const uniformTranslucent = new Uint8Array([255, 64, 32, 128, 10, 20, 30, 128]); const uniformOpaque = new Uint8Array([255, 64, 32, 255, 10, 20, 30, 255]); assert.notEqual(sourceImageAliasKey({ width: 2, height: 1, rgba: uniformTranslucent, alphaMode: "OPAQUE" }), sourceImageAliasKey({ width: 2, height: 1, rgba: uniformOpaque, alphaMode: "OPAQUE" }), "uniform non-opaque alpha also remains part of identity when native use is unresolved"); const nativeAlphaA = sourceImageAliasKey({ width: 2, height: 1, rgba: source, alphaMode: "OPAQUE", alphaUsed: true }); const nativeAlphaB = sourceImageAliasKey({ width: 2, height: 1, rgba: aliasWithDifferentAlpha, alphaMode: "OPAQUE", alphaUsed: true }); assert.notEqual(nativeAlphaA, nativeAlphaB, "native translucency preserves RGBA even if the converted material says OPAQUE"); const blendedA = sourceImageAliasKey({ width: 2, height: 1, rgba: source, alphaMode: "BLEND" }); const blendedB = sourceImageAliasKey({ width: 2, height: 1, rgba: aliasWithDifferentAlpha, alphaMode: "BLEND" }); assert.notEqual(blendedA, blendedB, "materials that use alpha retain separate RGBA identities"); assert.notEqual(opaqueA, sourceImageAliasKey({ width: 1, height: 2, rgba: source, alphaMode: "OPAQUE", alphaUsed: false }), "equal pixel bytes at another image shape do not alias"); assert.notEqual(opaqueA, sourceImageAliasKey({ width: 2, height: 1, rgba: source, alphaMode: "OPAQUE", alphaUsed: false, baseColorFactor: [0.5, 1, 1, 1] }), "material base-color factors remain part of rendered source equivalence"); }); test("instance TRS expansion preserves UV rank and accounts for transformed area and cells", async () => { const temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "rom-quality-instancing-test-")); const fixtureFile = path.join(temporaryDirectory, "instances.glb"); try { const document = new Document(); const buffer = document.createBuffer(); const positions = document.createAccessor("POSITION") .setType(Accessor.Type.VEC3) .setArray(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0])) .setBuffer(buffer); const primitive = document.createPrimitive().setAttribute("POSITION", positions); const node = document.createNode("fixture-node") .setMesh(document.createMesh("fixture-mesh").addPrimitive(primitive)) .setTranslation([5, 2, 0]) .setScale([2, 1, 1]); const instanceExtension = document.createExtension(EXTMeshGPUInstancing); const instancedMesh = instanceExtension.createInstancedMesh() .setAttribute("TRANSLATION", document.createAccessor("TRANSLATION") .setType(Accessor.Type.VEC3) .setArray(new Float32Array([0, 0, 0, 40, 0, 0])) .setBuffer(buffer)) .setAttribute("ROTATION", document.createAccessor("ROTATION") .setType(Accessor.Type.VEC4) .setArray(new Float32Array([0, 0, 0, 1, 0, 0, Math.SQRT1_2, Math.SQRT1_2])) .setBuffer(buffer)) .setAttribute("SCALE", document.createAccessor("SCALE") .setType(Accessor.Type.VEC3) .setArray(new Float32Array([2, 1, 1, 1, 3, 1])) .setBuffer(buffer)); node.setExtension("EXT_mesh_gpu_instancing", instancedMesh); document.createScene("fixture-scene").addChild(node); const io = new NodeIO().registerExtensions([EXTMeshGPUInstancing]); await io.write(fixtureFile, document); const parsed = await io.read(fixtureFile); const parsedNode = parsed.getRoot().listNodes().find((candidate) => candidate.getName() === "fixture-node"); const transforms = nodeInstanceTransforms(parsedNode); assert.equal(transforms.length, 2, "NodeIO reads both instance records"); const worldMatrix = parsedNode.getWorldMatrix(); const triangle = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]; const uvTriangle = [[0, 0], [1, 0], [0, 1]]; const collinearUvs = [[0, 0], [1, 1], [2, 2]]; const expandedTriangles = transforms.map((transform) => triangle.map((point) => transformInstancedPoint(worldMatrix, point, transform))); const areas = expandedTriangles.map(triangleArea); assert.ok(Math.abs(areas[0] - 2) < 1e-9 && Math.abs(areas[1] - 3) < 1e-6, "instance and node scales transform each triangle's area"); assert.ok(Math.abs(areas.reduce((total, area) => total + area, 0) - 5) < 1e-6, "both placed triangles contribute their transformed surface area"); const cells = expandedTriangles.map((points) => { const centroidX = points.reduce((total, point) => total + point[0] / 3, 0); return Math.floor(centroidX / 20); }); assert.deepEqual(cells, [0, 4], "instance TRS precedes the non-identity node world transform"); for (const points of expandedTriangles) { const valid = mappingMetrics(points, uvTriangle, 1024, 1024); const degenerate = mappingMetrics(points, collinearUvs, 1024, 1024); assert.ok(valid && valid.uvDoubleArea > 0 && valid.smallestStretchPxPerMeter > 0, "an invertible texture mapping retains UV rank under instance TRS"); assert.equal(degenerate?.uvDoubleArea, 0, "collinear UVs remain degenerate under instance TRS"); } } finally { await rm(temporaryDirectory, { recursive: true, force: true }); } });