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, KHRMaterialsSpecular } from "@gltf-transform/extensions"; import { MeshoptDecoder } from "meshoptimizer"; import { environmentOptimizationArguments } from "./environment-optimization.mjs"; import { applyNativeMaterialSemantics, hasVerifiedNativeMaterialIdentity } from "./native-material-semantics.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"); const nativeIdentity = { nativeSourceModel: "model\\dungeon\\the_origin\\act_precipice_01.ros", nativeSourceResourceSha256: "b".repeat(64), nativeMaterialIndex: 8, nativeSourceTexture: "texture\\dungeon\\the_origin\\wave_01", }; 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 patternedPng() { const header = Buffer.alloc(13); header.writeUInt32BE(2, 0); header.writeUInt32BE(2, 4); header[8] = 8; header[9] = 6; const rows = Buffer.from([ 0, 210, 170, 125, 255, 15, 80, 210, 255, 0, 25, 240, 95, 255, 160, 20, 180, 255, ]); return Buffer.concat([ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), pngChunk("IHDR", header), pngChunk("IDAT", deflateSync(rows)), pngChunk("IEND", Buffer.alloc(0)), ]); } async function writeFixture(file, extras) { const document = new Document(); const buffer = document.createBuffer(); const position = document.createAccessor() .setType("VEC3") .setArray(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0])) .setBuffer(buffer); const material = document.createMaterial("native wave") .setAlphaMode("BLEND") .setExtras(extras); material.setExtension("KHR_materials_specular", document.createExtension(KHRMaterialsSpecular).createSpecular().setSpecularFactor(0)); const primitive = document.createPrimitive() .setAttribute("POSITION", position) .setMaterial(material); const mesh = document.createMesh("wave mesh").addPrimitive(primitive); const node = document.createNode("wave node").setMesh(mesh); const scene = document.createScene("fixture scene").addChild(node); document.getRoot().setDefaultScene(scene); await new NodeIO().registerExtensions(ALL_EXTENSIONS).write(file, document); } test("adds KHR_materials_unlit only to exact-proven prelit materials and retains additive extras", async () => { const directory = await mkdtemp(path.join(os.tmpdir(), "rom-native-material-")); try { const file = path.join(directory, "wave.glb"); await writeFixture(file, { ...nativeIdentity, nativeBlendMode: "additive", nativeAlphaPolicy: "native-srcalpha-one-additive", nativeLightingType: "prelit", }); const report = await applyNativeMaterialSemantics(file); assert.equal(report.khrMaterialsUnlit.length, 1); assert.equal(report.nativeAdditiveMaterials.length, 1); assert.equal(report.nativeAdditiveMaterials[0].alphaMode, "BLEND"); const document = await new NodeIO().registerExtensions(ALL_EXTENSIONS).read(file); const material = document.getRoot().listMaterials()[0]; assert.ok(material.getExtension("KHR_materials_unlit")); assert.equal(material.getExtension("KHR_materials_specular"), null); assert.equal(material.getExtras().nativeBlendMode, "additive"); assert.equal(material.getAlphaMode(), "BLEND"); } finally { await rm(directory, { recursive: true, force: true }); } }); test("rejects prelit claims without complete native material identity", async () => { assert.equal(hasVerifiedNativeMaterialIdentity(nativeIdentity), true); assert.equal(hasVerifiedNativeMaterialIdentity({ ...nativeIdentity, nativeSourceResourceSha256: "bad" }), false); const directory = await mkdtemp(path.join(os.tmpdir(), "rom-native-material-invalid-")); try { const file = path.join(directory, "bad.glb"); await writeFixture(file, { nativeLightingType: "prelit", ...nativeIdentity, nativeMaterialIndex: -1 }); await assert.rejects(() => applyNativeMaterialSemantics(file), /lacks verified material provenance/); } finally { await rm(directory, { recursive: true, force: true }); } }); test("gltf-transform optimize keeps shared-map standard and prelit materials distinct", async (context) => { const directory = await mkdtemp(path.join(os.tmpdir(), "rom-native-material-optimize-")); context.after(async () => rm(directory, { recursive: true, force: true })); const sourceFile = path.join(directory, "source.glb"); const outputFile = path.join(directory, "optimized.glb"); const document = new Document(); const buffer = document.createBuffer(); const image = document.createTexture("shared-water-image") .setMimeType("image/png") .setImage(patternedPng()); const positions = document.createAccessor("triangle positions") .setType("VEC3") .setArray(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0])) .setBuffer(buffer); const texcoords = document.createAccessor("triangle UVs") .setType("VEC2") .setArray(new Float32Array([0, 0, 1, 0, 0, 1])) .setBuffer(buffer); const indices = document.createAccessor("triangle indices") .setType("SCALAR") .setArray(new Uint16Array([0, 1, 2])) .setBuffer(buffer); const scene = document.createScene("shared scene"); for (const [name, lightingType, nativeMaterialIndex] of [ ["standard water", "standard", 8], ["prelit water", "prelit", 9], ]) { const material = document.createMaterial(name) .setBaseColorTexture(image) .setExtras({ ...nativeIdentity, nativeMaterialIndex, nativeLightingType: lightingType }); const primitive = document.createPrimitive() .setAttribute("POSITION", positions) .setAttribute("TEXCOORD_0", texcoords) .setIndices(indices) .setMaterial(material); const mesh = document.createMesh(`${name} mesh`).addPrimitive(primitive); scene.addChild(document.createNode(name).setMesh(mesh)); } document.getRoot().setDefaultScene(scene); const io = new NodeIO() .registerExtensions(ALL_EXTENSIONS) .registerDependencies({ "meshopt.decoder": MeshoptDecoder }); await io.write(sourceFile, document); assert.ok((await io.read(sourceFile)).getRoot().listMaterials().every((material) => material.getBaseColorTexture())); const semantics = await applyNativeMaterialSemantics(sourceFile); assert.equal(semantics.khrMaterialsUnlit.length, 1); // Keep the fixture materials identical apart from the glTF lighting extension, // isolating the behavior at risk from optimizer material de-duplication. const annotated = await io.read(sourceFile); for (const material of annotated.getRoot().listMaterials()) material.setExtras({}); await io.write(sourceFile, annotated); assert.equal((await io.read(sourceFile)).getRoot().listMaterials().filter((material) => material.getBaseColorTexture()).length, 2); const args = environmentOptimizationArguments(sourceFile, outputFile); 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 materials = optimized.getRoot().listMaterials(); assert.equal(materials.length, 2, "optimization must not merge materials with different lighting modes"); const unlitMaterials = materials.filter((material) => material.getExtension("KHR_materials_unlit")); assert.equal(unlitMaterials.length, 1); assert.equal(materials.filter((material) => !material.getExtension("KHR_materials_unlit")).length, 1); assert.ok(materials.every((material) => material.getBaseColorTexture()), JSON.stringify(materials.map((material) => ({ name: material.getName(), texture: material.getBaseColorTexture()?.getName() ?? null, unlit: Boolean(material.getExtension("KHR_materials_unlit")), })).concat([{ textureCount: optimized.getRoot().listTextures().length, sceneChildren: optimized.getRoot().listScenes().flatMap((entry) => entry.listChildren()).length, primitives: optimized.getRoot().listMeshes().flatMap((mesh) => mesh.listPrimitives()).map((primitive) => ({ material: primitive.getMaterial()?.getName(), attributes: Object.keys(primitive.listAttributes()), })), }]))); });