▲ 유영천 개발자

[인벤게임컨퍼런스(IGC) 발표자 소개] 유영천 개발자는 기본적인 복셀 렌더링 기법에서부터 네트워크를 타고 클라이언트와 서버 간에 복셀 월드를 동기화하는 방법 등, 그의 노하우를 전격 공개한다.

복셀은 3D 그래픽의 최소 단위로, '부피를 가진 도트(dot)' 정도의 개념이다. 복셀을 활용한 게임으로 '마인크래프트'가 있다. 게임의 데이터 측면에서 보면, 복셀은 도트보다 배 이상의 메모리를 소모한다. X와 Y축으로만 이뤄진 2D 그래픽에서 높이 값을 더하기 때문이다. 계산으로 보면 3^2과 3^3의 차이다. 이 값이 수만 개의 오브젝트로 퍼진다면, 메모리 소모는 급격하게 커지기 마련이다.

유영천 개발자는 복셀로 이뤄진 새로운 액션 게임을 상상한다. 그 게임은 지형이 플레이어의 공격에 반응해 실시간으로 변한다. 기존 슈팅 게임의 구조물은 아무리 총알을 퍼부어도 끄떡없다. 만약 유영천 개발자의 복셀 기술이 완성되어 적용됐다면, 화력에 따라 구조물이 변할 것이다. 복셀 상에서 액션은 지형을 활용해 더 공격적이고 창의적인 플레이가 가능해진다.

현재 유영천 개발자의 복셀 프로젝트는 시험 단계다. 실력파로 유명한 그 역시도 수많은 난제를 풀어나가고 있다. IGC 2018에서 유영천 개발자가 겪는 복셀 프로젝트의 생생한 이야기를 들을 수 있었다.


■ 강연주제: Voxel 기반 네트워크 게임 최적화 기법

⊙ 유영천 개발자가 만들고 싶은 '복셀'은?

MSX용 WARROID (영상 참고: 유튜브 채널 qurataro)

유영천 개발자가 영감을 받은 게임은 1986년에 출시된 MSX용 '워로이드'이다. 이 게임의 특징은 캐릭터의 공격이 지형에 변화를 미친다는 점이다. 변화는 실시간으로 반영돼 상대방에게도 영향을 미친다. 복셀 기반의 네트워크 게임으로는 '마인크래프트'가 있다.

그러나 유영천 개발자는 마인크래프트보다 더 동적인 게임을 원했다. 지형 뒤에 숨는 것은 물론이고 미사일 등으로 파괴하며 전투를 이어나가는 게임이다. 여기에 유저들이 모여서 놀 수 있는 MMO 게임을 유영천 개발자는 원했다.

그는 정교한 복셀 네트워크 게임을 위해 가로-세로-높이 50cm를 목표로 개발을 시작했다. 현재 쓰이는 1m보다 더 정교한 액션이 구현하기 위해서다. 유영천 개발자는 "처음 목표는 25cm였지만, 과부하가 '말도 안 되게' 많이 걸리는 바람"에 50cm로 타협했다고 덧붙였다.

복셀 네트워크 액션 게임인 만큼 구조 변형이 실시간으로 동기화되도록 했다. 그리고 자연스러운 경험을 위해 권장사양 기준으로 120FPS를 목표로 했으며, MMO는 4,000명의 데이터를 처리할 수 있도록 설계했다. 이와 함께 실시간으로 '변경 가능한' 라이팅을 주고자 했다.

▲ 실시간으로 복셀을 편집, 파괴하고, 다른 유저에게도 적용됨을 확인할 수 있다

유영천 개발자는 오브젝트 한 개당 8x8x8까지 가변 정밀도를 갖도록 만들었다. 크기는 4m^3으로 디자인했으며, 기하구조 데이터와 텍스처 인덱스 데이터는 분리했다. 이때, 텍스처 인덱스 데이터는 버텍스 정보에 포함되지 않도록 했다. 기하구조는 복셀당 1bit, 텍스처 인덱스는 8bits다.

복셀 구조를 세부적으로 살펴보면 시스템 메모리 안에 텍스처 인덱스 테이블이 있는 형태다. 기하구조는 버텍스 버퍼, 충돌 처리를 위한 삼각형 리스트, 라이트맵 계산을 위한 사각형 패치 리스트로 구성된다. 텍스처 인덱스 테이블은 콘스턴트 버퍼(constant buffer)를 사용한다.

초기 모델을 만들고 나니 버텍스를 압축할 필요가 생겼다. 버텍스에 위치좌표(12bytes), 법선벡터(12butes), 라이트맵 텍스쳐 좌표(8bytes)가 담겨있다 보니 총 32bytes가 돼버렸다. 게임당 5만 개의 오브젝트를 반영해 개당 32bytes의 버텍스를 가지고서 게임을 만들면 감당하기 힘든 메모리 사용량이 필요하다.

버텍스 압축은 다섯 개의 정보를 기반으로 한다. 쿼드인덱스(QuadIndex), 오브젝트 안에서의 좌표(oPos), 복셀 안에서의 좌표(vPos), 사각형 안에서의 좌표(qPos), 법선벡터 8방향의 정보를 담은 N이다. 압축된 버텍스 구조는 다음과 같다.

▲ 압축된 버텍스 구조

버텍스 버퍼를 효율적으로 쓰는 방법도 필요했다. 유영천 개발자는 "월드를 구성하다 보면 중복되는 패턴도 많이 나온다"며 "이미 있는 텍스처 인덱스 테이블을 분리해 중복되는 패턴을 재활용하도록 했다"라고 전했다. 그는 이 방법을 통해 상당한 GPU를 절약할 수 있었다고 소개했다.

▲ 버텍스를 재활용하는 구조도, 같은 모양을 인덱스 처리해 쓴다



⊙ 매끄러운 충돌 처리를 위해

복셀을 아무리 잘 디자인했더라도, 충돌처리가 미흡하면 쓸모가 없다. 어찌 보면 원활한 충돌처리를 위해 지금까지의 과정이 있었다. 충돌처리를 잘 구현해야 캐릭터가 복셀 지형지물 위에서 매끄럽게 움직일 수 있다. 로켓탄을 쏴 복셀을 파괴하거나 미끄러져 방향이 바뀌는 경우도 같은 코드를 사용한다.

충돌처리는 멈춤, 미끄러짐, 반사로 나뉜다. 처리를 위해 그는 위치, 단축, 장축을 가진 타원체와 속도와 방향을 나타내는 벡터로 충돌 테스트를 할 삼각형을 수집했다.

선택하는 방법은 KD-Tree를 순회해 타원체 또는 ray에 교차하는 오브젝트를 가려내는 걸 택했다. 각 오브젝트에 대한 충돌처리 삼각형을 가져오고, 이에 타원체를 충돌시켜 '미끄러짐 벡터'로 다음 위치를 조정했다. 이 방법을 속도벡터가 거의 0이 될 때까지 반복했다. 유영천 개발자는 이 과정이 서버와 클라이언트가 공유하는 코드의 대표적인 사례라고 소개했다.

성능 향상을 위해 유영천 개발자는 복셀의 인접한 면을 병합했다. 단, 복셀이 서로 다른 텍스처 인덱스를 갖는 경우에는 렌더링을 위해 병합하면 안 된다. 렌더링 용도가 아닐 경우에는 기하구조의 충돌처리를 위해서라면 병합할 수 있다. 그는 "성능 향상을 위해 꼭 병합해야 한다"고 강조했다. 효율적인 충돌처리와 피킹(picking)을 위해서 최대한 복셀을 병합해 시스템 메모리상에 유지해야 한다.

▲ 충돌처리를 위해 인접한 면들을 병합해 메시를 최적화한다

▲ 다음 사각형들을 분해해 같은 평면 방정식에 따라 그룹화한다

▲ 2D 비트맵 상에서 X, Y 축으로 한칸씩 성장시킨다

▲ 오브젝트의 3D 위치, 분해된 사각형 그룹의 면 방정식으로 다시 3D화 시킨다

서버와 클라이언트 모두 충돌 처리를 멀티 스레드로 처리하는 관계로 코드와 KD-Tree 코드는 쓰레드 세이프(thread safe)하게 작성해야 한다. 트리(tree)를 순회할 때 필요한 스택만 스레드 별로 제공되면 쓰레드 세이프는 쉽다. 이 방법은 서버에서 특히 유용하다.


⊙ 복셀 네트워크 게임의 '컬링'은 어떻게?

유영천 개발자는 컬링이 가장 오랜 시간이 든 개발 과정이었다고 소개했다. 아무리 최적화를 거듭해도 불필요한 폴리곤이 너무 많이 나왔고, 오브젝트도 많이 나왔다. 그는 "몇만 개가 기본이었고, 가려진 애들을 다 그리려고 하니 정말 느렸다"며 "처음에는 대충 15프레임 정도 나온 거 같다"고 전했다. 현재는 컬링 최적화를 잘 맞춰 120프레임까지 끌어올렸다.

기본적인 컬링 방법은 뷰 프러스텀 컬링(View Frustum Culling)이다. 퀘이크 계열의 BSP, PVS나 초기 언리얼 엔진에서 쓰인 BSP, Room, Portal 방법 등이 있다. 그러나 퀘이크 계열과 초기 언리얼 엔진 방식은 복셀 프로젝트에서 지형지물이 실시간으로 바뀌기 때문에 사용할 수 없다.

어클루젼 컬링은 가려진 오브젝트를 안 그리는 기술이다. 그러나 복셀 프로젝트는 정육면체 특성상 무조건 안 보인다고 확신하기는 힘들었다. HW Occlusion Culling with D3DQuery도 복셀 프로젝트는 오브젝트가 너무 많고, 드로우콜 또한 너무 느려 사용하지 않았다고 밝혔다.

유영천 개발자는 SW Occlusion Culling을 선택했다. 이 기술은 CPU 코드로 구현한 z-buffer로, 쓰로우풋(throughput)은 HW 방식에 비해 크게 떨어진다. 대신 렌더링을 위한 셋업 과정이 필요 없고 응답이 빠르다. GPU가 없던 과거의 프로그래머들이 쓰던 방법으로 최근에는 잘 쓰이지 않는다. 그러나 복셀 프로젝트를 완성하기 위해 유영천 개발자는 고대의 방법을 끌어들였다.

공간 분리는 KD-Tree를 사용했다. KD-Tree는 각 노드를 3가지 축과 방향의 거리 'D 값'으로 표현할 수 있다. 탐색할 때 카메라로부터 가까운 노드와 먼 노드를 구분해 찾을 수 있다. 이를 통해 Ray의 교차 판정 등에 쓸 수 있다. 복셀 프로젝트의 경우 월드는 KD-Tree로 관리하는 경우가 많다.

만약 일반적인 방법으로 노드를 컬링 하면 하위 노드와 함께 리프(leaf)에 포함된 오브젝트도 같이 정보를 가져온다. KD-Tree 탐색 단계에서 노드에 대해 오클루전 컬링을 수행하면 렌더링 될 오브젝트들을 원천적으로 줄일 수 있다. 가까운 노드에서 먼 노드를 탐색해가면서 가까운 노드의 오브젝트를 z-buffer로 테스트하거나 래스터 화 한다.

유영천 개발자의 설명에 따르면 노드를 뷰 프러스텀 컬링 하는 과정은 먼저 AABB 육면체에 있는 삼각형 12개에 z-test를 한다. 그 결괏값에 렌더링 되는 픽셀이 하나도 없으면 다음 노드로 넘어간다. 리프일 경우 포함하는 오브젝트를 수집하고 뷰 프로스텀 컬링을 시행한다. 만일 오브젝트 z-test, z-write 결과 렌더링 되는 픽셀이 하나도 없으면 수집하지 않는다.

▲ 얻은 값으로 안 보이는 오브젝트를 만들지 않을지 결정한다

오브젝트 내에 임의의 복셀에 대해 인접한 6면에 복셀이 있다면, 이 복셀은 보이지 않는다. 카메라 방향에 상관없이 안 보이는 숨겨진 복셀과 오브젝트는 제거하는 게 좋다. 또한, 카메라 방향에 대해 A복셀 오브젝트가 B복셀 오브젝트에 있고, A의 카메라 방향에 구멍(터널)이 없으면, B는 보이지 않는다.

모든 오브젝트는 인접한 오브젝트의 터널 상태에 따라 bits flags 값을 갖는다. 카메라와 상관없이 오브젝트가 보이는 여부를 결정할 수 있으며, ray의 X, Y, Z 성분과 변수에 따라 빠르게 판단할 수 있다. 이 과정은 오브젝트에 변형이 생길 때마다 인접한 오브젝트들에 대해서 수행해야 한다.

▲ 오브젝트를 탑뷰에서 봤을 때의 경우


⊙ 복셀 프로젝트에 적합한 네트워크는?

유영천 개발자에 따르면 복셀 프로젝트의 네트워크는 다른 게임들과 크게 다를 건 없다. 전체 월드를 특정 간격으로 잘라 그리드로 구분한 뒤, 패킷 전송의 지역적 구분 단위로 사용한다. 이벤트가 발생한 섹터와 주변 8섹터를 패킷 전송 범위로 삼고, 모든 플레이어와 NPC는 위치에 따라 섹터에 저장된다. 이 데이터는 플레이어나 NPC가 움직일 때 즉시 갱신된다.

섹터의 방문 여부에 따라 1과 0으로 구분한다. 이 데이터는 서버 측의 플레이어 인스턴스마다 갖게 하고, 클라이언트도 지니고 있는다. 서버는 플레이어가 처음 로그인하거나 이동했을 때 데이터를 확인하고, 해당 섹터의 복셀 지형을 플레이어에게 전송한다.

스트리밍은 서버가 복셀 지형이 변형되는 이벤트를 월드 전역에 브로드캐스팅 한다. 이때 클라이언트는 자신이 방문했던 섹터에 대한 패킷만 처리하게 된다. 클라이언트가 방문한 적 없는, 복셀 지형을 가지고 있지 않은 섹터에서의 변형 이벤트는 그냥 폐기하도록 했다.

따라서 복셀 지형의 변형 이벤트를 수신하면 클라이언트에서 바로 반영하므로 항상 최신 데이터가 유지된다. 방문한 적 없는 섹터의 복셀 변형 이벤트는 마찬가지로 폐기한다.

▲ 일반적인 복셀 데이터 전송 규칙

▲ 복셀 데이터가 변형되는 이벤트가 발생했을 때 패킷 처리 방식

유영천 개발자는 복셀 데이터를 압축할 때 변수를 제한하는 방법을 사용했다. 8^3 복셀 오브젝트를 2^3 블럭으로 쪼개면, 패턴은 8bits로 최대 256가지가 나온다. 이때 최대 패턴 개수를 16개로 제한하고, 초과할 경우 압축 불가로 처리한다. 패킷 자체는 큰 데이터가 아니어서 손실이 크지는 않다.

8bits의 16가지 경우는 128bits로 16bytes다. 곧 패턴 팔레트 사이즈가 된다. 2^3 블럭 하나는 16가지 중 하나의 패턴을 갖게 되므로 각 블럭은 4bits로 표현할 수 있다. 비슷한 원리로 복셀 데이터 바디 사이즈를 정하면 8^3 복셀 오브젝트 데이터를 약 37.5% 감소시킬 수 있다. 이는 스트리밍 시 평균 61% 정도의 오브젝트에 대해 25% 패킷 사이즈를 감소시키는 효과가 있다.

한편, 유영천 개발자의 복셀 프로젝트는 다양한 방법으로 개발을 시도하는 단계다. 그는 "복셀 프로젝트는 메모리 소모가 너무 심하다"며 "무조건 최대한 아끼고 압축하는 게 중요하다"고 전했다. 유영천 개발자의 프로젝트 과정은 그의 개인 홈페이지에서 직접 확인할 수 있다.