본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 29. · 2 Views
Three.js 충돌 감지 완벽 가이드
3D 웹 게임에서 캐릭터가 벽을 뚫고 지나가는 문제를 해결하는 충돌 감지 기술을 배웁니다. Bounding Box부터 물리 엔진까지 실전 예제로 완벽하게 마스터해보세요.
목차
1. 충돌 감지 기본 원리
김개발 씨가 Three.js로 첫 3D 게임을 만들었습니다. 캐릭터를 조종하며 신나게 테스트하던 중, 캐릭터가 벽을 그냥 통과해버리는 황당한 상황이 발생했습니다.
선배 박시니어 씨가 웃으며 말했습니다. "충돌 감지를 구현하지 않았네요."
충돌 감지는 3D 공간에서 두 물체가 겹치는지 판단하는 기술입니다. 마치 현실 세계에서 두 공이 부딪히는 것을 감지하는 것처럼, 가상 세계에서도 물체 간의 접촉을 계산해야 합니다.
Three.js는 이를 위해 기하학적 계산 방식을 제공합니다. 이것을 제대로 이해하면 현실감 있는 3D 인터랙션을 구현할 수 있습니다.
다음 코드를 살펴봅시다.
// 두 구체 사이의 충돌 감지
function checkSphereCollision(sphere1, sphere2) {
// 두 구체 중심 간의 거리 계산
const distance = sphere1.position.distanceTo(sphere2.position);
// 반지름의 합
const radiusSum = sphere1.geometry.parameters.radius +
sphere2.geometry.parameters.radius;
// 거리가 반지름 합보다 작으면 충돌
if (distance < radiusSum) {
console.log('충돌 발생!');
return true;
}
return false;
}
김개발 씨는 입사 3개월 차 프론트엔드 개발자입니다. 회사에서 3D 웹 프로젝트를 맡게 되어 Three.js를 배우기 시작했습니다.
첫 프로젝트는 간단한 미로 게임이었습니다. 캐릭터를 만들고, 벽을 만들고, 카메라를 설정하는 것까지는 순조로웠습니다.
하지만 테스트를 시작하자마자 문제가 발생했습니다. 화살표 키로 캐릭터를 움직이면 벽을 그냥 통과해버렸습니다.
마치 유령처럼 모든 장애물을 뚫고 지나갔습니다. 당황한 김개발 씨는 선배에게 도움을 요청했습니다.
박시니어 씨가 코드를 살펴보더니 말했습니다. "아, 충돌 감지를 구현하지 않았네요.
Three.js는 물체를 화면에 그려주기만 할 뿐, 물리 법칙은 우리가 직접 만들어야 해요." 그렇다면 충돌 감지란 정확히 무엇일까요? 쉽게 비유하자면, 충돌 감지는 마치 두 사람이 복도에서 마주칠 때를 감지하는 것과 같습니다.
두 사람 사이의 거리를 계속 측정하다가, 거리가 0에 가까워지면 "부딪힐 것 같다"고 판단하는 것입니다. 3D 공간에서도 동일합니다.
두 물체의 위치를 계속 추적하면서, 서로 겹치는 순간을 포착하는 것입니다. 충돌 감지가 없던 시절에는 어땠을까요?
초창기 3D 게임에서는 충돌 감지를 매우 단순하게 처리했습니다. 게임 맵을 격자로 나누고, 각 칸에 "이동 가능" 또는 "이동 불가능" 플래그를 설정했습니다.
하지만 이 방식은 복잡한 형태의 물체를 표현하기 어려웠습니다. 더 큰 문제는 대각선 이동이나 회전하는 물체를 처리할 때 부정확했다는 점입니다.
바로 이런 문제를 해결하기 위해 기하학적 충돌 감지가 등장했습니다. 기하학적 충돌 감지를 사용하면 정확한 거리 계산이 가능해집니다.
또한 회전, 크기 변화, 복잡한 움직임도 정밀하게 처리할 수 있습니다. 무엇보다 물리 법칙을 자연스럽게 적용할 수 있다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 distanceTo 메서드를 사용하여 두 구체 중심 간의 3D 거리를 계산합니다.
이것은 피타고라스 정리를 3차원으로 확장한 것입니다. 다음으로 각 구체의 반지름을 더합니다.
이 값이 "충돌 임계값"이 됩니다. 마지막으로 실제 거리와 임계값을 비교하여 충돌 여부를 판단합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 자동차 시뮬레이터를 개발한다고 가정해봅시다.
자동차가 가드레일에 충돌하는 순간을 감지해야 합니다. 충돌 감지를 활용하면 정확한 충돌 지점을 계산하고, 차량의 속도를 줄이고, 파손 효과를 표시할 수 있습니다.
많은 웹 기반 3D 제품 뷰어에서 이런 패턴을 적극적으로 사용하고 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 매 프레임마다 모든 물체를 서로 비교하는 것입니다. 물체가 100개라면 약 5,000번의 계산이 필요합니다.
이렇게 하면 성능 문제가 발생할 수 있습니다. 따라서 공간 분할 기법이나 브로드 페이즈 필터링으로 불필요한 계산을 줄여야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 벽을 통과했군요!" 충돌 감지의 기본 원리를 제대로 이해하면 더 현실감 있고 인터랙티브한 3D 웹 경험을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 성능을 위해 화면 밖 물체는 충돌 검사에서 제외하세요
- 복잡한 형태는 여러 개의 단순한 도형으로 근사하면 빠릅니다
- 디버깅 시 충돌 영역을 시각화하는 헬퍼를 사용하세요
2. Bounding Box 활용
김개발 씨가 다음 단계로 넘어갔습니다. 캐릭터는 사람 모양이고, 벽은 복잡한 곡선입니다.
구체 충돌만으로는 정확도가 떨어집니다. 박시니어 씨가 화이트보드에 사각형을 그리며 설명했습니다.
"Bounding Box를 사용하면 됩니다."
Bounding Box는 물체를 감싸는 가장 작은 직육면체입니다. 마치 택배 상자가 물건을 감싸듯이, 3D 모델의 최소/최대 좌표로 박스를 만듭니다.
복잡한 형태의 물체도 간단한 박스로 충돌을 검사할 수 있습니다. Three.js의 Box3 클래스가 이 기능을 제공합니다.
다음 코드를 살펴봅시다.
// Bounding Box를 이용한 충돌 감지
import * as THREE from 'three';
function checkBoxCollision(mesh1, mesh2) {
// 각 메시의 Bounding Box 생성
const box1 = new THREE.Box3().setFromObject(mesh1);
const box2 = new THREE.Box3().setFromObject(mesh2);
// 두 박스가 겹치는지 확인
if (box1.intersectsBox(box2)) {
console.log('Bounding Box 충돌!');
return true;
}
return false;
}
// 애니메이션 루프에서 사용
function animate() {
if (checkBoxCollision(player, wall)) {
// 충돌 시 위치 복원
player.position.copy(previousPosition);
}
}
김개발 씨는 기본적인 구체 충돌 감지를 구현했습니다. 하지만 새로운 문제가 생겼습니다.
게임 속 캐릭터는 사람 모양이고, 벽은 L자 형태입니다. 이런 복잡한 형태를 구체로 근사하면 너무 부정확했습니다.
예를 들어, 캐릭터의 팔이 벽에 닿지 않았는데도 충돌로 감지되거나, 반대로 명백히 부딪혔는데 감지되지 않는 경우가 생겼습니다. 테스터들이 "캐릭터 움직임이 이상해요"라는 피드백을 보냈습니다.
박시니어 씨가 다시 도와주러 왔습니다. "복잡한 형태는 Bounding Box를 사용하세요.
훨씬 정확하고 계산도 빠릅니다." Bounding Box란 무엇일까요? 쉽게 비유하자면, Bounding Box는 마치 선물 포장 상자와 같습니다.
모양이 복잡한 선물을 포장할 때, 우리는 선물을 완전히 감쌀 수 있는 최소 크기의 직육면체 상자를 사용합니다. Bounding Box도 마찬가지입니다.
3D 모델이 아무리 복잡해도, 그것을 완전히 감싸는 직육면체 하나로 표현하는 것입니다. 이 방식이 왜 효율적일까요?
직육면체 두 개가 겹치는지 확인하는 것은 매우 간단한 수학 계산입니다. 각 축(X, Y, Z)에서 범위가 겹치는지만 확인하면 됩니다.
복잡한 3D 메시의 모든 삼각형을 비교하는 것에 비하면 엄청나게 빠릅니다. 실제로 수천 배에서 수만 배 빠를 수 있습니다.
Bounding Box가 없던 시절의 문제를 살펴봅시다. 초기 3D 게임 개발자들은 복잡한 모델의 충돌을 처리하기 위해 모든 삼각형 면을 일일이 검사했습니다.
캐릭터 모델에 1,000개의 삼각형이 있고, 벽에 500개가 있다면, 최악의 경우 50만 번의 계산이 필요했습니다. 게임 프레임이 뚝뚝 끊기는 것은 당연했습니다.
그래서 **Axis-Aligned Bounding Box (AABB)**가 표준이 되었습니다. AABB는 축에 정렬된 Bounding Box를 의미합니다.
물체가 회전하더라도 박스는 항상 XYZ 축과 평행합니다. 이렇게 하면 충돌 검사가 단순한 숫자 비교로 줄어듭니다.
Three.js의 Box3 클래스가 바로 AABB를 구현한 것입니다. 위의 코드를 자세히 분석해보겠습니다.
setFromObject 메서드는 메시의 모든 정점을 검사하여 최소/최대 좌표를 찾아냅니다. 이 과정은 한 번만 하면 되므로 효율적입니다.
그다음 intersectsBox 메서드가 두 박스의 각 축 범위를 비교합니다. X축에서 겹치고, Y축에서도 겹치고, Z축에서도 겹치면 충돌입니다.
하나라도 겹치지 않으면 충돌이 아닙니다. 실무에서는 이것을 어떻게 활용할까요?
가구 배치 시뮬레이터를 만든다고 가정해봅시다. 사용자가 소파를 드래그하여 방 안에 배치합니다.
소파가 벽이나 다른 가구와 겹치면 빨간색으로 표시되어야 합니다. Bounding Box를 사용하면 실시간으로 겹침을 감지하고 즉각 피드백을 줄 수 있습니다.
IKEA 같은 가구 회사의 3D 뷰어가 바로 이런 방식을 사용합니다. 하지만 주의할 점이 있습니다.
물체가 회전할 때 AABB의 크기가 변합니다. 길쭉한 막대를 45도 회전하면 Bounding Box가 훨씬 커집니다.
이로 인해 충돌 감지가 부정확해질 수 있습니다. 더 정밀한 감지가 필요하면 **Oriented Bounding Box (OBB)**를 고려해야 합니다.
또 다른 함정은 바로 성능입니다. setFromObject를 매 프레임마다 호출하면 비효율적입니다.
물체가 움직이거나 회전할 때만 업데이트하는 것이 좋습니다. 정적인 벽이나 장애물은 한 번만 계산하고 캐싱해두세요.
김개발 씨는 Bounding Box를 적용하고 다시 테스트했습니다. 이번에는 캐릭터 움직임이 훨씬 자연스러웠습니다.
벽 모서리를 지나갈 때도 정확하게 막혔습니다. 테스터들의 반응도 좋았습니다.
Bounding Box를 제대로 활용하면 복잡한 3D 씬에서도 빠르고 정확한 충돌 감지가 가능합니다. 여러분의 프로젝트에도 꼭 적용해보세요.
실전 팁
💡 - 정적인 물체의 Bounding Box는 초기화 시점에 한 번만 계산하세요
- 회전하는 물체는 OBB를 고려하되, 성능과 정확도를 저울질하세요
- BoxHelper를 사용하면 Bounding Box를 화면에 표시하여 디버깅할 수 있습니다
3. 캐릭터가 물체를 통과하지 못하게 하기
김개발 씨가 충돌을 감지하는 데는 성공했습니다. 하지만 문제가 하나 더 있었습니다.
충돌을 감지만 하고, 캐릭터가 여전히 벽을 통과했습니다. 박시니어 씨가 웃으며 말했습니다.
"감지했으면 이제 반응도 해야죠."
충돌을 감지한 후에는 충돌 반응을 구현해야 합니다. 가장 간단한 방법은 이전 위치로 되돌리는 것입니다.
조금 더 발전하면 충돌 방향을 계산하여 미끄러지듯 움직이게 할 수 있습니다. 이것을 슬라이딩 충돌이라고 부릅니다.
다음 코드를 살펴봅시다.
// 충돌 시 위치 복원 및 슬라이딩 처리
const previousPosition = new THREE.Vector3();
const velocity = new THREE.Vector3();
function updatePlayer(delta) {
// 현재 위치 저장
previousPosition.copy(player.position);
// 속도 적용
player.position.add(velocity.clone().multiplyScalar(delta));
// 충돌 검사
if (checkCollisionWithWalls(player)) {
// 충돌 발생: 위치 복원
player.position.copy(previousPosition);
// 슬라이딩: 수직 성분만 유지
const slideDirection = velocity.clone();
slideDirection.y = 0; // Y축 움직임 제거
player.position.add(slideDirection.multiplyScalar(delta * 0.5));
}
}
김개발 씨는 드디어 충돌을 감지하는 데 성공했습니다. 콘솔에 "충돌 발생!"이라는 로그가 찍혔습니다.
하지만 실망스럽게도 캐릭터는 여전히 벽을 통과했습니다. 충돌을 '알기만' 하고 '막지는' 못한 것입니다.
이것은 마치 문 앞에 감지 센서만 달아놓고, 문은 열어둔 것과 같았습니다. 센서가 "누가 왔어요!"라고 알려주지만, 정작 문은 닫히지 않는 상황입니다.
김개발 씨는 당황했습니다. 박시니어 씨가 코드를 보더니 말했습니다.
"충돌 감지는 첫 번째 단계일 뿐입니다. 이제 충돌 반응을 구현해야 해요." 충돌 반응이란 무엇일까요?
충돌 감지가 "부딪혔는지 확인"하는 것이라면, 충돌 반응은 "부딪혔을 때 어떻게 할지 결정"하는 것입니다. 가장 간단한 방법은 시간을 되돌리듯 이전 위치로 캐릭터를 복원하는 것입니다.
마치 잘못된 길로 간 것을 깨닫고 한 걸음 뒤로 물러나는 것과 같습니다. 하지만 단순히 위치만 되돌리면 어떤 문제가 생길까요?
캐릭터가 벽을 향해 대각선으로 달려간다고 상상해봅시다. 벽에 부딪히면 완전히 멈춰버립니다.
현실에서는 벽을 따라 미끄러지듯 움직일 텐데 말이죠. 이렇게 딱딱하게 멈추는 움직임은 매우 부자연스럽습니다.
게임 플레이가 답답하게 느껴집니다. 그래서 슬라이딩 충돌 반응이 필요합니다.
슬라이딩은 충돌 방향의 움직임만 차단하고, 나머지 방향으로는 계속 움직이게 하는 기법입니다. 벽을 향해 대각선으로 가면, 벽에 수직인 성분은 막고 평행한 성분만 유지합니다.
결과적으로 캐릭터가 벽을 따라 쭉 미끄러지는 것처럼 보입니다. 위의 코드를 단계별로 살펴보겠습니다.
먼저 previousPosition에 현재 위치를 저장합니다. 이것은 충돌 시 돌아갈 '세이브 포인트'입니다.
그다음 속도 벡터를 위치에 더하여 캐릭터를 이동시킵니다. 델타 시간을 곱하는 것은 프레임레이트에 관계없이 일정한 속도를 유지하기 위함입니다.
충돌이 감지되면 즉시 이전 위치로 복원합니다. 여기까지가 기본적인 충돌 반응입니다.
하지만 코드는 한 걸음 더 나아갑니다. 속도 벡터의 Y축 성분을 0으로 만들어 수평 방향으로만 살짝 이동시킵니다.
이것이 바로 슬라이딩 효과입니다. 실무에서는 어떻게 활용될까요?
FPS 게임을 생각해보세요. 플레이어가 벽을 향해 달리면서 동시에 옆으로 움직입니다.
충돌 반응이 없다면 캐릭터가 벽에 박혀서 꼼짝 못 하겠죠. 하지만 슬라이딩이 구현되어 있으면 벽을 따라 자연스럽게 미끄러집니다.
모든 현대 3D 게임이 이런 방식을 사용합니다. 주의할 점이 있습니다.
슬라이딩 계수를 너무 크게 하면 캐릭터가 벽을 타고 올라가는 버그가 생길 수 있습니다. 위의 코드에서 0.5를 곱한 것도 이런 이유입니다.
또한 여러 벽과 동시에 충돌하는 모서리 상황을 고려해야 합니다. 이런 경우 슬라이딩 방향을 잘못 계산하면 캐릭터가 떨리는 현상이 생깁니다.
더 정교한 방법도 있습니다. 레이캐스팅을 사용하면 충돌 전에 미리 감지할 수 있습니다.
캐릭터 앞쪽으로 보이지 않는 광선을 쏴서 벽까지의 거리를 측정하는 것입니다. 벽이 너무 가까우면 아예 그쪽으로 이동하지 않습니다.
이 방법은 좀 더 복잡하지만 훨씬 부드러운 움직임을 만들어냅니다. 김개발 씨는 슬라이딩 충돌을 구현하고 다시 테스트했습니다.
이번에는 캐릭터가 벽에 부딪혀도 자연스럽게 미끄러졌습니다. "오, 훨씬 좋네요!" 김개발 씨가 감탄했습니다.
박시니어 씨가 고개를 끄덕였습니다. "충돌 감지와 반응을 함께 구현해야 비로소 완성입니다." 충돌 반응을 제대로 구현하면 플레이어가 스트레스받지 않는 자연스러운 게임 경험을 만들 수 있습니다.
여러분도 꼭 시도해보세요.
실전 팁
💡 - 슬라이딩 계수는 0.3~0.7 사이에서 조정하며 최적값을 찾으세요
- 모서리 충돌 시 떨림 현상이 생기면 속도를 0으로 만드는 것도 방법입니다
- 점프 중에는 슬라이딩 로직을 다르게 적용해야 자연스럽습니다
4. 경계 영역 설정
김개발 씨의 게임이 점점 완성되어 갔습니다. 하지만 플레이어가 맵 밖으로 떨어지는 문제가 생겼습니다.
"무한히 떨어지네요." 박시니어 씨가 웃으며 말했습니다. "경계 영역을 설정해야 합니다."
경계 영역은 플레이어가 벗어날 수 없는 게임 공간의 한계입니다. 마치 수영장 가장자리처럼, 특정 좌표를 넘어가면 더 이상 이동할 수 없게 만듭니다.
이것은 간단한 좌표 비교만으로 구현할 수 있습니다. 보이지 않는 벽을 만드는 가장 효율적인 방법입니다.
다음 코드를 살펴봅시다.
// 게임 맵의 경계 영역 설정
const MAP_BOUNDS = {
minX: -50,
maxX: 50,
minZ: -50,
maxZ: 50,
minY: 0,
maxY: 20
};
function clampPositionToBounds(position) {
// X축 제한
position.x = Math.max(MAP_BOUNDS.minX,
Math.min(MAP_BOUNDS.maxX, position.x));
// Y축 제한 (높이)
position.y = Math.max(MAP_BOUNDS.minY,
Math.min(MAP_BOUNDS.maxY, position.y));
// Z축 제한
position.z = Math.max(MAP_BOUNDS.minZ,
Math.min(MAP_BOUNDS.maxZ, position.z));
return position;
}
// 플레이어 업데이트 시 적용
function updatePlayer() {
player.position.add(velocity);
clampPositionToBounds(player.position);
}
김개발 씨는 충돌 감지와 반응을 모두 구현했습니다. 게임이 제법 그럴듯해 보였습니다.
하지만 QA 팀에서 이상한 버그 리포트를 보내왔습니다. "플레이어가 맵 가장자리에서 떨어지면 영원히 추락합니다." 확인해보니 정말 그랬습니다.
맵의 끝까지 가서 한 걸음만 더 나가면 캐릭터가 허공으로 떨어졌습니다. Y 좌표가 -100, -200, -300으로 끝없이 내려갔습니다.
다시 돌아올 방법도 없었습니다. 박시니어 씨가 이 문제를 보더니 말했습니다.
"경계 영역을 설정하지 않았네요. 게임 세계에도 한계가 필요합니다." 경계 영역이란 무엇일까요?
쉽게 비유하자면, 경계 영역은 마치 놀이터 울타리와 같습니다. 아이들이 울타리 안에서는 자유롭게 뛰어놀지만, 울타리를 넘어서는 못 나가게 막는 것입니다.
3D 게임도 마찬가지입니다. 플레이어가 정해진 공간 안에서만 움직이도록 보이지 않는 경계를 만드는 것입니다.
왜 경계 영역이 필요할까요? 첫째, 플레이어가 맵 밖으로 떨어지는 것을 방지합니다.
무한 낙하는 게임을 망치는 치명적인 버그입니다. 둘째, 성능을 보호합니다.
플레이어가 터무니없이 먼 곳으로 이동하면 렌더링이나 물리 계산에 문제가 생길 수 있습니다. 셋째, 게임 디자인을 강제합니다.
플레이어가 의도하지 않은 구역으로 가는 것을 막아 정해진 경험을 제공합니다. 경계 영역 없이 게임을 만들면 어떤 일이 벌어질까요?
초기 오픈 월드 게임에서는 맵 끝에 거대한 산맥이나 바다를 배치했습니다. 물리적인 장애물로 플레이어를 막은 것입니다.
하지만 이것은 메모리를 많이 차지했고, 교묘한 플레이어는 버그를 이용해 넘어가곤 했습니다. 그래서 현대 게임은 보이지 않는 벽, 즉 경계 영역을 사용합니다.
Clamping이라는 기법을 알아봅시다. Clamp는 "조이다" 또는 "제한하다"라는 뜻입니다.
특정 값을 최소값과 최대값 사이로 강제하는 것입니다. 예를 들어 X 좌표가 -50에서 50 사이여야 한다면, -60이 들어오면 -50으로, 70이 들어오면 50으로 자동 조정합니다.
이것이 바로 위 코드의 핵심입니다. 코드를 자세히 분석해보겠습니다.
Math.max와 Math.min을 중첩하여 사용하는 것이 핵심입니다. 먼저 Math.max(minX, position.x)는 "최소값보다 작으면 최소값으로 올려줍니다".
그다음 Math.min(maxX, ...)는 "최대값보다 크면 최대값으로 내려줍니다". 이 두 단계를 거치면 값이 항상 범위 안에 들어옵니다.
Y축도 동일하게 처리합니다. 특히 Y축은 높이를 나타내므로 중요합니다.
플레이어가 땅 밑으로 떨어지거나 하늘 너머로 날아가는 것을 막아줍니다. Z축은 깊이를 담당하며, X축과 함께 수평 이동을 제한합니다.
실무에서는 어떻게 사용될까요? 배틀로얄 게임을 생각해보세요.
시간이 지나면 플레이 가능한 영역이 줄어듭니다. 이것을 구현하려면 MAP_BOUNDS 값을 동적으로 변경하면 됩니다.
시간에 따라 minX, maxX 등을 점점 좁혀가는 것입니다. 플레이어는 자연스럽게 중앙으로 모이게 됩니다.
주의해야 할 점이 있습니다. 경계 영역을 너무 딱딱하게 구현하면 플레이어 경험이 나빠집니다.
벽에 부딪힌 것처럼 갑자기 멈추는 것보다, 살짝 튕겨나가는 효과나 경고 메시지를 주는 것이 좋습니다. 또한 경계에서 너무 가까우면 카메라가 이상해질 수 있으므로, 카메라 위치도 함께 조정해야 합니다.
또 다른 접근 방식도 있습니다. **데스 존(Death Zone)**을 만드는 것입니다.
경계를 약간 넘어가면 바로 막는 대신, 일정 범위를 더 허용하되 그 구역에서는 데미지를 입히는 것입니다. 플레이어가 다시 안으로 들어올 수 있는 여지를 주면서도, 결국 경계를 넘을 수 없게 만듭니다.
김개발 씨는 경계 영역을 설정하고 테스트했습니다. 이번에는 맵 가장자리에 가도 떨어지지 않았습니다.
보이지 않는 벽에 막혀 더 이상 나갈 수 없었습니다. "완벽하네요!" 김개발 씨가 만족스러워했습니다.
경계 영역은 간단하지만 필수적인 기능입니다. 몇 줄의 코드로 게임 세계를 안전하게 만들 수 있습니다.
여러분의 프로젝트에도 꼭 적용해보세요.
실전 팁
💡 - 경계에 가까워지면 화면에 경고 효과를 주면 더 친절합니다
- 경계값을 상수로 분리하면 나중에 맵 크기를 조정하기 쉽습니다
- 디버그 모드에서는 경계 영역을 시각화하는 헬퍼를 추가하세요
5. 물리 엔진 라이브러리 소개
김개발 씨의 게임이 거의 완성되었습니다. 하지만 이제 공이 튀어 오르고, 상자가 쌓이는 물리 효과를 추가하고 싶었습니다.
박시니어 씨가 말했습니다. "직접 만들기엔 너무 복잡해요.
물리 엔진을 사용하세요."
물리 엔진은 중력, 마찰, 충돌 반응 등을 자동으로 계산해주는 라이브러리입니다. Three.js와 함께 사용할 수 있는 대표적인 엔진으로는 Cannon.js, Ammo.js, Rapier가 있습니다.
이것들은 복잡한 물리 시뮬레이션을 몇 줄의 코드로 구현할 수 있게 해줍니다. 게임 개발에서는 거의 필수적인 도구입니다.
다음 코드를 살펴봅시다.
// Cannon.js를 이용한 물리 엔진 설정
import * as CANNON from 'cannon-es';
// 물리 세계 생성
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0); // 중력 설정
// 물리 바디 생성 (구체)
const sphereShape = new CANNON.Sphere(1);
const sphereBody = new CANNON.Body({
mass: 5, // 질량 5kg
shape: sphereShape,
position: new CANNON.Vec3(0, 10, 0)
});
world.addBody(sphereBody);
// 바닥 생성 (정적 물체)
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0 }); // 질량 0 = 정적
groundBody.addShape(groundShape);
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);
// 애니메이션 루프에서 물리 업데이트
function animate() {
world.step(1/60); // 60 FPS로 물리 계산
// Three.js 메시를 물리 바디 위치와 동기화
sphereMesh.position.copy(sphereBody.position);
sphereMesh.quaternion.copy(sphereBody.quaternion);
}
김개발 씨는 기본적인 충돌 감지를 모두 구현했습니다. 캐릭터가 벽을 통과하지 않고, 맵 밖으로 떨어지지도 않았습니다.
하지만 기획자가 새로운 요청을 했습니다. "공이 바닥에 떨어지면 튀어 오르게 해주세요.
그리고 상자를 쌓을 수 있게요." 김개발 씨는 막막했습니다. 공의 반발력을 계산하고, 상자끼리의 마찰을 처리하고, 쌓였을 때 균형을 맞추는 것을 직접 구현해야 한다니요.
박시니어 씨가 다가와 말했습니다. "물리 엔진을 사용하면 이 모든 게 자동입니다." 물리 엔진이란 무엇일까요?
쉽게 비유하자면, 물리 엔진은 마치 인형극의 줄과 같습니다. 극작가는 인형이 어떻게 움직여야 하는지 복잡한 역학을 몰라도 됩니다.
그냥 줄을 당기면 중력, 관성, 마찰이 자연스럽게 적용됩니다. 물리 엔진도 마찬가지입니다.
개발자는 "이 공을 떨어뜨린다"고만 말하면, 나머지는 엔진이 알아서 처리합니다. 물리 엔진이 없던 시절의 고통을 생각해봅시다.
초기 3D 게임 개발자들은 뉴턴의 운동 법칙을 직접 코드로 구현했습니다. 속도, 가속도, 충돌 후 반발 계수, 회전 모멘트...
대학 물리학 수준의 수식이 코드 곳곳에 박혀 있었습니다. 버그가 생기면 디버깅하기도 어려웠습니다.
"왜 공이 이상하게 튀지?"라는 질문에 답하려면 물리학 교수가 필요했습니다. 그래서 전문 물리 엔진이 등장했습니다.
게임 엔진 개발사들이 수년간의 노하우를 담아 물리 계산 라이브러리를 만들었습니다. 이것들은 수십만 시간의 최적화와 검증을 거쳤습니다.
이제 개발자는 라이브러리를 가져다 쓰기만 하면 됩니다. Three.js 생태계에서 인기 있는 것은 Cannon.js, Ammo.js, Rapier 세 가지입니다.
각 엔진의 특징을 살펴봅시다. Cannon.js는 가장 사용하기 쉽습니다.
JavaScript로 작성되어 디버깅이 편하고, 문서도 친절합니다. 중간 규모의 프로젝트에 적합합니다.
Ammo.js는 Bullet Physics 엔진을 WebAssembly로 포팅한 것입니다. 매우 빠르지만 API가 C++ 스타일이라 조금 어렵습니다.
대규모 시뮬레이션에 적합합니다. Rapier는 Rust로 작성된 최신 엔진입니다.
성능과 사용성을 모두 잡았지만, 아직 생태계가 작습니다. 위의 코드를 단계별로 분석해봅시다.
먼저 CANNON.World를 생성합니다. 이것은 모든 물리 객체가 존재하는 가상의 우주입니다.
gravity.set(0, -9.82, 0)은 지구의 중력을 설정합니다. Y축이 -9.82m/s²입니다.
달 표면을 시뮬레이션하고 싶다면 -1.62로 바꾸면 됩니다. 다음으로 CANNON.Body를 만듭니다.
이것은 물리적 속성을 가진 물체입니다. mass: 5는 5kg의 질량을 의미합니다.
질량이 클수록 무겁고, 충돌 시 덜 밀립니다. mass: 0은 특별합니다.
무한히 무거워서 절대 움직이지 않는 정적 물체를 의미합니다. 바닥이나 벽에 사용합니다.
world.step(1/60)이 핵심입니다. 이것은 물리 시뮬레이션을 1/60초만큼 진행시킵니다.
60 FPS로 게임을 실행하면 현실 시간과 동기화됩니다. 이 한 줄이 호출될 때마다 모든 물체의 위치, 속도, 회전이 업데이트됩니다.
충돌도 자동으로 감지하고 처리합니다. 마지막으로 물리 세계의 결과를 Three.js 메시에 반영합니다.
copy 메서드로 위치와 회전을 동기화하는 것입니다. 물리 엔진은 보이지 않는 계산만 하고, 실제 화면 렌더링은 Three.js가 담당합니다.
이 두 세계를 연결하는 것이 개발자의 역할입니다. 실무에서는 어떻게 활용될까요?
자동차 시뮬레이터를 만든다고 가정해봅시다. 바퀴의 마찰, 차체의 무게 중심, 충돌 시 변형까지 모두 물리 엔진이 처리합니다.
개발자는 차의 질량, 엔진 출력, 바퀴 위치만 설정하면 됩니다. 나머지는 엔진이 현실적으로 계산해줍니다.
많은 웹 기반 제품 데모가 이런 방식을 사용합니다. 주의할 점도 있습니다.
물리 계산은 CPU 집약적입니다. 물체가 수백 개를 넘어가면 프레임이 떨어질 수 있습니다.
정적인 물체는 반드시 mass: 0으로 설정하여 계산에서 제외해야 합니다. 또한 물리 시뮬레이션의 시간 간격(step의 인자)이 너무 크면 부정확해지고, 너무 작으면 느려집니다.
1/60이 보통 적절합니다. 김개발 씨는 Cannon.js를 프로젝트에 추가했습니다.
공을 떨어뜨리자 자연스럽게 튀어 올랐습니다. 상자를 쌓자 균형을 잡으며 쓰러졌습니다.
"마법 같네요!" 김개발 씨가 감탄했습니다. 박시니어 씨가 미소 지었습니다.
"물리 엔진은 현대 게임 개발의 필수 도구입니다. 바퀴를 다시 발명하지 마세요." 물리 엔진을 사용하면 복잡한 시뮬레이션을 간단하게 구현할 수 있습니다.
여러분의 3D 프로젝트에도 도입해보세요.
실전 팁
💡 - 정적인 물체는 반드시 mass: 0으로 설정하여 성능을 아끼세요
- 물리 세계와 렌더링 세계의 단위를 통일하세요 (보통 1 unit = 1 meter)
- CannonDebugger 같은 도구로 물리 바디를 시각화하면 디버깅이 쉽습니다
6. 간단한 충돌 처리 구현
김개발 씨가 프로젝트를 마무리하고 있었습니다. 박시니어 씨가 말했습니다.
"마지막으로 충돌 이벤트를 처리하는 법을 알려줄게요. 부딪혔을 때 소리를 내거나 점수를 주는 거죠."
충돌 이벤트는 두 물체가 부딪혔을 때 발생하는 신호입니다. 물리 엔진은 이런 이벤트를 감지하고 콜백 함수를 호출해줍니다.
이것을 활용하면 충돌 시 소리 재생, 이펙트 표시, 점수 계산 등을 구현할 수 있습니다. 게임의 인터랙션을 만드는 핵심 기능입니다.
다음 코드를 살펴봅시다.
// Cannon.js로 충돌 이벤트 처리
import * as CANNON from 'cannon-es';
// 충돌 이벤트 리스너 등록
sphereBody.addEventListener('collide', (event) => {
// event.body는 충돌한 상대방 물체
const impact = event.contact.getImpactVelocityAlongNormal();
// 충돌 강도가 일정 이상이면 소리 재생
if (Math.abs(impact) > 3) {
playCollisionSound(impact);
}
// 특정 물체와 충돌 시 처리
if (event.body.name === 'goal') {
console.log('골 성공!');
score += 10;
showGoalEffect();
}
});
// 충돌음 재생 함수
function playCollisionSound(velocity) {
const audio = new Audio('/sounds/impact.mp3');
// 충돌 강도에 따라 볼륨 조절
audio.volume = Math.min(Math.abs(velocity) / 10, 1);
audio.play();
}
// 시각적 효과 표시
function showGoalEffect() {
// 파티클 효과나 UI 업데이트
particleSystem.burst(sphereMesh.position);
}
김개발 씨의 게임은 거의 완성되었습니다. 공이 바닥에 떨어지고, 벽에 부딪히고, 자연스럽게 튀어 올랐습니다.
하지만 뭔가 허전했습니다. 충돌이 일어나도 아무런 피드백이 없었습니다.
조용히 부딪히고 조용히 튕겨나갈 뿐이었습니다. 박시니어 씨가 게임을 플레이해보더니 말했습니다.
"물리는 완벽하네요. 이제 충돌 이벤트를 추가해서 생동감을 불어넣어야 합니다." 충돌 이벤트란 무엇일까요?
쉽게 비유하자면, 충돌 이벤트는 마치 문에 달린 초인종과 같습니다. 누군가 문을 두드리면 초인종이 울립니다.
우리는 초인종 소리를 듣고 문을 열러 갑니다. 3D 게임에서도 마찬가지입니다.
두 물체가 부딪히면 "충돌했어요!"라는 신호가 울리고, 우리는 그 신호를 받아서 원하는 동작을 실행합니다. 충돌 이벤트 없이 게임을 만들면 어떻게 될까요?
개발자가 매 프레임마다 직접 충돌을 확인해야 합니다. "이전 프레임에는 안 닿았는데 지금은 닿았나?"를 계속 체크하는 것입니다.
이것은 매우 비효율적입니다. 게다가 놓치기 쉽습니다.
물체가 빠르게 움직이면 프레임 사이에 충돌이 발생하고 사라질 수 있습니다. 물리 엔진이 제공하는 이벤트 시스템을 사용하면 이런 문제가 사라집니다.
충돌 강도를 측정하는 방법을 알아봅시다. 단순히 "부딪혔다"만 아는 것은 부족합니다.
살짝 스친 것인지, 강하게 박은 것인지 알아야 적절한 반응을 만들 수 있습니다. getImpactVelocityAlongNormal()이 바로 이것을 측정합니다.
충돌 방향의 상대 속도를 반환하는 것입니다. 이 값이 크면 강한 충돌, 작으면 약한 충돌입니다.
위의 코드를 자세히 살펴보겠습니다. addEventListener('collide', ...)는 충돌이 발생할 때마다 콜백 함수를 호출하도록 등록합니다.
이것은 웹의 클릭 이벤트와 똑같은 패턴입니다. event 객체에는 충돌에 대한 모든 정보가 담겨 있습니다.
상대방 물체, 충돌 지점, 충돌 강도 등을 모두 알 수 있습니다. impact 변수는 충돌의 강도를 숫자로 나타냅니다.
이 값이 3보다 크면 소리를 재생합니다. 왜 3일까요?
이것은 경험적인 값입니다. 테스트하면서 "이 정도는 되어야 소리 날 만하다"고 판단한 임계값입니다.
프로젝트마다 다를 수 있으므로 조정이 필요합니다. 충돌 소리의 볼륨도 강도에 비례하게 만들었습니다.
살짝 부딪히면 작은 소리, 세게 부딪히면 큰 소리가 나는 것입니다. 이런 디테일이 게임을 현실감 있게 만듭니다.
Math.min(..., 1)로 볼륨이 1을 넘지 않게 제한한 것도 중요합니다. 특정 물체와의 충돌 처리도 가능합니다.
event.body.name으로 충돌한 상대방을 식별합니다. 골대와 충돌하면 점수를 주고, 적과 충돌하면 데미지를 입히는 식입니다.
이를 위해 물리 바디를 생성할 때 name 속성을 설정해야 합니다. goalBody.name = 'goal' 같은 식으로요.
실무에서는 어떻게 활용될까요? 볼링 게임을 만든다고 가정해봅시다.
볼링공이 핀에 부딪히면 충돌 이벤트가 발생합니다. 이때 충돌 강도를 확인하여 핀이 쓰러질지 결정합니다.
약하게 스치면 핀이 흔들리기만 하고, 세게 부딪히면 넘어갑니다. 쓰러진 핀의 개수를 세어 점수를 계산합니다.
충돌음과 환호 소리도 재생합니다. 주의할 점이 있습니다.
충돌 이벤트는 매우 자주 발생합니다. 두 물체가 접촉한 상태로 계속 닿아 있으면 매 프레임마다 이벤트가 발생할 수 있습니다.
소리를 재생하는 코드가 있다면 소리가 겹쳐서 시끄러워질 수 있습니다. 따라서 쿨다운을 구현하는 것이 좋습니다.
"마지막으로 소리를 낸 시간"을 기록해두고, 일정 시간 내에는 다시 소리를 내지 않는 것입니다. 또 다른 팁을 드리겠습니다.
충돌 처리 함수에서 무거운 작업을 하면 게임이 버벅일 수 있습니다. 복잡한 이펙트나 네트워크 요청은 큐에 넣어두고 나중에 처리하세요.
충돌 이벤트 콜백은 최대한 가볍게 유지하는 것이 원칙입니다. 디버깅 팁도 하나 드리겠습니다.
충돌 이벤트가 제대로 발생하는지 확인하려면 console.log를 찍어보세요. 예상치 못한 물체끼리 충돌하는 경우도 있습니다.
바닥과 벽이 계속 충돌한다거나, 정적 물체끼리 이벤트를 발생시키는 경우입니다. 이런 것들은 필터링하여 성능을 아껴야 합니다.
김개발 씨는 충돌 이벤트를 구현하고 테스트했습니다. 공이 바닥에 떨어질 때마다 '쿵' 소리가 났습니다.
골대에 들어가면 환호 소리와 함께 점수가 올라갔습니다. "완전히 살아났네요!" 김개발 씨가 기뻐했습니다.
박시니어 씨가 고개를 끄덕였습니다. "충돌 감지만으로는 절반입니다.
충돌 이벤트로 피드백을 주어야 완성입니다." 충돌 이벤트 처리를 마스터하면 플레이어와 소통하는 생동감 있는 게임을 만들 수 있습니다. 여러분의 프로젝트에 적용하여 완성도를 높여보세요.
실전 팁
💡 - 충돌 소리에 쿨다운을 적용하여 소리가 겹치지 않게 하세요
- 충돌 이벤트 콜백은 가볍게 유지하고, 무거운 작업은 큐로 분리하세요
- name 속성 대신 userData 객체를 활용하면 더 복잡한 정보를 저장할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
GitHub와 Vercel로 시작하는 배포 완벽 가이드
코드를 작성했다면 이제 세상에 공개할 차례입니다. GitHub에 코드를 올리고 Vercel로 배포하는 전체 과정을 실무 상황 스토리로 풀어냅니다. 초급 개발자도 따라하면서 자연스럽게 배포 프로세스를 이해할 수 있습니다.
3D 포트폴리오 웹사이트 개발 완벽 가이드
Three.js와 Blender를 활용하여 인상적인 3D 포트폴리오 웹사이트를 만드는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 중심으로 설명합니다. 프로젝트 구조부터 접근성까지 모든 과정을 담았습니다.
Three.js 고급 코드 분석 완벽 가이드
Three.js 공식 예제를 분석하고 복잡한 씬 구조를 이해하는 방법부터 커스텀 Shader, PostProcessing, 성능 최적화, 디버깅까지 실무에서 바로 활용할 수 있는 고급 기법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 상황 스토리와 비유로 풀어낸 가이드입니다.
Three.js Raycaster 상호작용 구현 완벽 가이드
Three.js의 Raycaster를 활용하여 3D 객체와의 상호작용을 구현하는 방법을 초급자 눈높이에서 설명합니다. 마우스 클릭, 터치 이벤트, 하이라이트 효과까지 실전 예제와 함께 배워봅니다.
GSAP 애니메이션 라이브러리 완벽 가이드
웹 애니메이션의 표준, GSAP를 처음부터 제대로 배워봅니다. 기본 사용법부터 Timeline, Easing, Three.js 통합까지 실무에서 바로 쓸 수 있는 핵심 내용을 다룹니다.