I have a map glb file. I’ve taken the skeld map from Among Us for walking around in and for some reason I can’t collide with it properly. For example I can’t walk on the floor, only on the ceiling plane.
My best guess is that the floor collision mesh is offset vertically but even after fixing it it doesn’t work. I’d love some help.
I’m new to three.js and javascript in general so a lot of it is vibe coding so please forgive me if the answer is obvious. Here’s my main.js file:
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls.js';
import * as CANNON from 'cannon-es';
const PLAYER_RADIUS = 1;
const MOVE_SPEED = 10;
const SCALE_FACTOR = 20;
const JUMP_FORCE = 10;
let canJump = true;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
const camera = new THREE.PerspectiveCamera(
75, window.innerWidth / window.innerHeight, 0.1, 1000
);
camera.position.set(0, PLAYER_RADIUS, 0);
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 0.6));
const dir = new THREE.DirectionalLight(0xffffff, 0.8);
dir.position.set(5, 10, 7.5);
dir.castShadow = true;
scene.add(dir);
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0)
});
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 20;
world.solver.tolerance = 0.001;
const playerBody = new CANNON.Body({
mass: 1,
fixedRotation: true,
linearDamping: 0.9
});
playerBody.addShape(new CANNON.Sphere(PLAYER_RADIUS));
world.addBody(playerBody);
const controls = new PointerLockControls(camera, renderer.domElement);
scene.add(controls.getObject());
const blocker = document.getElementById('blocker');
blocker.addEventListener('click', () => controls.lock());
controls.addEventListener('lock', () => blocker.style.display = 'none');
controls.addEventListener('unlock', () => blocker.style.display = '');
const move = {
forward: 0,
back: 0,
left: 0,
right: 0
};
window.addEventListener('keydown', e => {
if (e.code === 'KeyW') move.forward = 1;
if (e.code === 'KeyS') move.back = 1;
if (e.code === 'KeyA') move.left = 1;
if (e.code === 'KeyD') move.right = 1;
if (e.code === 'Space' && canJump) {
playerBody.velocity.y = JUMP_FORCE;
canJump = false;
}
});
window.addEventListener('keyup', e => {
if (e.code === 'KeyW') move.forward = 0;
if (e.code === 'KeyS') move.back = 0;
if (e.code === 'KeyA') move.left = 0;
if (e.code === 'KeyD') move.right = 0;
});
const groundRay = new CANNON.Ray(
new CANNON.Vec3(),
new CANNON.Vec3(0, -1, 0)
);
groundRay.mode = CANNON.Ray.CLOSEST;
groundRay.skipBackfaces = true;
new GLTFLoader().load(
'amongusSkeld.glb',
({
scene: map
}) => {
map.scale.set(SCALE_FACTOR, SCALE_FACTOR, SCALE_FACTOR);
map.updateMatrixWorld(true);
scene.add(map);
const vertices = [];
const indices = [];
let vOff = 0;
map.traverse(node => {
if (!node.isMesh) return;
const geom = node.geometry.clone();
geom.applyMatrix4(node.matrixWorld);
const pa = geom.attributes.position;
for (let i = 0; i < pa.count; i++) {
vertices.push(pa.getX(i), pa.getY(i), pa.getZ(i));
}
if (geom.index) {
const ix = geom.index;
for (let i = 0; i < ix.count; i += 3) {
indices.push(
ix.getX(i) + vOff,
ix.getX(i + 1) + vOff,
ix.getX(i + 2) + vOff
);
}
} else {
for (let i = 0; i < pa.count; i += 3) {
indices.push(vOff + i, vOff + i + 1, vOff + i + 2);
}
}
vOff += pa.count;
});
console.log(`Merged collision mesh: ${vertices.length/3} verts, ${indices.length/3} tris`);
const triShape = new CANNON.Trimesh(vertices, indices);
const mapBody = new CANNON.Body({
mass: 0
});
mapBody.addShape(triShape);
world.addBody(mapBody);
const colGeom = new THREE.BufferGeometry();
colGeom.setAttribute('position',
new THREE.BufferAttribute(new Float32Array(vertices), 3)
);
colGeom.setIndex(indices);
const colMat = new THREE.MeshBasicMaterial({
color: 0xff0000,
wireframe: true,
transparent: true,
opacity: 0.5
});
const colMesh = new THREE.Mesh(colGeom, colMat);
scene.add(colMesh);
const bbox = new THREE.Box3().setFromObject(map);
const {
min,
max
} = bbox;
const center = bbox.getCenter(new THREE.Vector3());
const rayFrom = new CANNON.Vec3(center.x, max.y + SCALE_FACTOR, center.z);
const rayTo = new CANNON.Vec3(center.x, min.y - SCALE_FACTOR, center.z);
const ray = new CANNON.Ray(rayFrom, rayTo);
const result = new CANNON.RaycastResult();
ray.intersectWorld(
world, {
mode: CANNON.Ray.CLOSEST,
skipBackfaces: true
},
result
);
const spawnY = result.hasHit ?
result.hitPointWorld.y + PLAYER_RADIUS + 0.1 :
max.y + PLAYER_RADIUS + 0.1;
playerBody.position.set(center.x, spawnY, center.z);
controls.getObject().position.set(center.x, spawnY, center.z);
console.log(`Spawn at Y=${spawnY.toFixed(2)}, hit at Y=${result.hasHit ? result.hitPointWorld.y.toFixed(2) : 'none'}`);
},
xhr => console.log(`Loading map: ${(xhr.loaded/xhr.total*100).toFixed(1)}%`),
err => console.error('Map load error:', err)
);
const clock = new THREE.Clock();
const FIXED_DT = 1 / 60;
const euler = new THREE.Euler(0, 0, 0, 'YXZ');
const quat = new THREE.Quaternion();
function animate() {
requestAnimationFrame(animate);
const dt = clock.getDelta();
groundRay.from.copy(playerBody.position);
groundRay.to.copy(playerBody.position);
groundRay.to.y -= PLAYER_RADIUS + 0.1;
const result = new CANNON.RaycastResult();
groundRay.intersectWorld(world, {}, result);
canJump = result.hasHit && result.distance < PLAYER_RADIUS + 0.1;
if (controls.isLocked) {
const forward = new THREE.Vector3();
camera.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
const rightDir = new THREE.Vector3();
rightDir.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
const moveDir = new THREE.Vector3();
moveDir.addScaledVector(forward, move.forward - move.back);
moveDir.addScaledVector(rightDir, move.right - move.left);
if (moveDir.lengthSq() > 0) {
moveDir.normalize().multiplyScalar(MOVE_SPEED);
playerBody.velocity.x = moveDir.x;
playerBody.velocity.z = moveDir.z;
}
controls.getObject().position.copy(playerBody.position);
}
world.step(FIXED_DT, dt, 10);
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});