All 27 shipped GLBs run through gltf-transform (meshopt + webp + 1024 textures). Measured per-asset, no failures: 27,304 KB -> 7,132 KB (74% off) with PBR intact. The biggest wins are the wave-1/wave-2 assets that shipped raw (artifact-bottler 1453->329, chroma-keyer 1928->301, mv-extractor 1376->190); the six baked through the new farm script were already lean and barely moved, which is the check that the farm-side step is doing its job. tools/optimize-assets.mjs: idempotent gate wired into deploy.sh BEFORE vite build. Skips anything already carrying EXT_meshopt_compression (so it costs ~0s when nothing is new) and FAILS THE BUILD on any asset over 900 KB — a farm regression that emits trellis2-dense meshes now stops the deploy instead of shipping 36 MB. deploy.sh verify gap closed: it tested exactly one .js file, so a deploy that shipped ZERO models would have reported success. Now checks the JS bundle, a real GLB endpoint, and asserts live model count == built model count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
70 lines
2.7 KiB
JavaScript
70 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Idempotent GLB optimization gate. Runs from deploy.sh BEFORE vite build.
|
||
*
|
||
* Why this exists: the farm emits 1–8 MB PBR GLBs. Shipped raw, 27 of them were
|
||
* 27.3 MB; through `gltf-transform optimize` they are 7.0 MB with the same PBR.
|
||
* That step used to live in my head, which is not a build system. An asset added
|
||
* by hand — or by a farm run that skipped the post-step — would silently ship raw.
|
||
*
|
||
* Skips anything already carrying EXT_meshopt_compression, so it is safe to run
|
||
* every deploy and costs ~0s when there is nothing new.
|
||
*/
|
||
import { execFileSync } from 'node:child_process';
|
||
import { readdirSync, readFileSync, statSync, renameSync, rmSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
|
||
const DIR = new URL('../public/assets/models/', import.meta.url).pathname;
|
||
const MAX_KB = 900; // per-asset ceiling; a farm regression should fail the build
|
||
|
||
/** Read the GLB JSON chunk without a glTF library. */
|
||
function meta(path) {
|
||
const b = readFileSync(path);
|
||
if (b.length < 20 || b.subarray(0, 4).toString() !== 'glTF') return null;
|
||
const jsonLen = b.readUInt32LE(12);
|
||
try {
|
||
return JSON.parse(b.subarray(20, 20 + jsonLen).toString());
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const files = readdirSync(DIR).filter((f) => f.endsWith('.glb'));
|
||
let optimized = 0, skipped = 0, before = 0, after = 0;
|
||
const oversize = [];
|
||
|
||
for (const f of files) {
|
||
const p = join(DIR, f);
|
||
const sizeBefore = statSync(p).size;
|
||
before += sizeBefore;
|
||
const m = meta(p);
|
||
if (m?.extensionsRequired?.includes('EXT_meshopt_compression')) {
|
||
skipped++;
|
||
after += sizeBefore;
|
||
} else {
|
||
const tmp = `${p}.opt`;
|
||
execFileSync('npx', ['--yes', '@gltf-transform/cli@latest', 'optimize', p, tmp,
|
||
'--texture-compress', 'webp', '--texture-size', '1024',
|
||
'--simplify-error', '0.002', '--compress', 'meshopt'],
|
||
{ stdio: 'pipe', timeout: 900_000 });
|
||
rmSync(p);
|
||
renameSync(tmp, p);
|
||
optimized++;
|
||
after += statSync(p).size;
|
||
console.log(` optimized ${f}: ${(sizeBefore / 1024) | 0} KB -> ${(statSync(p).size / 1024) | 0} KB`);
|
||
}
|
||
const kb = (statSync(p).size / 1024) | 0;
|
||
if (kb > MAX_KB) oversize.push(`${f} (${kb} KB)`);
|
||
}
|
||
|
||
console.log(`assets: ${files.length} files, ${optimized} optimized, ${skipped} already lean — ` +
|
||
`${(before / 1048576).toFixed(1)} MB -> ${(after / 1048576).toFixed(1)} MB`);
|
||
|
||
if (oversize.length) {
|
||
console.error(`\nFAIL: ${oversize.length} asset(s) over ${MAX_KB} KB after optimization:`);
|
||
for (const o of oversize) console.error(` ${o}`);
|
||
console.error('A mesh this heavy usually means the bake used too dense a pipeline ' +
|
||
'(trellis2 1024_cascade produces ~487k tris; hunyuan3d_mlx is the machine default).');
|
||
process.exit(1);
|
||
}
|