File size: 4,906 Bytes
fae8ddf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | /**
* 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;
|