ThreeJS로 Raycasting Collision Detection을 구현한 사례를 정리해본다. 게임 개발에 있어 충돌 이벤트는 가장 기초적이다. AABB, OBB, 원 충돌 등 수 많은 충돌 구현 방법이 있지만, 그중에서도 Raycasting을 통해 구현해볼 예정이다. Raycasting은 대상 object의 vertex가 복잡하거나 정밀한 계산을 요해야 하는 경우 쓰이고 그에 비례하게 성능이 떨어진다는 특징이 있다.
이 포스트에서는 레이싱 게임을 개발하며, Raycasting 구현 방법과 성능 최적화에 대해 논할 예정이다.
개발을 위해 공식 문서를 찾아봤다.
THREE.Raycaster 클래스의 구조를 확인해보면 다음과 같다. 광선(Ray)의 위치 벡터 값과 방향 벡터값을 입력해주면 필요 조건은 충족했다. near는 최소 위치, far는 ray의 범위를 지정해준다. near~far 사이에 충돌 된 오브젝트가 있으면 이를 반환한다. 아래는 init코드 예제다.
this.raycaster = new THREE.Raycaster()
위 코드는 raycaster 클래스 이다. 우리는 레이싱 게임을 만들 예정이기 때문에 origin과 direction을 지속적으로 업데이트 해주어야 한다.
업데이트는 코드는 다음과 같다.
const rayOrigin = new THREE.Vector3(0, 0, 0)
const rayDirection = new THREE.Vector3(-2, 0, 0)
rayDirection.normalize()
this.raycaster.set(rayOrigin, rayDirection, 0, 50)
자동자의 위치가 변경될 때 마다 업데이트 해주어야 한다. 또한 대상의 object vertex가 복잡해 질 수록 그만큼 성능도 저하됨으로 가장 적게 업데이트 해줄 수록 빨라진다.
이제 충돌 감지 코드를 살펴보자.
let object = this.scene.getObjectByName( "targetObject" );
const objectsToTest = [ object ]
const intersects = this.raycaster.intersectObjects(objectsToTest)
intersects.forEach((element: any) => {
if (element.distance < 5) {
this.isCollision = true
console.log("충돌")
}
});
raycaster와 타겟 오브젝트의 길이가 5 이하일 경우 충돌로 감지하는 코드다. 이때 raycaster는 첫 충돌 뿐 아니라 선 범위 상에 있는 모든 물체를 탐지하기 때문에 forEach문을 활용해서 모든 충돌 요소와 raycaster origin간 거리를 측정해주어야 한다.
과연 Raycasting이란 무엇인가. Raycasting은 원래 2D 그래픽 환경에서 원근감을 표현하기 위해 제안된 기술이다. 2D 평면상에서 플레이어 화면까지 거리를 측정한 다음 멀 수록 어둡게 처리하는 기법을 활용해 보다 입체적으로 표현되도록 한다.
따라서 매 순간 플레이어가 움직일때마다 frame 기준으로 계산해야 한다. 타겟 오브젝트가 많을 때는 당연히 비효율적이다. 그럼에도 많은 vertex를 가진 오브젝트에 대해 처리할 때에는 이만한 알고리즘이 없다.
threejs에서는 이 기술을 활용한 한 가지 예제가 있다.
https://threejs.org/examples/?q=terr#webgl_geometry_terrain_raycast
terrain의 복잡한 표현에 마우스를 가져다 대면 그에 해당하는 표면을 지정하는 간단한 예시다.
앞서 설명했듯, 웹이라는 플랫폼 특성상 성능적 한계가 있을 수 밖에 없다. 따라서 최적화 방안을 모색해본다.
1초를 60프레임으로 가정하고 Raycasting을 업데이트 한다면 당연히 성능은 하락한다. 따라서 별도의 setinterval 함수를 만들어 1초에 6~10번 정도 업데이트하는 방향으로 수정하는 방법도 존재한다. 상황에 따라 업데이트 주기는 조절할 수 있다. 필자의 주기는 초당 6번 정도가 적당했다. 또한 플레이어가 움직이지 않는 상황일 경우 업데이트하지 않는 방법도 존재한다. origin과 direaction이 변경되지 않는다면 최대한 반복을 줄이자.
setInterval(() => {
const rayOrigin = new THREE.Vector3(x, y, z)
const rayDirection = new THREE.Vector3(dx, dy, dz)
rayDirection.normalize()
this.raycaster.set(rayOrigin, rayDirection, near, far)
}, 1000/6)
Raycasting 받는 오브젝트의 수를 제한하는 방법도 있다.
const objectsToTest = [ object1, object2 ... ]
const intersects = this.raycaster.intersectObjects(objectsToTest)
위 처럼 많은 오브젝트를 대상으로 raycasting 하는 경우 성능은 저하될 수 있다. 오브젝트의 수가 많다면 이 방법보다는 문제해결에 최적화된 방법을 쓰는게 올바르다. 예를들어 충돌 감지를 하기 위해서는 AABB, OBB와 같은 계산량이 적은 알고리즘을 사용하는게 더 효율적이다.
단 한개의 오브젝트만을 대상으로 하기 위해서는 다음과 같다.
const intersects = this.raycaster.intersectObject(object)
Raycasting은 꼭 필요한 오브젝트에만 사용해야 한다. 웹이라는 성능 한계상 컴퓨팅 자원이 그리 크지 않기 때문에 충돌 알고리즘에서는 비효율적인 축에 속한다.
Raycasting은 게임에서 가장 많이 쓰이는 도구중 하나다. ThreeJS에서는 이를 구현하기 위해 단 몇 줄의 코드만 가지고도 편하게 개발할 수 있도록 구성해놓았다.
현대는 보다 사실적인 게임을 위해 레이트레이싱이라는 기술을 사용한다. 그 영향력이 얼마나 중요하던지 그래픽카드 이름이 RTX(Ray Tracing Texel eXtreme)로 출시되고 있을 정도다. 이 레이트레이싱이라는 기술도 결국 레이케스팅을 기반으로 하고 있다. 80~90년대의 게임이 음영표현과 공간감을 위해 레이케스팅을 사용한게, 현대로 넘어와서는 레이트레이싱을 사용하는 원천 기술이 되었다.
그만큼 중요한 개념이고 알아둘 필요가 있어 정리해봤다. 이상으로 포스트를 마친다.