Files
healer-man/scripts/runewaker-pipeline/lib/bounded-collision-glb.mjs
T
2026-10-06 22:25:34 -04:00

61 lines
3.1 KiB
JavaScript

import { Document, NodeIO } from "@gltf-transform/core";
import { Matrix4, Vector3 } from "three";
import path from "node:path";
import { mkdir } from "node:fs/promises";
/** Preserve world-space source triangles while bounding each physical GLB. */
export async function writeBoundedCollisionGlbs(sourceFile, outputDirectory, slug, {
maximumTriangles = 90_000,
sourceSha256,
} = {}) {
if (!Number.isSafeInteger(maximumTriangles) || maximumTriangles < 1) {
throw new Error("Collision chunk limit must be a positive integer.");
}
const io = new NodeIO();
const source = await io.read(sourceFile);
const scene = source.getRoot().getDefaultScene() ?? source.getRoot().listScenes()[0];
if (!scene) throw new Error("Native collision source has no scene.");
const positions = [];
scene.traverse((node) => {
const mesh = node.getMesh();
if (!mesh) return;
const matrix = new Matrix4().fromArray(node.getWorldMatrix());
const reverse = matrix.determinant() < 0;
for (const primitive of mesh.listPrimitives()) {
if (primitive.getMode() !== 4) throw new Error("Collision must contain triangle primitives.");
const position = primitive.getAttribute("POSITION");
const index = primitive.getIndices();
const count = index?.getCount() ?? position?.getCount() ?? 0;
if (!position || count % 3) throw new Error("Invalid collision triangle positions.");
for (let triangle = 0; triangle < count; triangle += 3) {
for (const corner of reverse ? [0, 2, 1] : [0, 1, 2]) {
const vertex = index ? index.getScalar(triangle + corner) : triangle + corner;
const point = new Vector3(...position.getElement(vertex, [])).applyMatrix4(matrix);
if (!point.toArray().every(Number.isFinite)) throw new Error("Non-finite collision coordinate.");
positions.push(...point.toArray());
}
}
}
});
if (!positions.length) throw new Error("Native collision source is empty.");
await mkdir(outputDirectory, { recursive: true });
const records = [];
for (let begin = 0; begin < positions.length; begin += maximumTriangles * 9) {
const document = new Document();
const buffer = document.createBuffer();
const targetScene = document.createScene("native-options-and-source-supports");
document.getRoot().setDefaultScene(targetScene);
const array = new Float32Array(positions.slice(begin, begin + maximumTriangles * 9));
const primitive = document.createPrimitive().setAttribute("POSITION", document.createAccessor("collision-positions", buffer).setType("VEC3").setArray(array));
targetScene.addChild(document.createNode("collision").setMesh(document.createMesh().addPrimitive(primitive)));
document.getRoot().setExtras({
collisionMode: "respect-options", sourceCandidateSha256: sourceSha256 ?? null,
triangleRange: [begin / 9, begin / 9 + array.length / 9],
});
const file = `${slug}-collision-${String(records.length).padStart(3, "0")}.glb`;
await io.write(path.join(outputDirectory, file), document);
records.push({ file, triangles: array.length / 9, nativeSerializedCollision: true, derivedSourceAnchorSupport: true });
}
return records;
}