import { spawn } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { NodeIO } from "@gltf-transform/core"; import { ALL_EXTENSIONS, EXTTextureWebP, KHRTextureBasisu } from "@gltf-transform/extensions"; import { MeshoptDecoder, MeshoptEncoder } from "meshoptimizer"; import sharp from "sharp"; export const TOKTX_COMMAND = process.env.TOKTX ?? "toktx"; export async function createGameAssetIO() { await MeshoptDecoder.ready; await MeshoptEncoder.ready; return new NodeIO() .registerExtensions(ALL_EXTENSIONS) .registerDependencies({ "meshopt.decoder": MeshoptDecoder, "meshopt.encoder": MeshoptEncoder, }); } function run(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: "inherit" }); child.once("error", reject); child.once("exit", (code) => { if (code === 0) resolve(); else reject(new Error(`${command} exited with code ${code ?? "unknown"}.`)); }); }); } export async function convertDocumentTexturesToKtx2(document, temporaryPrefix) { const textures = document.getRoot().listTextures(); if (textures.length === 0) throw new Error("Asset contains no textures to convert."); const temporaryDirectory = await mkdtemp(path.join(tmpdir(), temporaryPrefix)); try { for (const [index, texture] of textures.entries()) { const sourceImage = texture.getImage(); if (!sourceImage) throw new Error(`Texture ${texture.getName() || index} contains no image data.`); const pngPath = path.join(temporaryDirectory, `texture-${index}.png`); const ktx2Path = path.join(temporaryDirectory, `texture-${index}.ktx2`); await writeFile(pngPath, await sharp(sourceImage).png().toBuffer()); await run(TOKTX_COMMAND, [ "--t2", "--encode", "uastc", "--uastc_quality", "4", "--zcmp", "18", "--threads", process.env.TOKTX_THREADS ?? "4", "--genmipmap", "--assign_oetf", "srgb", "--assign_primaries", "bt709", ktx2Path, pngPath, ]); texture .setName(`${texture.getName() || `texture_${index}`}_uastc`) .setMimeType("image/ktx2") .setImage(new Uint8Array(await readFile(ktx2Path))); } document.getRoot().listExtensionsUsed() .find((extension) => extension.extensionName === EXTTextureWebP.EXTENSION_NAME) ?.dispose(); document.createExtension(KHRTextureBasisu).setRequired(true); } finally { await rm(temporaryDirectory, { recursive: true, force: true }); } }