2026-04-04 07:55:42 +02:00
|
|
|
import {
|
|
|
|
|
BoxGeometry,
|
|
|
|
|
BufferGeometry,
|
|
|
|
|
Float32BufferAttribute,
|
|
|
|
|
Group,
|
|
|
|
|
Mesh,
|
|
|
|
|
MeshBasicMaterial,
|
|
|
|
|
Vector3,
|
|
|
|
|
type Material
|
|
|
|
|
} from "three";
|
|
|
|
|
import { ConvexGeometry } from "three/examples/jsm/geometries/ConvexGeometry.js";
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
GeneratedModelBoxCollider,
|
|
|
|
|
GeneratedModelCollider,
|
|
|
|
|
GeneratedModelCompoundCollider,
|
|
|
|
|
GeneratedModelHeightfieldCollider,
|
|
|
|
|
GeneratedModelTriMeshCollider
|
|
|
|
|
} from "./model-instance-collider-generation";
|
|
|
|
|
|
|
|
|
|
const DEBUG_COLLIDER_COLORS = {
|
|
|
|
|
simple: 0x87d2ff,
|
|
|
|
|
terrain: 0x7be7b4,
|
|
|
|
|
static: 0xffc66d,
|
2026-04-11 16:30:55 +02:00
|
|
|
"static-simple": 0xa3ff9c,
|
2026-04-04 07:55:42 +02:00
|
|
|
dynamic: 0xff8b7a
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
function createWireframeMaterial(color: number): MeshBasicMaterial {
|
|
|
|
|
return new MeshBasicMaterial({
|
|
|
|
|
color,
|
|
|
|
|
wireframe: true,
|
|
|
|
|
transparent: true,
|
|
|
|
|
opacity: 0.85,
|
|
|
|
|
depthWrite: false,
|
|
|
|
|
toneMapped: false
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function markDebugMesh(mesh: Mesh) {
|
|
|
|
|
mesh.userData.shadowIgnored = true;
|
|
|
|
|
mesh.userData.nonPickable = true;
|
|
|
|
|
mesh.renderOrder = 3_500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createBoxColliderDebugMesh(collider: GeneratedModelBoxCollider): Mesh {
|
|
|
|
|
const mesh = new Mesh(new BoxGeometry(collider.size.x, collider.size.y, collider.size.z), createWireframeMaterial(DEBUG_COLLIDER_COLORS.simple));
|
|
|
|
|
mesh.position.set(collider.center.x, collider.center.y, collider.center.z);
|
|
|
|
|
markDebugMesh(mesh);
|
|
|
|
|
return mesh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createTriMeshColliderDebugMesh(collider: GeneratedModelTriMeshCollider): Mesh {
|
|
|
|
|
const geometry = new BufferGeometry();
|
|
|
|
|
|
|
|
|
|
geometry.setAttribute("position", new Float32BufferAttribute(collider.vertices, 3));
|
|
|
|
|
geometry.setIndex(Array.from(collider.indices));
|
|
|
|
|
|
|
|
|
|
const mesh = new Mesh(geometry, createWireframeMaterial(DEBUG_COLLIDER_COLORS.static));
|
|
|
|
|
markDebugMesh(mesh);
|
|
|
|
|
return mesh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createHeightfieldColliderDebugMesh(collider: GeneratedModelHeightfieldCollider): Mesh {
|
|
|
|
|
const vertices: number[] = [];
|
|
|
|
|
const indices: number[] = [];
|
|
|
|
|
const width = collider.maxX - collider.minX;
|
|
|
|
|
const depth = collider.maxZ - collider.minZ;
|
|
|
|
|
|
|
|
|
|
for (let zIndex = 0; zIndex < collider.cols; zIndex += 1) {
|
|
|
|
|
const zLerp = collider.cols === 1 ? 0 : zIndex / (collider.cols - 1);
|
|
|
|
|
const z = collider.minZ + depth * zLerp;
|
|
|
|
|
|
|
|
|
|
for (let xIndex = 0; xIndex < collider.rows; xIndex += 1) {
|
|
|
|
|
const xLerp = collider.rows === 1 ? 0 : xIndex / (collider.rows - 1);
|
|
|
|
|
const x = collider.minX + width * xLerp;
|
|
|
|
|
const y = collider.heights[xIndex + zIndex * collider.rows];
|
|
|
|
|
|
|
|
|
|
vertices.push(x, y, z);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let zIndex = 0; zIndex < collider.cols - 1; zIndex += 1) {
|
|
|
|
|
for (let xIndex = 0; xIndex < collider.rows - 1; xIndex += 1) {
|
|
|
|
|
const topLeft = xIndex + zIndex * collider.rows;
|
|
|
|
|
const topRight = topLeft + 1;
|
|
|
|
|
const bottomLeft = topLeft + collider.rows;
|
|
|
|
|
const bottomRight = bottomLeft + 1;
|
|
|
|
|
|
|
|
|
|
indices.push(topLeft, bottomLeft, bottomRight, topLeft, bottomRight, topRight);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const geometry = new BufferGeometry();
|
|
|
|
|
|
|
|
|
|
geometry.setAttribute("position", new Float32BufferAttribute(vertices, 3));
|
|
|
|
|
geometry.setIndex(indices);
|
|
|
|
|
|
|
|
|
|
const mesh = new Mesh(geometry, createWireframeMaterial(DEBUG_COLLIDER_COLORS.terrain));
|
|
|
|
|
markDebugMesh(mesh);
|
|
|
|
|
return mesh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createCompoundColliderDebugGroup(collider: GeneratedModelCompoundCollider): Group {
|
|
|
|
|
const group = new Group();
|
2026-04-11 16:38:53 +02:00
|
|
|
const color = collider.mode === "static-simple" ? DEBUG_COLLIDER_COLORS["static-simple"] : DEBUG_COLLIDER_COLORS.dynamic;
|
2026-04-04 07:55:42 +02:00
|
|
|
|
|
|
|
|
for (const piece of collider.pieces) {
|
2026-04-11 16:30:55 +02:00
|
|
|
if (piece.kind === "box") {
|
|
|
|
|
const mesh = new Mesh(new BoxGeometry(piece.size.x, piece.size.y, piece.size.z), createWireframeMaterial(color));
|
|
|
|
|
mesh.position.set(piece.center.x, piece.center.y, piece.center.z);
|
|
|
|
|
markDebugMesh(mesh);
|
|
|
|
|
group.add(mesh);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 07:55:42 +02:00
|
|
|
const points: Vector3[] = [];
|
|
|
|
|
|
|
|
|
|
for (let index = 0; index < piece.points.length; index += 3) {
|
|
|
|
|
points.push(new Vector3(piece.points[index], piece.points[index + 1], piece.points[index + 2]));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 16:30:55 +02:00
|
|
|
const mesh = new Mesh(new ConvexGeometry(points), createWireframeMaterial(color));
|
2026-04-04 07:55:42 +02:00
|
|
|
markDebugMesh(mesh);
|
|
|
|
|
group.add(mesh);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return group;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createModelColliderDebugGroup(collider: GeneratedModelCollider): Group {
|
|
|
|
|
const group = new Group();
|
|
|
|
|
|
|
|
|
|
switch (collider.kind) {
|
|
|
|
|
case "box":
|
|
|
|
|
group.add(createBoxColliderDebugMesh(collider));
|
|
|
|
|
break;
|
|
|
|
|
case "trimesh":
|
|
|
|
|
group.add(createTriMeshColliderDebugMesh(collider));
|
|
|
|
|
break;
|
|
|
|
|
case "heightfield":
|
|
|
|
|
group.add(createHeightfieldColliderDebugMesh(collider));
|
|
|
|
|
break;
|
|
|
|
|
case "compound":
|
|
|
|
|
group.add(createCompoundColliderDebugGroup(collider));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
group.userData.nonPickable = true;
|
|
|
|
|
return group;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function disposeMaterial(material: Material | Material[]) {
|
|
|
|
|
if (Array.isArray(material)) {
|
|
|
|
|
for (const item of material) {
|
|
|
|
|
item.dispose();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
material.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function disposeModelColliderDebugGroup(group: Group) {
|
|
|
|
|
group.traverse((object) => {
|
|
|
|
|
const maybeMesh = object as Mesh & { isMesh?: boolean };
|
|
|
|
|
|
|
|
|
|
if (maybeMesh.isMesh !== true) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
maybeMesh.geometry.dispose();
|
|
|
|
|
disposeMaterial(maybeMesh.material);
|
|
|
|
|
});
|
|
|
|
|
}
|