Created
March 18, 2025 11:05
-
-
Save lukevanin/7db569b741b4170849d288ecdb9f1885 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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