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