import { access } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { EXTMeshoptCompression, } from "@gltf-transform/extensions"; import { dedup, mergeDocuments, prune, unpartition } from "@gltf-transform/functions"; import { createGameAssetIO, convertDocumentTexturesToKtx2 } from "./lib/ktx2.mjs"; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const sourceDirectory = path.join( repositoryRoot, "game_assets", "models", "original", "environment", "kaykit-dungeon", ); const outputPath = path.join(sourceDirectory, "dungeon-kit-uastc.glb"); const sources = ["pillar-decorated.glb", "wall-pillar.glb", "torch-lit.glb"]; const expectedMeshNames = ["pillar_decorated", "wall_pillar", "torch_lit"]; await Promise.all(sources.map((source) => access(path.join(sourceDirectory, source)))); const io = await createGameAssetIO(); const documents = await Promise.all(sources.map((source) => io.read(path.join(sourceDirectory, source)))); const document = documents[0]; const destinationScene = document.getRoot().listScenes()[0].setName("dungeon_kit"); for (const sourceDocument of documents.slice(1)) { const sourceScene = sourceDocument.getRoot().listScenes()[0]; const propertyMap = mergeDocuments(document, sourceDocument); const copiedScene = propertyMap.get(sourceScene); if (!copiedScene) throw new Error(`Could not merge scene from ${sourceScene.getName() || "unnamed source"}.`); for (const child of [...copiedScene.listChildren()]) destinationScene.addChild(child); copiedScene.dispose(); } await document.transform( dedup({ keepUniqueNames: false }), unpartition(), prune({ keepSolidTextures: true }), ); const root = document.getRoot(); const meshNames = root.listMeshes().map((mesh) => mesh.getName()).sort(); const expectedSorted = [...expectedMeshNames].sort(); if (JSON.stringify(meshNames) !== JSON.stringify(expectedSorted)) { throw new Error(`Dungeon kit mesh names changed: expected ${expectedSorted.join(", ")}; received ${meshNames.join(", ")}.`); } if (root.listTextures().length !== 1) { throw new Error(`Dungeon kit must contain exactly one shared texture; received ${root.listTextures().length}.`); } if (root.listMaterials().length !== 1) { throw new Error(`Dungeon kit must contain exactly one shared material; received ${root.listMaterials().length}.`); } await convertDocumentTexturesToKtx2(document, "iwt-dungeon-kit-"); const meshoptExtension = root.listExtensionsUsed() .find((extension) => extension.extensionName === EXTMeshoptCompression.EXTENSION_NAME) ?? document.createExtension(EXTMeshoptCompression); meshoptExtension .setRequired(true) .setEncoderOptions({ method: EXTMeshoptCompression.EncoderMethod.QUANTIZE }); await io.write(outputPath, document); const outputDocument = await io.read(outputPath); const outputRoot = outputDocument.getRoot(); const outputTexture = outputRoot.listTextures()[0]; if ( outputRoot.listMeshes().length !== 3 || outputRoot.listMaterials().length !== 1 || outputRoot.listTextures().length !== 1 || outputTexture.getMimeType() !== "image/ktx2" ) { throw new Error("Generated dungeon kit failed structural validation."); } console.log(`Built ${path.relative(repositoryRoot, outputPath)}`); console.log("Meshes: pillar_decorated, wall_pillar, torch_lit; shared textures: 1; encoding: KTX2/UASTC.");