3d-outdoor-map-explorer / js /engine /CollisionSystem.js
timfromhcs's picture
Deploy 3D Outdoor Map Explorer and POV Walker
fae8ddf verified
Raw History Blame Contribute Delete
4.91 kB
/**
* CollisionSystem.js
* High-performance kinematic collision detection, floor snapping, slope testing,
* and wall sliding against physical collision meshes.
*/
class CollisionSystem {
constructor() {
this.collisionMesh = null;
this.walkableMesh = null;
this.bounds = null;
this.maxSlopeDeg = 45.0;
this.raycaster = new THREE.Raycaster();
this.downVector = new THREE.Vector3(0, -1, 0);
}
/**
* Sets the active collision and walkable meshes for the current map.
*/
setCollisionMesh(colMesh, walkMesh, bounds) {
this.collisionMesh = colMesh;
this.walkableMesh = walkMesh;
this.bounds = bounds;
// Build BVH if library is present
[this.collisionMesh, this.walkableMesh].forEach(mesh => {
if (mesh && mesh.geometry && window.MeshBVH && !mesh.geometry.boundsTree) {
try {
mesh.geometry.computeBoundsTree();
} catch (e) {
console.warn("BVH generation note:", e);
}
}
});
}
/**
* Casts a ray downward to detect ground contact and slope.
* @param {THREE.Vector3} playerPos Base feet position
* @param {number} stepHeight Maximum step height player can step up
* @returns {Object|null} Ground hit info { groundY, normal, slopeDeg, distance }
*/
checkGround(playerPos, stepHeight = 0.04) {
const targetMesh = this.collisionMesh || this.walkableMesh;
if (!targetMesh) {
return { groundY: 0, normal: new THREE.Vector3(0, 1, 0), slopeDeg: 0, distance: playerPos.y };
}
// Ray origin placed slightly above feet
const origin = new THREE.Vector3(playerPos.x, playerPos.y + stepHeight + 0.06, playerPos.z);
this.raycaster.set(origin, this.downVector);
this.raycaster.near = 0.001;
this.raycaster.far = stepHeight + 0.35;
const hits = this.raycaster.intersectObject(targetMesh, true);
if (hits.length > 0) {
const hit = hits[0];
let normal = hit.face ? hit.face.normal.clone() : new THREE.Vector3(0, 1, 0);
normal.transformDirection(hit.object.matrixWorld);
const dot = Math.max(-1, Math.min(1, normal.dot(new THREE.Vector3(0, 1, 0))));
const slopeDeg = Math.acos(dot) * (180 / Math.PI);
return {
groundY: hit.point.y,
normal: normal,
slopeDeg: slopeDeg,
distance: origin.y - hit.point.y
};
}
return null;
}
/**
* Tests horizontal movement against walls and obstacles.
* If an obstacle is hit within radius, velocity is deflected along the wall plane.
* @param {THREE.Vector3} currentPos Current feet position
* @param {THREE.Vector3} desiredVelocity Desired movement velocity
* @param {number} radius Player collision radius
* @param {number} height Player height
* @returns {THREE.Vector3} Adjusted velocity vector
*/
collideAndSlide(currentPos, desiredVelocity, radius = 0.03, height = 0.15) {
const targetMesh = this.collisionMesh || this.walkableMesh;
if (!targetMesh || desiredVelocity.lengthSq() < 1e-8) {
return desiredVelocity.clone();
}
const moveDir = desiredVelocity.clone().normalize();
const testHeights = [height * 0.3, height * 0.7]; // Test at knee and chest height
let adjustedVelocity = desiredVelocity.clone();
for (const h of testHeights) {
const origin = new THREE.Vector3(currentPos.x, currentPos.y + h, currentPos.z);
this.raycaster.set(origin, moveDir);
this.raycaster.near = 0.001;
this.raycaster.far = radius + 0.02;
const hits = this.raycaster.intersectObject(targetMesh, true);
if (hits.length > 0) {
const hit = hits[0];
if (hit.face) {
const wallNormal = hit.face.normal.clone();
wallNormal.transformDirection(hit.object.matrixWorld);
wallNormal.y = 0; // Horizontal wall deflection only
wallNormal.normalize();
// Deflect velocity along wall: v_slide = v - (v . n) * n
const dot = adjustedVelocity.dot(wallNormal);
if (dot < 0) {
adjustedVelocity.sub(wallNormal.multiplyScalar(dot));
}
}
}
}
return adjustedVelocity;
}
/**
* Checks if player has fallen out of the world boundaries.
*/
isOutOfBounds(position) {
if (!this.bounds) return position.y < -5.0;
return position.y < (this.bounds.min[1] - 0.5);
}
}
window.CollisionSystem = CollisionSystem;