117 lines
6.5 KiB
JavaScript
117 lines
6.5 KiB
JavaScript
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 });
|
|
}
|
|
});
|