Files
healer-man/scripts/runewaker-pipeline/environment-optimization.test.mjs
2026-10-06 22:25:34 -04:00

216 lines
8.2 KiB
JavaScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { deflateSync } from "node:zlib";
import test from "node:test";
import { Document, NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { MeshoptDecoder } from "meshoptimizer";
import { environmentOptimizationArguments } from "./lib/environment-optimization.mjs";
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(scriptDirectory, "../..");
const gltfTransformCli = path.join(projectRoot, "node_modules/@gltf-transform/cli/bin/cli.js");
function crc32(bytes) {
let crc = 0xffffffff;
for (const byte of bytes) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
}
}
return (crc ^ 0xffffffff) >>> 0;
}
function pngChunk(type, data) {
const typeBytes = Buffer.from(type, "ascii");
const contents = Buffer.concat([typeBytes, data]);
const header = Buffer.alloc(4);
header.writeUInt32BE(data.length);
const checksum = Buffer.alloc(4);
checksum.writeUInt32BE(crc32(contents));
return Buffer.concat([header, contents, checksum]);
}
function makePng(width, height) {
const header = Buffer.alloc(13);
header.writeUInt32BE(width, 0);
header.writeUInt32BE(height, 4);
header[8] = 8;
header[9] = 6;
const row = Buffer.alloc(1 + width * 4);
const pixels = Buffer.alloc(width * 4);
for (let offset = 0; offset < pixels.length; offset += 4) {
pixels[offset] = (offset / 4) % 256;
pixels[offset + 1] = 135;
pixels[offset + 2] = 65;
pixels[offset + 3] = 255;
}
pixels.copy(row, 1);
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
return Buffer.concat([
signature,
pngChunk("IHDR", header),
pngChunk("IDAT", deflateSync(row)),
pngChunk("IEND", Buffer.alloc(0)),
]);
}
function pngDimensions(bytes) {
bytes = Buffer.from(bytes);
assert.equal(bytes.toString("ascii", 1, 4), "PNG");
return [bytes.readUInt32BE(16), bytes.readUInt32BE(20)];
}
function atlasGridGeometry(document, buffer, segments) {
const positions = new Float32Array((segments + 1) * (segments + 1) * 3);
const texcoords = new Float32Array((segments + 1) * (segments + 1) * 2);
const indices = new Uint16Array(segments * segments * 6);
const atlasColumns = [0.03, 0.94, 0.08, 0.89];
let indexOffset = 0;
for (let row = 0; row <= segments; row += 1) {
for (let column = 0; column <= segments; column += 1) {
const vertex = row * (segments + 1) + column;
positions.set([column, row, 0], vertex * 3);
texcoords.set([
atlasColumns[column % atlasColumns.length],
row / segments,
], vertex * 2);
if (row < segments && column < segments) {
const topLeft = vertex;
const topRight = vertex + 1;
const bottomLeft = vertex + segments + 1;
const bottomRight = bottomLeft + 1;
indices.set([
topLeft, bottomLeft, topRight,
topRight, bottomLeft, bottomRight,
], indexOffset);
indexOffset += 6;
}
}
}
return {
positions: document.createAccessor("positions")
.setType("VEC3")
.setArray(positions)
.setBuffer(buffer),
texcoords: document.createAccessor("texcoords")
.setType("VEC2")
.setArray(texcoords)
.setBuffer(buffer),
indices: document.createAccessor("indices")
.setType("SCALAR")
.setArray(indices)
.setBuffer(buffer),
};
}
function geometryUvStats(document) {
let triangles = 0;
let degenerateUvTriangles = 0;
for (const mesh of document.getRoot().listMeshes()) {
for (const primitive of mesh.listPrimitives()) {
const texcoords = primitive.getAttribute("TEXCOORD_0")?.getArray();
const indices = primitive.getIndices()?.getArray();
if (!texcoords || !indices) continue;
triangles += indices.length / 3;
for (let offset = 0; offset < indices.length; offset += 3) {
const a = indices[offset] * 2;
const b = indices[offset + 1] * 2;
const c = indices[offset + 2] * 2;
const area = (texcoords[b] - texcoords[a]) * (texcoords[c + 1] - texcoords[a + 1])
- (texcoords[c] - texcoords[a]) * (texcoords[b + 1] - texcoords[a + 1]);
if (Math.abs(area) < 1e-8) degenerateUvTriangles += 1;
}
}
}
return { triangles, degenerateUvTriangles };
}
test("environment optimization preserves source texture dimensions and atlas UV topology", async (context) => {
const width = 4096;
const height = 1;
const gridSegments = 24;
const workingDirectory = await mkdtemp(path.join(os.tmpdir(), "runewaker-visual-quality-"));
context.after(async () => rm(workingDirectory, { recursive: true, force: true }));
const sourceFile = path.join(workingDirectory, "source.glb");
const outputFile = path.join(workingDirectory, "optimized.glb");
const simplifiedControlFile = path.join(workingDirectory, "simplified-control.glb");
const io = new NodeIO()
.registerExtensions(ALL_EXTENSIONS)
.registerDependencies({ "meshopt.decoder": MeshoptDecoder });
const document = new Document();
const buffer = document.createBuffer();
const texture = document.createTexture("wall-atlas")
.setMimeType("image/png")
.setImage(makePng(width, height));
const material = document.createMaterial("wall")
.setBaseColorTexture(texture);
const geometry = atlasGridGeometry(document, buffer, gridSegments);
const primitive = document.createPrimitive()
.setAttribute("POSITION", geometry.positions)
.setAttribute("TEXCOORD_0", geometry.texcoords)
.setIndices(geometry.indices)
.setMaterial(material);
const mesh = document.createMesh("wall").addPrimitive(primitive);
const scene = document.createScene("scene").addChild(document.createNode("wall").setMesh(mesh));
document.getRoot().setDefaultScene(scene);
await io.write(sourceFile, document);
const source = await io.read(sourceFile);
assert.equal(source.getRoot().listTextures().length, 1);
assert.ok(source.getRoot().listMaterials()[0].getBaseColorTexture());
const args = environmentOptimizationArguments(sourceFile, outputFile);
assert.deepEqual(args.slice(0, 3), ["optimize", sourceFile, outputFile]);
assert.deepEqual(args.slice(3), [
"--compress", "meshopt", "--meshopt-level", "high",
"--instance", "true", "--instance-min", "3",
"--simplify", "false", "--texture-compress", "false",
]);
const run = spawnSync(process.execPath, [gltfTransformCli, ...args], {
cwd: projectRoot,
encoding: "utf8",
windowsHide: true,
});
assert.equal(run.status, 0, `${run.stdout}\n${run.stderr}`);
const optimized = await io.read(outputFile);
const optimizedStats = geometryUvStats(optimized);
assert.deepEqual(optimizedStats, {
triangles: gridSegments * gridSegments * 2,
degenerateUvTriangles: 0,
}, "atlas-grid geometry and its source UV seams must survive optimize intact");
const simplifyOption = args.indexOf("--simplify");
const simplifiedControlArgs = [...args];
simplifiedControlArgs[2] = simplifiedControlFile;
simplifiedControlArgs[simplifyOption + 1] = "true";
const simplifiedControl = spawnSync(process.execPath, [gltfTransformCli, ...simplifiedControlArgs], {
cwd: projectRoot,
encoding: "utf8",
windowsHide: true,
});
assert.equal(simplifiedControl.status, 0, `${simplifiedControl.stdout}\n${simplifiedControl.stderr}`);
const simplifiedStats = geometryUvStats(await io.read(simplifiedControlFile));
assert.ok(
simplifiedStats.triangles < optimizedStats.triangles || simplifiedStats.degenerateUvTriangles > 0,
"atlas-grid fixture must detect topology loss or UV collapse when simplification is enabled",
);
const textures = optimized.getRoot().listTextures();
assert.equal(textures.length, 1, JSON.stringify({
materials: optimized.getRoot().listMaterials().map((entry) => ({
name: entry.getName(),
texture: entry.getBaseColorTexture()?.getName() ?? null,
})),
}));
const outputTexture = textures[0];
assert.deepEqual(pngDimensions(outputTexture.getImage()), [width, height]);
});