102 lines
4.0 KiB
JavaScript
102 lines
4.0 KiB
JavaScript
import { readFile } from "node:fs/promises";
|
|
import { MeshoptDecoder } from "meshoptimizer";
|
|
import { Vector3 } from "three";
|
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
import { writeWmoGeometry } from "./build-wmo-arenas.mjs";
|
|
|
|
globalThis.self = globalThis;
|
|
globalThis.ProgressEvent ??= class ProgressEvent {
|
|
constructor(type, values = {}) { this.type = type; Object.assign(this, values); }
|
|
};
|
|
|
|
class UnionFind {
|
|
constructor(size) { this.parent = Array.from({ length: size }, (_, index) => index); }
|
|
find(value) {
|
|
if (this.parent[value] !== value) this.parent[value] = this.find(this.parent[value]);
|
|
return this.parent[value];
|
|
}
|
|
union(left, right) {
|
|
const a = this.find(left);
|
|
const b = this.find(right);
|
|
if (a !== b) this.parent[b] = a;
|
|
}
|
|
}
|
|
|
|
function key(point) {
|
|
return point.map((value) => Math.round(value * 1000)).join(":");
|
|
}
|
|
|
|
async function loadTriangles(file) {
|
|
await MeshoptDecoder.ready;
|
|
const bytes = await readFile(file);
|
|
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
const scene = await new Promise((resolve, reject) => new GLTFLoader()
|
|
.setMeshoptDecoder(MeshoptDecoder)
|
|
.parse(buffer, "", (gltf) => resolve(gltf.scene), reject));
|
|
scene.updateMatrixWorld(true);
|
|
const triangles = [];
|
|
const points = [new Vector3(), new Vector3(), new Vector3()];
|
|
scene.traverse((object) => {
|
|
if (!object.isMesh || !object.geometry?.getAttribute("position")) return;
|
|
const positions = object.geometry.getAttribute("position");
|
|
const indices = object.geometry.getIndex();
|
|
const count = indices?.count ?? positions.count;
|
|
const vertexIndex = (element) => indices ? indices.getX(element) : element;
|
|
for (let element = 0; element < count; element += 3) {
|
|
triangles.push(points.map((point, corner) => [
|
|
...point.fromBufferAttribute(positions, vertexIndex(element + corner)).applyMatrix4(object.matrixWorld).toArray(),
|
|
]));
|
|
}
|
|
});
|
|
return triangles;
|
|
}
|
|
|
|
function distanceSquared(point, target) {
|
|
return (point[0] - target[0]) ** 2 + (point[1] - target[1]) ** 2 + (point[2] - target[2]) ** 2;
|
|
}
|
|
|
|
export async function extractNavigationComponent({ source, output, teamSpawns }) {
|
|
const triangles = await loadTriangles(source);
|
|
const union = new UnionFind(triangles.length);
|
|
const ownerByVertex = new Map();
|
|
for (const [triangleIndex, triangle] of triangles.entries()) {
|
|
for (const point of triangle) {
|
|
const positionKey = key(point);
|
|
const owner = ownerByVertex.get(positionKey);
|
|
if (owner == null) ownerByVertex.set(positionKey, triangleIndex);
|
|
else union.union(owner, triangleIndex);
|
|
}
|
|
}
|
|
const components = new Map();
|
|
for (let index = 0; index < triangles.length; index += 1) {
|
|
const root = union.find(index);
|
|
const component = components.get(root) ?? [];
|
|
component.push(triangles[index]);
|
|
components.set(root, component);
|
|
}
|
|
const scored = [...components.values()].map((component) => {
|
|
const vertices = component.flat();
|
|
const spawnDistances = teamSpawns.map((spawn) => Math.sqrt(Math.min(...vertices.map((point) => distanceSquared(point, spawn)))));
|
|
return { component, spawnDistances, score: Math.max(...spawnDistances) };
|
|
}).sort((left, right) => left.score - right.score || right.component.length - left.component.length);
|
|
const selected = scored[0];
|
|
if (!selected?.component.length) throw new Error(`${source}: no navigation component could be selected.`);
|
|
const values = selected.component.flat(2);
|
|
const positions = new Float32Array(values);
|
|
const indices = Uint32Array.from({ length: positions.length / 3 }, (_, index) => index);
|
|
await writeWmoGeometry(output, [{
|
|
name: "rated-connected-navigation",
|
|
positions,
|
|
normals: null,
|
|
indices,
|
|
collisionIndices: indices,
|
|
flagCounts: {},
|
|
}], false);
|
|
return {
|
|
sourceComponents: components.size,
|
|
triangles: selected.component.length,
|
|
vertices: positions.length / 3,
|
|
spawnDistances: selected.spawnDistances,
|
|
};
|
|
}
|