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

Ilmari Heikkinen

세계의 불가사의 3D 지구본 소개

WebGL 지원 브라우저에서 최근에 출시된 Google 세계의 경이 사이트를 본 적이 있다면 화면 하단에 회전하는 멋진 지구본을 보셨을 것입니다. 이 도움말에서는 지구의 작동 방식과 지구를 만드는 데 사용된 도구를 설명합니다.

간단히 살펴보면 세계 기념물 지구본은 Google 데이터 아트팀에서 WebGL 지구본을 대폭 조정하여 만든 버전입니다. 원래 지구본을 가져와서 막대 그래프 비트를 제거하고, 셰이더를 변경하고, 클릭 가능한 멋진 HTML 마커와 Mozilla의 GlobeTweeter 데모에서 가져온 Natural Earth 대륙 도형을 추가했습니다 (세드릭 핀슨님, 감사합니다!). 사이트의 색 구성표와 일치하고 사이트에 세련된 느낌을 더하는 멋진 애니메이션 지구를 만들기 위한 것입니다.

지구본의 디자인 브리프는 클릭 가능한 마커가 세계 문화유산 위에 배치된 멋진 애니메이션 지도였습니다. 이를 염두에 두고 적절한 방법을 찾기 시작했습니다. 가장 먼저 떠오른 것은 Google 데이터 아트팀에서 빌드한 WebGL 지구입니다. 지구본 모양으로 멋있어 보입니다. 그 밖에 필요한 게 있나요?

WebGL 지구 설정

지구본 위젯을 만드는 첫 번째 단계는 WebGL 지구본을 다운로드하여 실행하는 것이었습니다. WebGL 지구본은 Google Code에서 온라인으로 제공되며 간편하게 다운로드하여 실행할 수 있습니다. zip 파일을 다운로드하여 압축을 푼 후 해당 디렉터리로 이동하여 기본 웹서버 python -m SimpleHTTPServer를 실행합니다. 기본적으로 UTF-8은 사용 설정되어 있지 않지만 사용할 수 있습니다. 이제 http://localhost:8000/globe/globe.html로 이동하면 WebGL 지구본이 표시됩니다.

WebGL 지구본이 작동하기 시작했으므로 이제 불필요한 부분을 모두 삭제할 때입니다. HTML을 수정하여 UI 비트를 삭제하고 지구 초기화 함수에서 지구 막대 그래프 설정 항목을 삭제했습니다. 이 과정이 끝나면 화면에 매우 간단한 WebGL 지구본이 표시됩니다. 회전하면 멋있어 보이지만 그뿐입니다.

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

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

대륙 도형 추가

카메라를 지구 표면에 가깝게 두고 싶었지만 지구를 확대하여 테스트했을 때 텍스처 해상도가 부족한 것이 명확해졌습니다. 확대하면 WebGL 지구의 텍스처가 블록이 있고 흐리게 보입니다. 더 큰 이미지를 사용할 수도 있지만 그러면 지구본을 다운로드하고 실행하는 속도가 느려지므로 육지와 국경을 벡터로 표현하는 방식을 선택했습니다.

육지 도형의 경우 오픈소스 GlobeTweeter 데모를 사용하고 그 안에 있는 3D 모델을 Three.js에 로드했습니다. 모델을 로드하고 렌더링했으므로 이제 지구의 모양을 다듬을 차례입니다. 첫 번째 문제는 지구 대륙 모델이 WebGL 지구와 평평해질 만큼 구형이 아니었기 때문에 대륙 모델을 더 구형으로 만드는 빠른 메시 분할 알고리즘을 작성했습니다.

구형 대륙 모델을 사용하여 지구 표면에서 약간 오프셋하여 배치하여, 아래에 검은색 2픽셀 선으로 윤곽을 그려 그림자를 표현하는 떠 있는 대륙을 만들었습니다. 또한 일종의 트론과 같은 느낌을 주기 위해 형광색 윤곽선을 실험해 보았습니다.

지구본과 대륙 렌더링을 사용하여 지구본의 다양한 디자인을 실험하기 시작했습니다. 차분한 모노크롬 스타일을 사용하고자 흑백 지구본과 대륙을 사용했습니다. 앞서 언급한 네온 윤곽선 외에도 밝은 배경에 어두운 육지가 있는 어두운 지구본을 시도해 보았는데, 꽤 멋진 것 같습니다. 하지만 대비가 너무 낮아 쉽게 읽을 수 없고 프로젝트의 느낌과도 맞지 않아 삭제했습니다.

지구본 디자인에 관해 초기에 생각했던 또 다른 아이디어는 유약을 바른 도자기처럼 보이게 하는 것이었습니다. 도자기 모양을 만드는 셰이더를 작성하지 못해 이 방법은 시도해 보지 못했습니다 (시각적 재료 편집기가 있으면 좋을 것 같습니다). 가장 근접한 것은 검은색 대륙이 있는 흰색의 빛나는 지구였습니다. 깔끔하지만 대비가 너무 높습니다. 그리고 깔끔하지도 않습니다. 또 하나의 스크랩 힙이 생겼습니다.

흑백 지구의 셰이더는 일종의 가짜 확산 백라이트 조명을 사용합니다. 지구의 밝기는 표면 법선과 화면 평면의 거리에 따라 달라집니다. 따라서 지구의 중앙에 있는 픽셀은 화면을 향하고 있으므로 어둡고 지구의 가장자리에 있는 픽셀은 밝습니다. 밝은 배경과 결합하면 지구본이 밝은 확산 배경을 반사하여 고급스러운 쇼룸 분위기를 연출할 수 있습니다. 검은색 지구본은 WebGL 지구본 텍스처를 광택 맵으로 사용하기도 합니다. 따라서 대륙붕 (얕은 수역)이 지구본의 다른 부분에 비해 반짝거리게 보입니다.

검은색 지구의 해양 셰이더는 다음과 같습니다. 매우 기본적인 정점 셰이더와 '오, 꽤 괜찮아 보이네 조정 조정' 하는 식으로 해킹된 프래그먼트 셰이더

    '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로 마커 만들기

마커에 관해 말하자면 지구와 대륙이 작동하는 상태에서 장소 마커 작업을 시작했습니다. 마커를 더 쉽게 만들고 스타일을 지정할 수 있으며 팀에서 작업 중인 2D 지도에서 마커를 재사용할 수 있도록 마커에 CSS 스타일의 HTML 요소를 사용하기로 결정했습니다. 당시에는 WebGL 마커를 클릭 가능하게 만드는 쉬운 방법을 몰랐고 마커 모델을 로드 / 만들기 위한 추가 코드를 작성하고 싶지 않았습니다. 돌이켜보면 CSS 마커는 잘 작동했지만 브라우저 컴포저와 렌더러가 변화하는 시기에 성능 문제가 발생하는 경향이 있었습니다. 성능 측면에서 보면 WebGL에서 마커를 사용하는 것이 더 나은 선택이었을 것입니다. 하지만 CSS 마커를 사용하면 상당한 개발 시간을 절약할 수 있었습니다.

CSS 마커는 CSS 변환 속성으로 절대 위치 지정된 두 개의 div로 구성됩니다. 마커의 배경은 CSS 그라데이션이고 마커의 삼각형 부분은 회전된 div입니다. 마커에는 배경에서 튀어나오도록 작은 그림자가 있습니다. 마커의 가장 큰 문제는 충분히 성능을 발휘하도록 하는 것이었습니다. 안타깝게도 모든 프레임에서 움직이고 z-index를 변경하는 수십 개의 div를 그리는 것은 모든 종류의 브라우저 렌더링 문제를 트리거하는 좋은 방법입니다.

마커가 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 작업이므로 작성하기가 매우 쉽습니다. 개발자가 별도로 작업하지 않아도 모든 링크와 텍스트 렌더링이 작동합니다.

파일 크기 줄이기

데모가 작동하고 나머지 세계 명소 사이트에 연결되었지만 해결해야 할 큰 문제가 하나 남아 있었습니다. 지구 대륙의 JSON 형식 메시는 크기가 약 3MB였습니다. 쇼케이스 사이트의 첫 페이지에는 적합하지 않습니다. gzip으로 메시지를 압축하면 350KB로 줄었습니다. 하지만 350KB는 여전히 약간 큽니다. 몇 번의 이메일 교환 끝에 거대한 Google 신체 메시 압축 작업을 담당했던 원 춘을 영입하여 메시 압축 작업을 도와주도록 했습니다. 그는 메시를 JSON 좌표로 제공된 큰 평면 삼각형 목록에서 색인이 생성된 삼각형이 포함된 압축된 11비트 좌표로 축소하여 파일 크기를 95KB(Gzip)로 줄였습니다.

압축된 메시스를 사용하면 대역폭을 절약할 뿐만 아니라 메시스를 더 빠르게 파싱할 수 있습니다. 문자열로 된 3MB의 숫자를 네이티브 숫자로 변환하는 작업은 100KB의 바이너리 데이터를 파싱하는 것보다 훨씬 더 많은 작업이 필요합니다. 그 결과 페이지의 크기가 250KB 감소했으며, 2Mbps 연결 시 초기 로드 시간이 1초도 안 되었습니다. 더 빠르고 더 작아졌습니다.

동시에 GlobeTweeter 메시가 파생된 원래 Natural Earth Shapefile을 로드하면서 놀고 있었습니다. Shapefile을 로드할 수 있었지만 평평한 육지 덩어리로 렌더링하려면 삼각 측정을 해야 합니다 (물론 호수의 구멍 포함). THREE.js 유틸리티를 사용하여 도형을 삼각형으로 분할했지만 구멍은 분할되지 않았습니다. 그 결과 메시의 가장자리가 매우 길어 메시를 더 작은 삼각형으로 분할해야 했습니다. 간단히 말씀드리면 제때 작동하도록 할 수 없었지만, 더 압축된 Shapefile 형식을 사용하면 8KB의 육지 모델을 얻을 수 있었습니다. 아, 다음에 기회가 있을 겁니다.

향후 작업

마커 애니메이션을 더 멋지게 만드는 작업은 약간의 추가 작업이 필요할 수 있습니다. 이제 지평선 너머로 넘어가면 효과가 약간 촌스럽습니다. 또한 마커가 열리는 멋진 애니메이션이 있으면 좋을 것 같습니다.

성능 측면에서 메시 분할 알고리즘을 최적화하고 마커를 더 빠르게 만드는 두 가지가 부족합니다. 그 밖에는 문제가 없습니다. 만세!

요약

이 도움말에서는 Google 세계 명소 프로젝트용 3D 지구본을 빌드하는 방법을 설명했습니다. 예시를 즐기셨기를 바라며 나만의 맞춤 지구 위젯을 빌드해 보시기 바랍니다.

참조