forked from phenom/healer-man
128 lines
4.9 KiB
TypeScript
128 lines
4.9 KiB
TypeScript
import {
|
|
Float32BufferAttribute,
|
|
Group,
|
|
Mesh,
|
|
type Object3D,
|
|
} from "three";
|
|
import type { DungeonTraversalRamp } from "../game/dungeonTypes";
|
|
import { smoothDungeonStairGeometry } from "../game/dungeonTraversalRamps";
|
|
|
|
export interface PreparedCollisionTrimesh {
|
|
readonly id: string;
|
|
readonly vertices: ArrayLike<number>;
|
|
readonly indices: ArrayLike<number>;
|
|
}
|
|
|
|
// A surface must be within 30 degrees of vertical to occlude combat sight.
|
|
// This deliberately rejects floors, ramps, slopes, and ceilings.
|
|
const MAX_SIGHT_BLOCKER_NORMAL_Y = 0.5;
|
|
|
|
export function prepareCollision(root: Object3D, ramps: readonly DungeonTraversalRamp[] = []): Group {
|
|
root.updateWorldMatrix(true, true);
|
|
const inverseRootMatrix = root.matrixWorld.clone().invert();
|
|
const prepared = new Group();
|
|
prepared.name = `${root.name || "collision"}-prepared`;
|
|
root.traverse((node) => {
|
|
const mesh = node as Mesh;
|
|
if (!mesh.isMesh) return;
|
|
const geometry = mesh.geometry.clone();
|
|
const position = geometry.getAttribute("position");
|
|
if (position.normalized || !(position.array instanceof Float32Array)) {
|
|
const dequantized = new Float32Array(position.count * position.itemSize);
|
|
for (let vertex = 0; vertex < position.count; vertex += 1) {
|
|
const offset = vertex * position.itemSize;
|
|
dequantized[offset] = position.getX(vertex);
|
|
if (position.itemSize > 1) dequantized[offset + 1] = position.getY(vertex);
|
|
if (position.itemSize > 2) dequantized[offset + 2] = position.getZ(vertex);
|
|
if (position.itemSize > 3) dequantized[offset + 3] = position.getW(vertex);
|
|
}
|
|
geometry.setAttribute(
|
|
"position",
|
|
new Float32BufferAttribute(dequantized, position.itemSize),
|
|
);
|
|
}
|
|
geometry.applyMatrix4(inverseRootMatrix.clone().multiply(mesh.matrixWorld));
|
|
if (ramps.length) smoothDungeonStairGeometry(geometry, ramps);
|
|
const preparedMesh = new Mesh(geometry, mesh.material);
|
|
preparedMesh.name = mesh.name;
|
|
preparedMesh.visible = false;
|
|
prepared.add(preparedMesh);
|
|
});
|
|
return prepared;
|
|
}
|
|
|
|
export function collisionTrimeshes(root: Object3D): readonly PreparedCollisionTrimesh[] {
|
|
const result: PreparedCollisionTrimesh[] = [];
|
|
root.traverse((node) => {
|
|
const mesh = node as Mesh;
|
|
if (!mesh.isMesh) return;
|
|
const position = mesh.geometry.getAttribute("position");
|
|
const index = mesh.geometry.index;
|
|
result.push({
|
|
id: mesh.uuid,
|
|
vertices: position.array,
|
|
indices: index?.array
|
|
?? Uint32Array.from({ length: position.count }, (_, vertex) => vertex),
|
|
});
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export function collisionLineOfSightTrimeshes(root: Object3D): readonly PreparedCollisionTrimesh[] {
|
|
const result: PreparedCollisionTrimesh[] = [];
|
|
root.traverse((node) => {
|
|
const mesh = node as Mesh;
|
|
if (!mesh.isMesh) return;
|
|
const position = mesh.geometry.getAttribute("position");
|
|
const sourceIndices = mesh.geometry.index?.array
|
|
?? Uint32Array.from({ length: position.count }, (_, vertex) => vertex);
|
|
const blockerSourceIndices: number[] = [];
|
|
|
|
for (let offset = 0; offset + 2 < sourceIndices.length; offset += 3) {
|
|
const first = Number(sourceIndices[offset]);
|
|
const second = Number(sourceIndices[offset + 1]);
|
|
const third = Number(sourceIndices[offset + 2]);
|
|
const firstX = position.getX(first);
|
|
const firstY = position.getY(first);
|
|
const firstZ = position.getZ(first);
|
|
const abX = position.getX(second) - firstX;
|
|
const abY = position.getY(second) - firstY;
|
|
const abZ = position.getZ(second) - firstZ;
|
|
const acX = position.getX(third) - firstX;
|
|
const acY = position.getY(third) - firstY;
|
|
const acZ = position.getZ(third) - firstZ;
|
|
const normalX = abY * acZ - abZ * acY;
|
|
const normalY = abZ * acX - abX * acZ;
|
|
const normalZ = abX * acY - abY * acX;
|
|
const normalLength = Math.hypot(normalX, normalY, normalZ);
|
|
if (normalLength <= Number.EPSILON) continue;
|
|
if (Math.abs(normalY) / normalLength > MAX_SIGHT_BLOCKER_NORMAL_Y) continue;
|
|
blockerSourceIndices.push(first, second, third);
|
|
}
|
|
|
|
if (!blockerSourceIndices.length) return;
|
|
const remappedVertices: number[] = [];
|
|
const remappedIndices = new Uint32Array(blockerSourceIndices.length);
|
|
const remap = new Map<number, number>();
|
|
blockerSourceIndices.forEach((sourceIndex, indexOffset) => {
|
|
let remappedIndex = remap.get(sourceIndex);
|
|
if (remappedIndex === undefined) {
|
|
remappedIndex = remap.size;
|
|
remap.set(sourceIndex, remappedIndex);
|
|
remappedVertices.push(
|
|
position.getX(sourceIndex),
|
|
position.getY(sourceIndex),
|
|
position.getZ(sourceIndex),
|
|
);
|
|
}
|
|
remappedIndices[indexOffset] = remappedIndex;
|
|
});
|
|
result.push({
|
|
id: `${mesh.uuid}-line-of-sight`,
|
|
vertices: Float32Array.from(remappedVertices),
|
|
indices: remappedIndices,
|
|
});
|
|
});
|
|
return result;
|
|
}
|