BabylonJS 12일차 : 충돌과 중력(걷기 기초) — collisions, gravity, ellipsoid / UniversalCamera 걷기
3D 씬에서 “걷기”가 어색해지는 가장 흔한 이유는 카메라(또는 캐릭터)가 바닥을 뚫거나, 벽을 통과하거나, 경사에서 미끄러지듯 흔들리는 문제입니다. Babylon.js는 물리 엔진 없이도 기본 충돌 시스템(collisions)과 중력(gravity)을 조합해, 초보자도 빠르게 ‘바닥 위를 걷는 느낌’을 만들 수 있게 해줍니다. 이번 글에서는 UniversalCamera로 1인칭/3인칭의 기초가 되는 “걷기”를 만들고, ellipsoid가 왜 중요한지까지 깊게 정리합니다.
목차
- 왜 바닥을 뚫을까: 좌표 이동 vs 충돌 이동
- 핵심 개념 3가지: collisions, gravity, ellipsoid
- 필수 설정 요약 표
- 실습: UniversalCamera로 “걷기” 만들기
- 튜닝 포인트: 키감/안정성/스폰 위치
- 문제 해결 체크리스트(바닥 뚫림/끼임/떨림)
- 추가로 생각해볼 점(계단/경사/물리 엔진 전환)
- 블로그 최적화 정보
핵심 포인트: 왜 바닥을 뚫을까
- 단순히
position.y -= 0.1처럼 좌표를 직접 바꾸면, 그 과정에서 “바닥과의 충돌 검사”가 생략되어 관통이 발생할 수 있습니다. - Babylon.js 충돌 시스템은 “이동하려는 벡터”를 기준으로, 충돌을 고려한 이동(
moveWithCollisions)을 수행합니다. - 즉, “걷기”는 좌표를 마음대로 바꾸는 것이 아니라, 충돌 가능한 방식으로 이동해야 안정적입니다.
상세 설명: collisions, gravity, ellipsoid를 쉬운 비유로 이해하기
1) collisions: “통과 금지” 규칙
checkCollisions를 켜면, 해당 객체(카메라/메시)가 움직일 때 주변의 충돌 대상과 부딪히는지 계산합니다. 중요한 점은 “모든 메시가 자동으로 벽이 되는 것”이 아니라, 충돌 대상으로 지정된 메시만 벽/바닥 역할을 한다는 것입니다.
- 카메라 쪽:
camera.checkCollisions = true - 바닥/벽 쪽:
ground.checkCollisions = true,wall.checkCollisions = true
2) gravity: “아래로 끌어당기는 힘”
scene.gravity는 씬 전체에 적용할 중력 방향과 크기(벡터)입니다. camera.applyGravity = true를 켜면, 카메라는 매 프레임 중력 방향으로 이동하려고 하며, 그 이동이 충돌 시스템과 결합되어 바닥 위에 ‘안착’합니다.
- 중력 설정:
scene.gravity = new BABYLON.Vector3(0, -0.9, 0)처럼y에 음수 - 카메라에 적용:
camera.applyGravity = true
3) ellipsoid: “사람의 몸통을 대신하는 충돌 캡슐”
카메라는 점(point)처럼 보이지만, 사람이 걷는 느낌을 내려면 “몸통 부피”가 필요합니다. Babylon.js의 기본 충돌은 카메라를 타원체(ellipsoid)로 취급하여, 벽/바닥과의 거리를 계산합니다. 이 값이 너무 작으면 문틈/모서리에 과하게 끼고, 너무 크면 좁은 통로를 못 지나갑니다.
- 일반적인 사람 크기 예시:
camera.ellipsoid = new BABYLON.Vector3(0.5, 1.0, 0.5) - 시점 보정(중요):
camera.ellipsoidOffset로 “눈 위치”를 올려 자연스럽게 만듭니다.
필수 설정 요약 표
| 설정 | 어디에 적용 | 역할 | 초보자 실수 포인트 |
|---|---|---|---|
scene.collisionsEnabled = true |
Scene | 충돌 시스템 활성화(안전 장치) | 켜지지 않으면 카메라/메시에 체크해도 효과가 없을 수 있습니다. |
mesh.checkCollisions = true |
Ground/Wall/Obstacle | 해당 메시를 벽/바닥으로 취급 | 바닥에 안 켜면 중력이 있어도 그대로 떨어집니다. |
camera.checkCollisions = true |
Camera | 카메라 이동 시 충돌 고려 | 이것이 꺼져 있으면 모든 것을 통과합니다. |
scene.gravity + camera.applyGravity |
Scene + Camera | 바닥에 붙여 걷는 느낌 형성 | 바닥 충돌이 없으면 중력은 “낙하”로만 작동합니다. |
camera.ellipsoid / ellipsoidOffset |
Camera | 사람의 몸통 부피 + 눈 위치 보정 | 값이 부적절하면 끼임/떠있음/문 통과 불가가 생깁니다. |
실행 단계: UniversalCamera로 “걷기” 실습
아래 예제는 “바닥 + 벽(장애물)”을 만들고, UniversalCamera가 WASD로 이동하면서 바닥을 뚫지 않도록 구성합니다. 또한 ellipsoid와 ellipsoidOffset를 적용해 ‘눈높이’가 자연스럽게 느껴지도록 합니다.
1) 기본 씬 구성
- 바닥(ground)을 만들고
checkCollisions를 켭니다. - 박스 몇 개를 장애물로 만들고 역시
checkCollisions를 켭니다. - 씬 중력과 카메라 중력을 함께 켭니다.
2) 복사해서 바로 실행 가능한 예제(HTML + JavaScript)
아래 코드는 HTML 편집기/플레이그라운드에 그대로 붙여 사용할 수 있도록 한 파일 형태로 작성했습니다. 코드 블록 내의 <, >, &는 Blogger에서 깨지지 않도록 이스케이프 처리되어 있습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Babylon.js Collisions + Gravity Walking</title>
<style>
html, body { width:100%; height:100%; margin:0; overflow:hidden; }
#renderCanvas { width:100%; height:100%; touch-action:none; display:block; }
</style>
</head>
<body>
<canvas id="renderCanvas"></canvas>
<!-- Babylon.js CDN은 상황에 맞게 교체하세요. (Playground 사용 시 불필요) -->
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script>
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const createScene = () => {
const scene = new BABYLON.Scene(engine);
// 1) 조명
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
light.intensity = 0.9;
// 2) 충돌 + 중력(안전하게 명시)
scene.collisionsEnabled = true;
scene.gravity = new BABYLON.Vector3(0, -0.9, 0);
// 3) 바닥
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 60, height: 60 }, scene);
ground.checkCollisions = true;
const groundMat = new BABYLON.StandardMaterial("groundMat", scene);
groundMat.diffuseColor = new BABYLON.Color3(0.85, 0.85, 0.88);
ground.material = groundMat;
// 4) 장애물(벽 역할)
const wallMat = new BABYLON.StandardMaterial("wallMat", scene);
wallMat.diffuseColor = new BABYLON.Color3(0.75, 0.78, 0.82);
for (let i = 0; i < 8; i++) {
const box = BABYLON.MeshBuilder.CreateBox("box" + i, { size: 2 }, scene);
box.position = new BABYLON.Vector3(-10 + i * 3, 1, 6);
box.checkCollisions = true;
box.material = wallMat;
}
// 5) UniversalCamera: 걷기(키보드/마우스)
const camera = new BABYLON.UniversalCamera("cam", new BABYLON.Vector3(0, 3, -10), scene);
camera.attachControl(canvas, true);
// 이동 감각(원하는 값으로 튜닝)
camera.speed = 0.4; // 걷기 속도
camera.inertia = 0.75; // 관성(클수록 미끄러운 느낌)
camera.angularSensibility = 4000; // 마우스 회전 민감도(작을수록 민감)
// 6) 충돌 + 중력 적용(핵심)
camera.checkCollisions = true;
camera.applyGravity = true;
// 7) 사람의 몸통(ellipsoid) + 눈높이(ellipsoidOffset)
// 대략: 반지름 0.5, 키(충돌 높이) 1.0, 반지름 0.5
camera.ellipsoid = new BABYLON.Vector3(0.5, 1.0, 0.5);
// 카메라 위치는 보통 “눈”입니다. 충돌체는 몸통이므로 눈높이를 올려줍니다.
// ellipsoidOffset.y를 올리면 바닥에 발이 붙고 시점은 사람 눈높이에 가까워집니다.
camera.ellipsoidOffset = new BABYLON.Vector3(0, 1.0, 0);
// 8) 키 바인딩(WASD + 화살표)
camera.keysUp = [87, 38]; // W, Up
camera.keysDown = [83, 40]; // S, Down
camera.keysLeft = [65, 37]; // A, Left
camera.keysRight = [68, 39]; // D, Right
// 9) 충돌 이벤트(디버깅에 유용)
camera.onCollide = (collidedMesh) => {
// console.log("Collided with:", collidedMesh.name);
};
// 10) 하늘/배경
scene.clearColor = new BABYLON.Color4(0.95, 0.97, 1.0, 1.0);
// 11) (선택) 충돌체 시각화: 카메라 ellipsoid를 따라다니는 와이어프레임
const colliderViz = BABYLON.MeshBuilder.CreateSphere("colliderViz", { diameter: 1 }, scene);
colliderViz.isPickable = false;
colliderViz.material = new BABYLON.StandardMaterial("vizMat", scene);
colliderViz.material.wireframe = true;
colliderViz.material.alpha = 0.35;
scene.onBeforeRenderObservable.add(() => {
// ellipsoid는 타원체이므로 스케일로 흉내냅니다(정확한 충돌체 렌더는 아님).
colliderViz.scaling.x = camera.ellipsoid.x * 2;
colliderViz.scaling.y = camera.ellipsoid.y * 2;
colliderViz.scaling.z = camera.ellipsoid.z * 2;
colliderViz.position.copyFrom(camera.position.add(camera.ellipsoidOffset));
});
return scene;
};
const scene = createScene();
engine.runRenderLoop(() => {
scene.render();
});
window.addEventListener("resize", () => {
engine.resize();
});
</script>
</body>
</html>
튜닝 포인트: “걷기”가 더 자연스러워지는 설정들
- 스폰 높이: 시작 위치가 바닥과 겹치면 충돌 계산이 불안정합니다.
예:new BABYLON.Vector3(0, 3, -10)처럼 바닥보다 위에서 시작합니다. - 중력 크기:
scene.gravity.y가 너무 작으면 바닥에 “떠 있는” 느낌이 날 수 있고, 너무 크면 경사/모서리에서 떨림이 커질 수 있습니다.
초보자 기준으로-0.6~-1.2범위를 자주 사용합니다. - ellipsoid: 통로 폭/문 크기/캐릭터 크기에 맞춰 조절합니다.
너무 크면 문에 못 들어가고, 너무 작으면 모서리를 파고들어 “끼임”이 늘어납니다. - ellipsoidOffset: “시점(눈)”과 “충돌체(몸통)”의 관계를 맞춥니다.
Offset이 없으면 카메라가 바닥에 너무 가까워 보이거나, 바닥을 스치며 흔들리는 느낌이 날 수 있습니다. - camera.inertia: 키 입력을 떼도 살짝 미끄러지는 느낌을 조절합니다.
정밀한 컨트롤이 필요하면 낮추고, 부드러운 이동이면 높입니다.
문제 해결 체크리스트: 바닥 뚫림/벽 통과/떨림
| 증상 | 원인 후보 | 해결 방법 |
|---|---|---|
| 카메라가 바닥을 뚫고 떨어짐 | 바닥에 checkCollisions 미설정, 또는 camera.applyGravity만 켜고 충돌 미설정 |
ground.checkCollisions = true, camera.checkCollisions = true, scene.collisionsEnabled = true를 우선 확인합니다. |
| 벽/박스를 그대로 통과함 | 장애물 메시의 checkCollisions 미설정 |
벽/장애물 메시 모두에 checkCollisions = true를 켭니다. |
| 바닥에서 미세하게 떨림 | 중력이 과하거나, 시작 위치가 바닥과 너무 가깝거나, ellipsoid/offset이 부적절 | 스폰 높이를 올리고, scene.gravity.y를 완만하게 조정하며, ellipsoidOffset.y를 점진적으로 튜닝합니다. |
| 좁은 문/복도에서 자주 끼임 | ellipsoid가 너무 큼 | camera.ellipsoid.x/z를 줄여 통로 폭에 맞춥니다(예: 0.4~0.45). |
| 바닥에 닿았는데 시점이 너무 낮거나 높음 | ellipsoidOffset 미설정 또는 값 부정확 |
카메라가 ‘눈’이라면, ellipsoidOffset.y를 0.8~1.2 범위에서 맞춥니다. |
추가로 생각해볼 점: 여기서 한 단계 더
- 계단/턱 오르기: 기본 충돌+중력은 계단을 자연스럽게 “오르는” 로직이 부족할 수 있습니다.
실무에서는 계단 메쉬를 경사(램프)로 단순화하거나, 별도의 캐릭터 컨트롤러 로직(스텝 오프셋)을 구현합니다. - 경사면 안정성: 경사면이 복잡한 메시(삼각형이 많은 지형)일수록 끼임/미세 떨림이 늘 수 있습니다.
걷기용 충돌 지형은 가능한 단순한 콜라이더 메시로 분리하는 편이 안정적입니다. - 물리 엔진과의 구분: 이번 방식은 “걷기 체감”을 빠르게 만들기 좋지만, 질량/마찰/탄성 같은 물리적 상호작용을 정확히 다루지는 않습니다.
상호작용(박스 밀기, 점프 후 반동, 라그돌)이 필요해지면 Havok/Ammo 등의 물리 플러그인 전환을 검토합니다. - 3인칭 캐릭터로 확장: 다음 단계는 카메라가 아니라 캐릭터 메시를 움직이고, 카메라는 캐릭터를 따라가게 만드는 구조입니다.
원리는 동일하며, “충돌 대상(캐릭터)”이 카메라에서 캐릭터로 바뀐다고 이해하시면 됩니다.
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

0 댓글