148 lines
6.3 KiB
JavaScript
148 lines
6.3 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { copyFile, mkdtemp, rm, stat } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { exists, loadRecipe, projectRoot, readJson, workRoot, writeJson } from "./lib.mjs";
|
|
import { preprocessShadowfangVisual } from "./shadowfang-fallback-preprocess.mjs";
|
|
|
|
function runCli(args) {
|
|
const cli = path.join(projectRoot, "node_modules/@gltf-transform/cli/bin/cli.js");
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [cli, ...args], { cwd: projectRoot, stdio: "inherit", windowsHide: true });
|
|
child.once("error", reject);
|
|
child.once("exit", (code) => code === 0 ? resolve() : reject(new Error(`gltf-transform exited with code ${code}.`)));
|
|
});
|
|
}
|
|
|
|
function compressKtx2(file) {
|
|
const compressor = path.join(projectRoot, "scripts/asset-pipeline/compress-ktx2.mjs");
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [
|
|
compressor,
|
|
file,
|
|
"--concurrency",
|
|
"1",
|
|
"--jobs",
|
|
"2",
|
|
], { cwd: projectRoot, stdio: "inherit", windowsHide: true });
|
|
child.once("error", reject);
|
|
child.once("exit", (code) => code === 0
|
|
? resolve()
|
|
: reject(new Error(`KTX2 compression exited with code ${code}.`)));
|
|
});
|
|
}
|
|
|
|
export function dungeonEnvironmentOptimizationArguments(sourceFile, outputFile, { preserveSpatialBoundaries = false } = {}) {
|
|
return [
|
|
"optimize",
|
|
sourceFile,
|
|
outputFile,
|
|
"--compress",
|
|
"false",
|
|
"--meshopt-level",
|
|
"high",
|
|
"--instance",
|
|
preserveSpatialBoundaries ? "false" : "true",
|
|
"--instance-min",
|
|
"3",
|
|
"--flatten",
|
|
preserveSpatialBoundaries ? "false" : "true",
|
|
"--join",
|
|
preserveSpatialBoundaries ? "false" : "true",
|
|
"--weld",
|
|
preserveSpatialBoundaries ? "false" : "true",
|
|
"--simplify",
|
|
"false",
|
|
"--texture-compress",
|
|
"false",
|
|
];
|
|
}
|
|
|
|
export function dungeonEnvironmentFinalPackingArguments(sourceFile, outputFile) {
|
|
return [
|
|
"meshopt",
|
|
sourceFile,
|
|
outputFile,
|
|
"--level",
|
|
"high",
|
|
"--quantize-position",
|
|
"16",
|
|
"--quantize-texcoord",
|
|
"16",
|
|
];
|
|
}
|
|
|
|
export async function optimizeDungeonEnvironment(slug) {
|
|
const recipe = await loadRecipe(slug);
|
|
const directory = path.join(workRoot, slug, "staging", "environment");
|
|
const conversionFile = path.join(directory, "conversion-report.json");
|
|
if (!await exists(conversionFile)) return { status: "blocked", dungeonId: slug, blockers: ["Run dungeon:convert first."] };
|
|
const conversion = await readJson(conversionFile);
|
|
const source = path.join(directory, conversion.visual.file);
|
|
if (!await exists(source)) return { status: "blocked", dungeonId: slug, blockers: ["The lossless visual GLB is missing."] };
|
|
const selectiveProfile = recipe.visualOptimization?.selectiveFallbackSimplification;
|
|
if (slug === "shadowfang-keep" && !selectiveProfile) {
|
|
throw new Error("Shadowfang optimization requires its source-pinned selective fallback recipe profile.");
|
|
}
|
|
if (slug !== "shadowfang-keep" && selectiveProfile) {
|
|
throw new Error(`The Shadowfang-only selective fallback profile cannot be applied to ${slug}.`);
|
|
}
|
|
const preserveSpatialBoundaries = recipe.visualOptimization?.preserveSpatialBoundaries === true;
|
|
const optimized = path.join(directory, `${slug}-visual.optimized.glb`);
|
|
const packed = path.join(directory, `${slug}-visual.meshopt-final.glb`);
|
|
let temporaryDirectory = null;
|
|
let optimizationSource = source;
|
|
let sourcePreprocessing = null;
|
|
try {
|
|
if (slug === "shadowfang-keep") {
|
|
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "shadowfang-optimizer-source-"));
|
|
optimizationSource = path.join(temporaryDirectory, "shadowfang-selective-source.glb");
|
|
sourcePreprocessing = await preprocessShadowfangVisual(source, optimizationSource);
|
|
}
|
|
|
|
await runCli(dungeonEnvironmentOptimizationArguments(optimizationSource, optimized, { preserveSpatialBoundaries }));
|
|
await compressKtx2(optimized);
|
|
await rm(packed, { force: true });
|
|
await runCli(dungeonEnvironmentFinalPackingArguments(optimized, packed));
|
|
await copyFile(packed, optimized);
|
|
await rm(packed, { force: true });
|
|
const finalPackingCommand = "gltf-transform meshopt <optimized.glb> <meshopt-final.glb> --level high --quantize-position 16 --quantize-texcoord 16";
|
|
const report = {
|
|
schemaVersion: 1,
|
|
dungeonId: slug,
|
|
status: "review-required",
|
|
source: { file: path.basename(source), size: (await stat(source)).size },
|
|
sourcePreprocessing: sourcePreprocessing ? {
|
|
method: "source-pinned selective simplification of untextured ADT_Terrain and ADT_Liquid_B2 fallback materials",
|
|
sourceSha256: sourcePreprocessing.source.sha256,
|
|
texturedPrimitiveAccessorHashesUnchanged: sourcePreprocessing.texturedPrimitiveAccessorHashesUnchanged,
|
|
sourceTextureBytesUnchanged: sourcePreprocessing.sourceTextureBytesUnchanged,
|
|
...sourcePreprocessing.profile,
|
|
selectedByMaterial: sourcePreprocessing.selectedByMaterial,
|
|
} : null,
|
|
optimized: { file: path.basename(optimized), size: (await stat(optimized)).size },
|
|
compression: "meshopt+ktx2",
|
|
meshoptQuantization: { positionBits: 16, texcoordBits: 16 },
|
|
preserveSpatialBoundaries,
|
|
meshoptPackingRestoredAfterTextureCompression: true,
|
|
finalPackedStage: {
|
|
status: "complete",
|
|
file: path.basename(optimized),
|
|
size: (await stat(optimized)).size,
|
|
extension: "EXT_meshopt_compression",
|
|
command: finalPackingCommand,
|
|
},
|
|
commands: [
|
|
...(sourcePreprocessing ? ["source-pinned Shadowfang fallback preprocessing (see sourcePreprocessing profile)"] : []),
|
|
`gltf-transform optimize --compress false --instance ${preserveSpatialBoundaries ? "false" : "true"} --flatten ${preserveSpatialBoundaries ? "false" : "true"} --join ${preserveSpatialBoundaries ? "false" : "true"} --weld ${preserveSpatialBoundaries ? "false" : "true"} --simplify false --texture-compress false`,
|
|
"node scripts/asset-pipeline/compress-ktx2.mjs <optimized.glb> --concurrency 1 --jobs 2",
|
|
finalPackingCommand,
|
|
],
|
|
};
|
|
await writeJson(path.join(directory, "optimization-report.json"), report);
|
|
return report;
|
|
} finally {
|
|
if (temporaryDirectory) await rm(temporaryDirectory, { recursive: true, force: true });
|
|
}
|
|
}
|