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; }