세계 불가사의 3D 지구 만들기

Ilmari Heikkinen

World Wonders 3D 지구본 소개

WebGL이 지원되는 브라우저에서 최근에 출시된 Google World Wonders 사이트를 방문했다면 화면 하단에서 지구가 회전하는 모습을 본 적이 있을 것입니다. 이 도움말에서는 지구본의 작동 방식과 지구를 만드는 데 사용한 방법을 설명합니다.

World Wonders 지구본은 Google Data Arts Team이 WebGL Globe를 크게 수정한 버전입니다. 우리는 원래의 지구본을 가져와 막대 그래프 비트를 제거하고 셰이더를 변경하고 Mozilla의 GlobeTweeter 데모에서 만든 멋진 클릭 가능한 HTML 마커와 Natural Earth 대륙 도형을 추가했습니다 (Cedric Pinson 덕분에 이 모든 것을 사용하면 사이트의 색 구성표와 일치하는 멋진 애니메이션 지구본을 만들 수 있으며 사이트에 세련미를 더할 수 있습니다.

지구본을 위한 디자인 개요에서는 세계 유산 위에 클릭할 수 있는 마커가 배치된 멋진 애니메이션 지도를 넣는 것이었습니다. 그래서 저는 적절한 것을 찾기 시작했습니다. 가장 먼저 떠오르는 것은 Google 데이터 아트팀에서 만든 WebGL Globe입니다. 지구본이고 멋있어 보입니다. 더 필요한 게 있으세요?

WebGL Globe 설정

지구본 위젯을 만드는 첫 번째 단계는 WebGL Globe를 다운로드하여 실행하는 것이었습니다. WebGL Globe는 Google Code를 통해 온라인으로 제공되며 간편하게 다운로드하고 실행할 수 있습니다. zip을 다운로드하여 압축을 푼 후 여기로 CD로 이동하여 기본 웹 서버 python -m SimpleHTTPServer를 실행합니다. (기본적으로 UTF-8이 사용되지 않으므로 사용할 수 있습니다.) 이제 http://localhost:8000/globe/globe.html로 이동하면 WebGL Globe가 표시됩니다.

WebGL Globe를 가동하고 나면 이제 필요 없는 부분을 모두 삭감해야 했습니다. UI 비트를 소비하도록 HTML을 수정하고 지구본 초기화 함수에서 지구본 막대 그래프 설정 항목을 삭제했습니다. 그 과정에서 제 화면에는 아주 기초적인 WebGL Globe가 생겼습니다. 돌려보면 멋져 보일 수 있지만 그게 다입니다.

불필요한 것을 줄이기 위해 지구본의 index.html에서 모든 UI 요소를 삭제하고 초기화 스크립트를 다음과 같이 수정했습니다.

if(!Detector.webgl){
  Detector.addGetWebGLMessage();
} else {
  var container = document.getElementById('container');
  var globe = new DAT.Globe(container);
  globe.animate();
}

대륙 도형 추가

카메라를 지구 표면에 가깝게 배치하려고 했지만 지구본을 확대하면 텍스처 해상도가 부족하다는 점이 분명해졌습니다. 확대하면 WebGL Globe의 텍스처가 고르지 않고 흐릿해집니다. 더 큰 이미지를 사용할 수도 있었지만 지구본을 다운로드하고 실행하는 속도가 느려질 수 있으므로 육지와 경계선을 벡터로 표현하기로 했습니다.

육지 기하학의 경우 오픈소스 GlobeTweeter 데모를 사용해 3D 모델을 Three.js에 로드했습니다. 모델이 로드되고 렌더링되었으니 이제 지구본의 디자인을 다듬어 보겠습니다. 첫 번째 문제는 지구 육지 모델이 WebGL Globe와 같은 높이가 될 만큼 구형이 아니므로 결국 육지 모델을 더 구형으로 만드는 빠른 메시 분할 알고리즘을 작성했습니다.

구면 육지 모델을 사용하여 지구 표면에서 약간 오프셋된 위치에 떠다니는 대륙을 만들 수 있었고, 그 아래에 검은색 2픽셀 선으로 윤곽이 그려져 일종의 그림자가 생깁니다. 트론과 같은 느낌을 주기 위해 네온 색상의 윤곽선도 실험했습니다.

지구본과 육지가 렌더링되면서 저는 다양한 모양의 지구본을 실험해 보았습니다. 절제된 흑백 스타일을 살리고 싶었기 때문에 저는 그레이 스케일의 지구본과 대륙을 고수했습니다. 앞서 언급한 네온 윤곽선 외에도 밝은 색 배경에 육지가 어두운 어두운 지구를 그려 봤는데, 실은 꽤 멋져 보입니다. 하지만 대비가 너무 낮아서 쉽게 읽을 수 없고 프로젝트 분위기에 맞지 않아서 스크랩했습니다.

지구본을 디자인할 때 초창기에는 유약을 바른 도자기처럼 보이도록 하려고 했습니다. 그 것은 도자기 모양을 만들기 위해 셰이더를 작성하지 못했기 때문에 시도해 보지 못했습니다 (비주얼 머티리얼 편집기가 적합할 것 같음). 가장 가까운 건 검은 육지로 빛나는 하얀색 지구본이야. 깔끔하지만 너무 고대비입니다. 그리 보기 좋지도 않습니다. 다시 도전해 볼게요.

흑백 지구본의 셰이더는 일종의 가짜 디퓨즈 백라이트 조명을 사용합니다. 지구본의 밝기는 화면 평면과 수직인 표면의 거리에 따라 다릅니다. 따라서 지구본 중앙에서 화면을 가리키는 픽셀은 어둡고 지구 가장자리의 픽셀은 밝습니다. 밝은 배경과 함께 사용하면 지구본에 밝은 디퓨즈 배경을 반사하는 모습을 볼 수 있어 세련된 쇼룸 룩을 연출할 수 있습니다. 검은색 지구본도 WebGL Globe 텍스처를 유광 맵으로 사용하므로 대륙 선반 (얕은 수역)이 지구의 다른 지역에 비해 밝아 보입니다.

검은색 지구본의 바다 셰이더 모양은 다음과 같습니다. 매우 기본적인 꼭짓점 셰이더와 허술한 '조정 조정' 프래그먼트 셰이더

    'ocean' : {
      uniforms: {
        'texture': { type: 't', value: 0, texture: null }
      },
      vertexShader: [
        'varying vec3 vNormal;',
        'varying vec2 vUv;',
        'void main() {',
          'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
          'vNormal = normalize( normalMatrix * normal );',
          'vUv = uv;',
        '}'
      ].join('\n'),
      fragmentShader: [
        'uniform sampler2D texture;',
        'varying vec3 vNormal;',
        'varying vec2 vUv;',
        'void main() {',
          'vec3 diffuse = texture2D( texture, vUv ).xyz;',
          'float intensity = pow(1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) ), 4.0);',
          'float i = 0.8-pow(clamp(dot( vNormal, vec3( 0, 0, 1.0 )), 0.0, 1.0), 1.5);',
          'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * intensity;',
          'float d = clamp(pow(max(0.0,(diffuse.r-0.062)*10.0), 2.0)*5.0, 0.0, 1.0);',
          'gl_FragColor = vec4( (d*vec3(i)) + ((1.0-d)*diffuse) + atmosphere, 1.0 );',
        '}'
      ].join('\n')
    }

결국 하늘에서 밝은 회색 땅이 비추는 어두운 지구본을 택했습니다. 디자인 개요와 가장 가깝고 보기 좋고 읽기 쉬웠습니다. 또한 지구본을 약간 대비가 되도록 하면 마커와 콘텐츠의 나머지 부분이 더 눈에 띄도록 만들 수 있습니다. 아래 버전은 완전히 검은색 바다를 사용하는 반면 프로덕션 버전에는 진한 회색 바다와 약간 다른 마커가 있습니다.

CSS로 마커 만들기

마커에 관해서는 지구본과 땅이 작동하는 상태에서 장소 표시 아이콘을 만들기 시작했습니다. 저는 마커에 CSS 스타일 HTML 요소를 사용하기로 결정했습니다. 보다 쉽게 마커를 만들고 스타일을 지정할 수 있을 뿐만 아니라, 팀이 작업 중인 2D 지도에서 마커를 재사용할 수도 있습니다. 당시 저는 WebGL 마커를 클릭 가능하도록 만들 수 있는 쉬운 방법을 몰랐고, 마커 모델을 로드 / 생성하기 위한 추가 코드를 작성하고 싶지 않았습니다. 결과적으로, CSS 마커는 잘 작동했지만, 브라우저 컴포지터와 렌더러가 변동되는 동안에는 가끔 성능 문제가 발생하는 경향이 있었습니다. 성능 측면에서는 WebGL에서 마커를 사용하는 것이 더 나은 옵션일 것입니다. 또한 CSS 마커 덕분에 개발 시간이 상당히 절약되었습니다.

CSS 마커는 CSS 변환 속성을 사용하여 절대 위치로 배치된 몇 개의 div로 구성됩니다. 마커의 배경은 CSS 그라데이션이며 마커의 삼각형 부분은 회전된 div입니다. 마커에는 배경에서 표시되는 작은 그림자가 있습니다. 마커의 가장 큰 문제는 마커가 충분히 잘 작동하도록 하는 것이었습니다. 슬프게도 여러 개의 div를 이리저리 움직여 모든 프레임에서 Z-색인을 변경하는 것은 모든 종류의 브라우저 렌더링 문제를 일으키는 매우 좋은 방법입니다.

마커가 3D 장면과 동기화되는 방식은 그리 복잡하지 않습니다. Three.js 장면의 각 마커에는 마커를 추적하는 데 사용되는 해당 Object3D가 있습니다. 화면 공간 좌표를 얻기 위해 지구본과 마커의 Three.js 행렬을 취하고 0 벡터를 곱합니다. 그러면 마커의 장면 위치를 알 수 있습니다. 마커의 화면 위치를 가져오기 위해 카메라를 통해 장면 위치를 투영합니다. 그 결과로 투영된 벡터에는 마커의 화면 공간 좌표가 있으며 CSS에서 사용할 수 있습니다.

var mat = new THREE.Matrix4();
var v = new THREE.Vector3();

for (var i=0; i<locations.length; i++) {
  mat.copy(scene.matrix);
  mat.multiplySelf(locations[i].point.matrix);
  v.set(0,0,0);
  mat.multiplyVector3(v);
  projector.projectVector(v, camera);
  var x = w * (v.x + 1) / 2; // Screen coords are between -1 .. 1, so we transform them to pixels.
  var y = h - h * (v.y + 1) / 2; // The y coordinate is flipped in WebGL.
  var z = v.z;
}

결국 가장 빠른 접근 방법은 CSS 변환을 사용하여 마커를 이동하는 것이었습니다. 불투명도는 Firefox에서 느린 경로를 트리거하고 모든 마커를 DOM에 유지하고 지구본 뒤쪽으로 이동할 때 제거하지 않기 때문에 불투명도 페이드를 사용하지 않습니다. 우리는 Z-색인 대신 3D 변환을 사용해 실험해 봤지만, 앱에서는 제대로 작동하지 않았습니다. 하지만 축소된 테스트 사례에서는 제대로 작동했습니다. 하지만 출시 후 며칠만 남았기 때문에 출시 후 유지관리 작업을 수행해야 했습니다.

마커를 클릭하면 클릭 가능한 장소 이름 목록으로 확장됩니다. 이는 모두 일반적인 HTML DOM 항목이므로 작성하기가 매우 쉬웠습니다. 모든 링크 및 텍스트 렌더링은 별도의 작업 없이 작동합니다.

파일 크기 축소

데모가 제대로 작동하여 World Wonders 사이트의 다른 사이트와 연결되었는데 여전히 해결해야 할 큰 문제가 하나 남아 있었습니다. 지구본 육지를 위한 JSON 형식 메시의 크기는 약 3MB였습니다. 쇼케이스 사이트의 첫 페이지에는 적합하지 않습니다. 다행히 gzip으로 메시를 압축하자 350KB로 줄었습니다. 하지만 350KB는 아직 조금 큽니다. 이후 몇 번의 이메일을 통해 거대한 Google 바디 메시 압축 작업을 담당한 원천을 고용하여 메시 압축을 할 수 있게 되었습니다. 그는 JSON 좌표로 주어진 큰 삼각형 목록에서 인덱싱된 삼각형이 있는 압축된 11비트 좌표로 메시를 압착하여 파일 크기를 gzip으로 95KB로 줄였습니다.

압축된 메시를 사용하면 대역폭이 절약될 뿐만 아니라 메시의 파싱 속도도 더 빨라집니다. 3MB의 문자열화된 숫자를 기본 숫자로 변환하는 것은 100KB의 바이너리 데이터를 파싱하는 것보다 훨씬 더 많은 작업이 필요합니다. 그 결과 페이지 크기가 250KB 줄어드는 것도 좋습니다. 또한 2Mbps 연결에서 1초 미만의 초기 로드 시간을 얻을 수 있습니다. 더 빠르고 작게, 대단합니다.

이와 동시에 GlobeTweeter 메시에서 파생된 원본 Natural Earth Shapefile을 로드하는 작업을 하고 있었습니다. Shapefile을 로드했지만 평평한 육지로 렌더링하려면 삼각형을 삼각화해야 합니다 (호수용 구멍이 있음). 구멍이 아닌 THREE.js 유틸리티를 사용하여 도형을 삼각형으로 표시했습니다. 그 결과 메시의 가장자리가 매우 길었기 때문에 더 작은 트리로 분할해야 했습니다. 간단히 말씀드리면, 시간 내에 제대로 작동하지는 못했지만, 좋은 점은 추가로 압축된 Shapefile 형식을 사용하면 8KB의 육지 모델을 얻을 수 있다는 점입니다. 아마 다음에도 만나요.

향후 작업

약간의 추가 작업이 필요할 수 있는 한 가지는 마커 애니메이션을 더 멋지게 만드는 것입니다. 이제 수평선 위로 이동하면 효과가 약간 불안정합니다. 또한 마커를 여는 멋진 애니메이션이 있으면 좋습니다.

성능 면에서 부족한 두 가지는 메시 분할 알고리즘을 최적화하고 마커를 더 빠르게 만드는 것입니다. 그 외에는 뭔가 흥미진진해요. 만세!

요약

본 도움말에서는 Google World Wonders 프로젝트를 위해 3D 지구본을 구축한 방법을 설명했습니다. 예제가 도움이 되었기를 바라며 사용자 지정 지구본 위젯을 직접 빌드해 보시기 바랍니다.

참조