61 lines
3.1 KiB
JavaScript
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;
|
|
}
|