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, "static-simple": 0xa3ff9c, 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(); const color = collider.mode === "static-simple" ? DEBUG_COLLIDER_COLORS["static-simple"] : DEBUG_COLLIDER_COLORS.dynamic; for (const piece of collider.pieces) { 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; } 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])); } const mesh = new Mesh(new ConvexGeometry(points), createWireframeMaterial(color)); 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); }); }