99 lines
3.2 KiB
JavaScript
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));
|
|
}
|