Skip to content

Instantly share code, notes, and snippets.

@lukevanin
Created March 18, 2025 11:05
Show Gist options
  • Save lukevanin/7db569b741b4170849d288ecdb9f1885 to your computer and use it in GitHub Desktop.
Save lukevanin/7db569b741b4170849d288ecdb9f1885 to your computer and use it in GitHub Desktop.
import * as THREE from 'three';
// TODO: Change the navigation mesh completely. Use a texturemap on the navmesh to indicate walkable regions. Render the textures using an orthographic camera set at the player's head height.
export function cutHolesInFloor(group: THREE.Group, floorMesh: THREE.Mesh): void {
// Helper function to collect all meshes in the group, including nested ones
function getAllMeshes(object: THREE.Object3D): THREE.Mesh[] {
const meshes: THREE.Mesh[] = [];
object.traverse((child) => {
if (child instanceof THREE.Mesh) {
meshes.push(child);
}
});
return meshes;
}
// Compute the bounding box of the floor mesh
floorMesh.geometry.computeBoundingBox();
const bbox: THREE.Box3 | null = floorMesh.geometry.boundingBox;
if (!bbox) {
console.error('Floor geometry does not have a bounding box');
return;
}
// Ensure the floor is flat in the Y-axis (thickness is negligible)
if (Math.abs(bbox.min.y - bbox.max.y) > 1e-6) {
console.error('Floor is not flat in Y');
return;
}
const shapeOrigin: THREE.Vector2 = new THREE.Vector2(bbox.min.x, bbox.min.z);
const shapeSize: THREE.Vector2 = new THREE.Vector2(bbox.max.x - bbox.min.x, bbox.max.z - bbox.min.z);
// Define the outer shape of the floor based on its bounding box
const outerShape: THREE.Shape = new THREE.Shape();
outerShape.moveTo(0, 0);
outerShape.lineTo(shapeSize.x, 0);
outerShape.lineTo(shapeSize.x, shapeSize.y);
outerShape.lineTo(0, shapeSize.y);
outerShape.lineTo(0, 0);
// Transformation matrix to convert from world to floor coordinates
const worldToFloor: THREE.Matrix4 = floorMesh.matrixWorld.clone().invert();
const holes: THREE.Path[] = [];
// Get all meshes from the group, including those in nested groups
const allMeshes = getAllMeshes(group);
allMeshes.forEach((mesh: THREE.Mesh) => {
mesh.geometry.computeBoundingBox();
const localBox: THREE.Box3 | null = mesh.geometry.boundingBox;
if (!localBox) return;
const min: THREE.Vector3 = localBox.min;
const max: THREE.Vector3 = localBox.max;
// Define the 8 corners of the mesh's bounding box
const verticesLocal: THREE.Vector3[] = [
new THREE.Vector3(min.x, min.y, min.z),
new THREE.Vector3(max.x, min.y, min.z),
new THREE.Vector3(max.x, max.y, min.z),
new THREE.Vector3(min.x, max.y, min.z),
new THREE.Vector3(min.x, min.y, max.z),
new THREE.Vector3(max.x, min.y, max.z),
new THREE.Vector3(max.x, max.y, max.z),
new THREE.Vector3(min.x, max.y, max.z),
];
// Transform vertices to floor's local coordinate system
const transform: THREE.Matrix4 = worldToFloor.clone().multiply(mesh.matrixWorld);
const verticesFloor: THREE.Vector3[] = verticesLocal.map(v =>
v.clone().applyMatrix4(transform)
);
// Define edges of the bounding box (connecting the 8 corners)
const edges: [number, number][] = [
[0, 1], [1, 2], [2, 3], [3, 0], // Bottom face
[4, 5], [5, 6], [6, 7], [7, 4], // Top face
[0, 4], [1, 5], [2, 6], [3, 7], // Vertical edges
];
// Find intersection points with the floor plane (y = 0)
const intersectionPoints: THREE.Vector3[] = [];
edges.forEach(([i, j]) => {
const v1: THREE.Vector3 = verticesFloor[i];
const v2: THREE.Vector3 = verticesFloor[j];
if ((v1.y < 0 && v2.y > 0) || (v1.y > 0 && v2.y < 0)) {
const t: number = -v1.y / (v2.y - v1.y);
const point: THREE.Vector3 = v1.clone().lerp(v2, t);
intersectionPoints.push(point);
}
});
// Check if there are at least three intersection points.
if (intersectionPoints.length < 3) {
return;
}
// Convert 3D intersection points to 2D for hole creation
const points2D: THREE.Vector2[] = intersectionPoints.map(p => new THREE.Vector2(p.x - shapeOrigin.x, p.z - shapeOrigin.y));
// TODO: Check if at least two points are contained within the outer shape.
// Check if all of the points are contained within the outer shape
const allPointsContained: boolean = points2D.every(p => {
// Check if point is within the bounds defined by shapeSize
return p.x >= 0 && p.x <= shapeSize.x &&
p.y >= 0 && p.y <= shapeSize.y;
});
if (!allPointsContained) {
return;
}
// Compute centroid to sort points in a consistent order
const centroid2D: THREE.Vector2 = points2D
.reduce((sum, p) => sum.add(p), new THREE.Vector2())
.divideScalar(points2D.length);
// Sort points angularly around the centroid to form a closed loop
points2D.sort((a, b) => {
const vecA: THREE.Vector2 = a.clone().sub(centroid2D);
const vecB: THREE.Vector2 = b.clone().sub(centroid2D);
return Math.atan2(vecA.y, vecA.x) - Math.atan2(vecB.y, vecB.x);
});
points2D.reverse(); // Ensure counterclockwise order
// TODO: If the hole crosses the edge of the outer shape then modify the boundary of the outer shape to include the hole.
// Create a hole path and add it to the list
const holePath: THREE.Path = new THREE.Path();
holePath.setFromPoints(points2D);
holes.push(holePath);
});
if (holes.length === 0) {
console.log('No holes found');
return;
}
console.log(`Cut ${holes.length} holes in floor mesh`);
// Assign holes to the outer shape
outerShape.holes = holes;
// Create new geometry with holes and orient it correctly
const geometry: THREE.ShapeGeometry = new THREE.ShapeGeometry(outerShape);
geometry.rotateX(-Math.PI / 2); // Align with XZ plane
geometry.translate(shapeOrigin.x, 0, -shapeOrigin.y);
// Update the floor mesh with the new geometry
floorMesh.geometry.dispose();
floorMesh.geometry = geometry;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment