Files
healer-man/scripts/asset-pipeline/glb-performance.mjs
T
2026-08-22 15:53:43 -04:00

99 lines
3.2 KiB
JavaScript

import { open } from "node:fs/promises";
const GLB_MAGIC = 0x46546c67;
const JSON_CHUNK_TYPE = 0x4e4f534a;
const GPU_INSTANCING_EXTENSION = "EXT_mesh_gpu_instancing";
function primitiveCount(json, meshIndex) {
return json.meshes?.[meshIndex]?.primitives?.length ?? 0;
}
function gpuInstanceCount(json, node) {
const attributes = node.extensions?.[GPU_INSTANCING_EXTENSION]?.attributes;
if (!attributes || typeof attributes !== "object") return 0;
return Math.max(
0,
...Object.values(attributes).map((accessorIndex) => (
json.accessors?.[accessorIndex]?.count ?? 0
)),
);
}
export function analyzeGlbJson(json) {
const repeatedMeshes = new Map();
let estimatedDrawCalls = 0;
let expandedPrimitiveInstances = 0;
let meshNodes = 0;
let gpuInstanceBatches = 0;
let gpuInstances = 0;
for (const node of json.nodes ?? []) {
if (!Number.isInteger(node.mesh)) continue;
meshNodes += 1;
const primitives = primitiveCount(json, node.mesh);
const instances = gpuInstanceCount(json, node);
estimatedDrawCalls += primitives;
expandedPrimitiveInstances += primitives * Math.max(1, instances);
if (instances > 0) {
gpuInstanceBatches += 1;
gpuInstances += instances;
continue;
}
repeatedMeshes.set(node.mesh, (repeatedMeshes.get(node.mesh) ?? 0) + 1);
}
let eligibleRepeatedMeshes = 0;
let eligibleRepeatedNodes = 0;
let unbatchedDrawCallSavings = 0;
for (const [meshIndex, count] of repeatedMeshes) {
if (count < 3) continue;
eligibleRepeatedMeshes += 1;
eligibleRepeatedNodes += count;
unbatchedDrawCallSavings += (count - 1) * primitiveCount(json, meshIndex);
}
const materials = json.materials ?? [];
return {
nodes: json.nodes?.length ?? 0,
meshNodes,
meshes: json.meshes?.length ?? 0,
materials: materials.length,
textures: json.textures?.length ?? 0,
doubleSidedMaterials: materials.filter((material) => material.doubleSided === true).length,
blendedMaterials: materials.filter((material) => material.alphaMode === "BLEND").length,
estimatedDrawCalls,
expandedPrimitiveInstances,
gpuInstanceBatches,
gpuInstances,
eligibleRepeatedMeshes,
eligibleRepeatedNodes,
unbatchedDrawCallSavings,
usesGpuInstancing: (json.extensionsUsed ?? []).includes(GPU_INSTANCING_EXTENSION),
};
}
export async function readGlbJson(file) {
const handle = await open(file, "r");
try {
const header = Buffer.alloc(20);
const { bytesRead } = await handle.read(header, 0, header.length, 0);
if (bytesRead !== header.length
|| header.readUInt32LE(0) !== GLB_MAGIC
|| header.readUInt32LE(4) !== 2
|| header.readUInt32LE(16) !== JSON_CHUNK_TYPE) {
throw new Error(`${file} is not a supported GLB 2.0 file.`);
}
const jsonLength = header.readUInt32LE(12);
const jsonBuffer = Buffer.alloc(jsonLength);
const jsonRead = await handle.read(jsonBuffer, 0, jsonLength, 20);
if (jsonRead.bytesRead !== jsonLength) throw new Error(`${file} has a truncated JSON chunk.`);
return JSON.parse(jsonBuffer.toString("utf8"));
} finally {
await handle.close();
}
}
export async function analyzeGlbFile(file) {
return analyzeGlbJson(await readGlbJson(file));
}