The Hobbit Experience

Bringing Middle-Earth to Life with Mobile WebGL

HTML5 Rocks

역사적으로 인터랙티브하고 웹 기반이며 멀티미디어-중심(Multimedia-heavy)의 체험을 모바일과 태블릿으로 가져오는 것은 도전이었습니다. 주요 제한 사항들은 성능, API 효용성, 디바이스에서의 HTML5 오디오에 대한 제한들 그리고 매끄럽게 이어져야 할 비디오 재생의 랙이었습니다.

올해 초에 우리는 구글과 워너브라더스에서 온 친구들과 함께 새로운 호빗(the Hobbit) 영화, 호빗: 스마우그의 폐허를 위한 모바일 우선 웹 체험(Mobile-first Web Experience)를 만들기 위한 프로젝트를 시작했습니다. 멀티미디어-중심의 모바일 크롬 실험(Chrome Experiment)는 정말 자극되고 도전할만한 작업이었습니다.

체험은 이제 WebGL과 Web Audio를 액세스할 수 있는 새로운 넥서스 디바이스 상의 안드로이드용 크롬에 최적화되었습니다. 그러나 체험의 대부분은 WebGL을 지원하지 않는 디바이스와 브라우저에서도 고맙게도 하드웨어 가속 합성(Compositing) 및 CSS 애니메이션으로 이용 가능합니다.

전체 체험은 중간계(Middle-earth)의 지도와 영화 호빗의 등장 인물들을 기반으로 하고 있습니다. WebGL의 사용은 우리가 호빗 3부작의 다채로운 세계를 극적으로 보이게하고 탐험할 수 있게 하였으며 사용자가 체험을 컨트롤할 수 있도록 하였습니다.

모바일 디바이스 상에서의 WebGL 도전

첫번째, "모바일 디바이스" 용어는 매우 폭넓습니다. 각 디바이스의 사양들은 매우 다릅니다. 그러므로 더 많은 디바이스를 덜 복잡한 체험으로 지원할 것인지 아니면 -우리가 이 사례에서 했듯이- 훨씬 사실적인 3D 월드를 표시하는 것이 지원 가능한 디바이스들로 제한할 것인지는 개발자 여러분이 결정할 필요가 있습니다. 우리는 “중간계의 여정(Journey through Middle-earth)”에서 넥서스 디바이스와 5개의 대중적인 안드로이드 스마트폰에 집중했습니다.

실험(Experiment)에서, 기존 WebGL 프로젝트의 일부에서 했던 것처럼 three.js를 사용했습니다. 넥서스 10 태블릿에서도 훌륭하게 돌아가는 트롤숲 게임(Trollshaw Game)의 초기 버전 구축에 의해 구현을 시작했습니다. 디바이스 상에서 몇가지 초기 테스트 후에 다음과 같이 저사양 랩탑에서 일반적으로 사용하였던 것과 같은 마음 속에 있던 최적화의 리스트를 가지게 되었습니다.

  • 로우 폴리곤 모델을 사용하자

  • 저해상도 텍스쳐를 사용하자

  • merging geometry을 통해 최대한 그리기 호출(Drawcall)의 횟수를 감소시키자.

  • 재질(Material)과 광원(Lighting)을 단순화하자

  • 후시 효과(Post Effect)를 제거하고 안티앨리어싱을 끄자

  • 자바스크립트 성능을 최적화하자

  • 절반 크기의 WebGL 캔버스에 렌더링하고 CSS로 다시 확대하자

게임의 개략적인 첫번째 버전에 이 최적화들을 적용한 뒤 우리를 기쁘게 했던 30FPS의 꾸준한 프레임율을 가지게 되었습니다. 그 시점에 우리의 목표는 프레임율에 부정적인 영향이 없이 시각적인 개선을 하는 것이었습니다. 많은 트릭들을 시도한 결과 몇가지는 정말 성능 상의 영향을 미치는 것이었으며 일부는 우리가 바라던 만큼의 효과는 없었습니다.

로우 폴리곤 모델의 사용

모델들로 시작해봅시다. 로우 폴리곤 모델의 사용은 확실히 다운로드 시간에 도움이 되는 것은 물론 씬(Scene)을 초기화하기 위해 필요한 시간에도 도움이 됩니다. 우리는 성능에 큰 영향을 주지 않고 꽤 많은 복잡도를 증가할 수 있음을 확인했습니다. 이 게임에서 사용하는 트롤 모델들은 약 5천개의 면(Face)을 가지고 있으며 씬은 약 4만개의 면(Face)으로 구성되며 잘 동작합니다.

트롤숲(Trollshaw forest)의 트롤 중의 하나

(아직 릴리즈되지는 않았지만) 체험 내의 또다른 지역을 위해 폴리곤을 감소시킴으로써 우리는 성능에 더 큰 영향을 주는 것을 확인했습니다. 그 사례에서 데스크탑에서 로딩하는 것 대신 모바일 디바이스를 위한 로우 폴리곤 객체들을 로딩했습니다. 각기 다른 3D 모델의 세트를 생성하는 것은 추가적인 작업을 필요로 하지만 항상 필요한 것은 아닙니다. 시작 시 여러분의 모델이 얼마나 복잡한가에 정말 의존적입니다.

우리는 대량의 객체들로 이루어진 커다란 씬들을 동작할 때 어떻게 기하학적 구조(Geometry)를 나눌지에 대한 전략적인 시도를 했습니다. 모든 모바일 디바이스들에 대해 동작하는 세팅을 찾기위해 덜 중요한 메쉬들을 빠르게 on/off 전환을 할 수 있도록 하여 동적인 최적화를 할 때 런타임에서 자바스크립트를 통해 기하학적 구조(Geometry)를 합치거나 요청을 저장할 때 미리-생성된 프로덕트(pre-production) 안에 합칠 것인지를 선택할 수 있게 되었습니다.

저해상도 텍스쳐의 사용

모바일 디바이스에서 로딩 타임을 감소시키기 위해 데스크탑 상의 텍스쳐 크기의 절반을 차지하는 다른 텍스쳐를 로딩하기로 결정하였습니다. 모든 디바이스가 2080x2048px까지의 텍스쳐 크기를 다룰 수 있으며 대부분의 경우 4096x4096px를 다룰 수 있습니다. 개별적인 텍스쳐들 상에서 색인된 텍스쳐는 GPU에 한번 로딩되고 나면 문제를 발생하지 않아보입니다. 텍스쳐의 전체 크기는 텍스쳐가 끊임없이 업로드되고 다운로드되는 것을 피하기 위해 반드시 GPU 메모리에 맞아야 하지만 이것은 대부분의 모바일 웹체험에 있어 아마 큰 문제는 아닐 것입니다. 그러나 텍스쳐들을 가능한한 적은 스프라이트시트(Spritesheet)에 병합하는 것은 그리기 호출(Drawcall)의 감소에 중요합니다. — 이것이 모바일 디바이스들의 성능에 큰 영향을 주는 요소이기 때문입니다.

트롤숲의 트롤 중의 하나에 대한 텍스쳐
(원본 사이즈 512x512px)

재질(Material)과 광원(Lighting)의 단순화

재질(Material)의 선택 역시 성능에 엄청난 영향을 줄 수 있으며 모바일에서는 반드시 현명하게 관리되어야 합니다. three.js에서 (각 텍셀 라이트 계산에 대한) MeshPhongMaterial 대신에 (각 버텍스 라이트 계산에 대한) MeshLambertMaterial의 사용은 성능을 최적화하기 위해 우리가 사용한 방법 중의 하나입니다. 기본적으로 우리는 가능한한 적은 광원(Lighting) 계산을 하는 단순한 쉐이더를 사용하려 노력했습니다.

사용하는 재질이 씬의 성능에 얼마나 영향을 주는지 살펴보기 위해서 씬의 재질을 MeshBasicMaterial로 오버라이드할 수 있습니다. 이 방법은 좋은 비교 사례를 제시할 것입니다.

scene.overrideMaterial = new THREE.MeshBasicMaterial({color:0x333333, wireframe:true});

자바스크립트 성능 최적화

모바일에서 게임을 구축할 때 GPU는 항상 가장 큰 허들이 되지는 않습니다. 대다수의 시간이 CPU에서 특히 물리(Physics)와 스켈레톤 애니메이션(Skeleton Animation)을 수행하는 데 소모됩니다. 시뮬레이션에 한정해서 때때로 사용할 만한 한가지 트릭은 각기 다른 프레임에서 이러한 비싼 계산들을 실행하는 것입니다. 또한 객체 풀링이 발생하였을 때 가비지 콜렉션과 객체 생성와 같은 가능한 자바스크립트 최적화 테크닉을 사용할 수도 있습니다.

새로운 객체를 생성하는 대신 루프에서 미리-할당된 객체를 갱신하는 것은 게임에서 가비지 콜렉션에 의한 "딸꾹"(역주: 실행이 잠시 멈추는 듯한) 현상을 방지하는데 중요한 단계입니다.

예를 들어 다음과 같은 코드에 대해 생각해보도록 하겠습니다.

var currentPos = new THREE.Vector3();

function gameLoop() {
  currentPos = new THREE.Vector3(0+offsetX,100,0);
}

이 루프에서 개선된 버전은 다음과 같이 가비지 콜렉션을 반드시 발생시키는 새로운 객체들의 생성을 회피하는 것입니다.

var originPos = new THREE.Vector3(0,100,0);
var currentPos = new THREE.Vector3();
function gameLoop() {
  currentPos.copy(originPos).x += offsetX;
  // 혹은
  currentPos.set(originPos.x+offsetX,originPos.y,originPos.z);
}

가능한 최대로, 이벤트 핸들러는 속성들만 갱신해야 하며, requestAnimationFrame 렌더-루프(render-loop)가 스테이지의 갱신을 다루도록 놔두어야 합니다.

또 다른 팁은 레이캐스팅(ray-casting) 동작을 최적화하고 (혹은) 미리 계산(pre-calculate)하는 것입니다. 예를 들어 만약 정적인 경로 이동을 하는 동안 메쉬에 객체를 덧붙이는 것이 필요하다면, 루프마다 위치를 "저장(record)"하고 나서 메쉬에 대한 레이캐스팅을 하는 대신 (저장된) 이 데이터를 읽을 수 있습니다. 혹은 리벤델 체험(Rivendell experience)에서 우리가 한 것처럼 보이지 않는 단순한 로우 폴리곤 메쉬를 가지고 마우스 인터랙션을 찾기 위한 레이캐스팅을 할 수도 있습니다. 하이 폴리곤 메시 상에서 충돌을 찾는 것은 매우 느리고 일반적으로 게임 루프에서 피해야만 할 일입니다.

절반 사이즈의 WebGL 캔바스에 렌더링하고 CSS를 통해 확대하기

WebGL 캔버스의 사이즈는 아마도 여러분이 성능을 최적화하기 위해 조작할 수 있는 단일하고 가장 큰 영향을 주는 인자입니다. 3D 씬을 그리기 위해 사용하는 캔버스가 커질수록 매 프레임마다 그려야 하는 픽셀수가 많아집니다. 이 과정이 성능에 영향을 줍니다. 2560x1500 픽셀의 고해상도 디스플레이를 가지는 넥서스 10은 저해상도 태블릿에서의 픽셀수보다 4배의 입력이 발생합니다. 이를 모바일에서 최적화하기 위해 우리는 캔버스를 절반(50%) 사이즈로 설정하고 나서 그 크기를 하드웨어 가속이 가능한 CSS 3D 트랜스폼을 통해 의도했던 크기(100%)로 확대하는 트릭을 사용했습니다. 이 방법은 이미지 내의 얇은 라인이 도드라지는 문제를 가지고 있는 것이 불리한 점이지만 고해상도 스크린에서 효과는 그리 나쁘지 않습니다. 이는 추가적인 성능 개선을 위해 절대적인 가치가 있는 방법입니다.

넥서스 10에서 캔버스의 스케일링이 없는 씬(16fps)과 50%로 축소한 동일한 씬(33fps).

건물 블록으로써의 객체들

Dol Guldur 성과 리벤델의 끝나지않는 협곡(Never ending valley of Rivendell)과 같은 커다란 미로를 생성하는 것이 가능하기 위해서 우리가 재사용할 수 있는 빌딩 블록 3D 모델들의 세트를 만들어야 합니다. 객체를 재사용하는 것은 체험의 중간이 아니라 시작 시점에 객체들을 인스턴스화하고 업로드할 수 있도록 합니다.

Dol Guldur 미로에서 사용된 3D 객체의 건물 블록들.

리벤델(Rinvendell)에서는 사용자의 여행 진행에 따라 Z-깊이(Z-Depth)에 대한 지속적인 재위치를 나타내는 많은 량의 지형 섹션들을 가지게 됩니다. 사용자가 섹션을 지나칠수록 이들은 점점 먼 거리에서 재위치됩니다.

우리는 Dol Guldur 성을 위해 매 게임마다 미로를 재생성하기를 원했습니다. 이것을 하기 위해 미로를 재생성하는 스크립트를 생성해습니다.

맵을 재생성하려면 클릭하세요.

아주 커다란 씬에서 시작부터 전체 구조를 하나의 커다란 메쉬로 병합하는 것은 낮은 성능을 보입니다. 이것을 고려하기 위해 우리는 빌딩 블록들이 뷰에 존재하는지에 따라 보일 것인지 안보일 것인지를 결정했습니다. 우리는 시작부터 2D 레이캐스터(Raycaster) 스크립트의 사용에 대한 아이디어를 가지고 있었으나 최종적으로 three.js에 내장된 절두체 컬링(Frustom Culling)을 사용했습니다. 우리는 사용자가 "위험"에 처했을 때 줌을 위해 레이캐스터 스크립트를 재사용했습니다. (역주: 레이캐스터 스크립트는 성능은 느리지만 정밀한 충돌 검출이 가능하므로 사용자가 위험에 처했을 경우 줌 표현을 할 때 이를 이용하여 카메라의 시야를 방해하지 않는 위치를 조정하는데 사용했다는 의미로 이해할 수 있습니다.)

Dol Guldur 미로에서의 레이캐스터(Raycaster) 스크립트.

다음으로 다루어야 할 것은 사용자 인터랙션입니다. 데스크탑에서는 입력을 위한 마우스와 키보드가 있습니다만 모바일 디바이스에서 여러분의 사용자는 터치, 스와이프(Swipe), 핀치(Pinch), 디바이스 오리엔테이션(Orientation) 등으로 인터랙션을 수행합니다.

모바일 웹 체험의 터치 인터랙션 사용

터치 지원을 추가하는 것은 어렵지 않습니다. 이 주제에 대해 읽을만한 훌륭한 글들이 있습니다. 그러나 몇가지 사소한 것들이 이를 좀 더 복잡하게 만들 수 있습니다.

여러분은 터치(touch) 및 마우스를 둘 다 가지고 있을 수 있습니다. 크롬북 픽셀과 기타 터치가 가능한 랩탑들은 마우스와 터치 지원 모두를 가지고 있습니다. 한가지 일반적인 실수는 디바이스가 터치가 가능한지를 확인하고 마우스 이벤트 없이 터치 이벤트에 대한 리스너(Listner)만 등록을 하는 것입니다.

이벤트 리스너에서 렌더링을 갱신하지 마시기 바랍니다. 터치 이벤트를 변수에 저장하고 대신 그것들은 requestAnimationFrame 렌더링 루프에서 반응하게 하시기 바랍니다. 이것은 성능을 개선하고 또한 충돌이 나는 이벤트들을 합칠 수 있습니다. 이벤트 리스너에서 새 객첼들을 생성하는 대신 객체를 재사용하는 것을 명심하시기 바랍니다.

멀티 터치인 경우 다음을 기억하시 바랍니다. event.touches는 모든 터치들의 배열입니다. 몇가지 경우에서 event.tagetTouches나 event.changedTouches를 검색하고 관심있는 터치들만 반응하도록 하는 것은 더 흥미롭습니다. 스와이프(Swipe)에서 분리된 탭들을 위해 우리는 터치가 이동(Swipe)했거나 동일한 (탭된) 상태인지를 확인하여 지연시간(Delay)를 사용할 수 있습니다. 우리는 핀치(Pinch)를 사용하기 위해 2개의 초기 터치들의 거리와 그것이 시간에 따라 어떻게 변하는지 측정했습니다.

트롤숲 게임을 위한 스와이프(Swipe) 검출의 예. 캔바스에서 스와이프를 시도해보세요.

3D 월드에서 여러분은 카메라가 마우스 대 스와이프(Swipe) 동작에 어떻게 반응할지를 결정해야 합니다. 카메라 이동을 추가하는 일반적인 방법 하나는 마우스 이동을 따르도록 하는 것입니다. 이것은 마우스 좌표를 이용한 직접 조작과 더불어 델타의 이동(위치 변경)을 완료할 수 있습니다. 여러분은 데스크탑 브라우저처럼 모바일 디바이스에서 동일한 동작을 항상 원하지 않을 것입니다.

더 작은 스크린이나 터치스크린에 대해 조정을 할 때 사용자의 손가락과 UI 인터랙션 그래픽이 여러분이 보여주기를 원하는 방식임을 종종 발견할 수 있습니다. 이것은 네이티브 앱들을 디자인할 때 사용되어 온 어떤 것들이지만 웹 체험 시 미리 생각해야할 것은 정말 아닙니다. 이것은 진정으로 디자이너와 UX 디자이너의 도전일 것입니다. (역주: 작은 스크린에서 터치와 UI 그래픽 요소들에 대한 적절한 배치와 동작들의 조정이 필요하겠지만 이것은 개발의 범주라기 보다는 기획과 디자인 단계에 있어 고려되어야 한다는 것을 뜻합니다.)

요약

이 프로젝트에서 우리의 전체적인 체험은 모바일에서 정말 잘동작하는 WebGL이며 새롭고 하이엔드 디바이스에서는 더욱 더 그렇습니다. 성능이 확인할 때 폴리곤의 숫자나 텍스쳐의 크기가 다운로드와 초기화 시간에 가장 큰 영향을 주고 재질(Material), 쉐이더(Shader)와 WebGL 캔버스의 크기는 모바일의 성능을 위한 최적화에서 가장 중요한 부분입니다. 그러나, 전체의 합이 성능에 영향을 주기 때문에 여러분은 최적화 횟수를 위해 모든 것을 해야합니다.

또한 모바일 디바이스를 타겟팅하는 것은 여러분이 터치 인터랙션에 대해 고려해야만 하며 그것은 픽셀 크기 — 스크린의 물리적인 크기 — 만이 아닙니다 몇가지 경우에서 3D 카메라를 실제 우리가 보고자 하는 것보다 가까이 이동해야 했습니다.

실험(Experiment)은 이미 런칭되었고 환상적인 여정이었습니다. 여러분도 즐기시기 바랍니다!

해보고 싶으신가요? 중간계로의 여정을 직접 확인해보시기 바랍니다.

Comments

0