Создание 3D-глобуса «Чудеса света»

Ilmari Heikkinen

Знакомство с 3D-глобусом «Чудеса света»

Если вы просматривали недавно запущенный сайт Google World Wonders в браузере с поддержкой WebGL, вы, возможно, заметили причудливый вращающийся глобус в нижней части экрана. В этой статье вы узнаете, как устроен глобус и из чего мы его построили.

Чтобы дать вам краткий обзор, глобус «Чудеса света» — это сильно доработанная версия глобуса WebGL, разработанная командой Google Data Arts. Мы взяли исходный глобус, удалили фрагменты гистограммы, изменили шейдеры, добавили причудливые кликабельные HTML-маркеры и геометрию континента Natural Earth из демо-версии Mozilla GlobeTweeter (большое спасибо Седрику Пинсону!) И все это для того, чтобы создать красивый анимированный глобус, соответствующий дизайну сайта. цветовую гамму и добавляет сайту дополнительный уровень изысканности.

Задача дизайна земного шара заключалась в том, чтобы создать красивую анимированную карту с интерактивными маркерами, расположенными поверх объектов всемирного наследия. Помня об этом, я начал искать что-нибудь подходящее. Первое, что пришло на ум, — это WebGL Globe, созданный командой Google Data Arts. Это глобус, и он выглядит круто. Что тебе еще нужно, а?

Настройка глобуса WebGL

Первым шагом в создании виджета глобуса была загрузка глобуса WebGL и его запуск. WebGL Globe доступен на сайте Google Code , его легко загрузить и запустить. Загрузите и распакуйте zip-архив , перейдите к нему и запустите базовый веб-сервер: python -m SimpleHTTPServer . (Обратите внимание: по умолчанию UTF-8 не включен; вы можете использовать его .) Теперь, если вы перейдете по адресу http://localhost:8000/globe/globe.html вы должны увидеть глобус WebGL.

Когда WebGL Globe был запущен, пришло время отрезать все ненужные части. Я отредактировал HTML, исключив элементы пользовательского интерфейса, и удалил настройки гистограммы глобуса из функции инициализации глобуса. В конце этого процесса на моем экране появился очень простой WebGL Globe. Вы можете вращать его, и это будет выглядеть круто, но это все.

Чтобы избавиться от ненужного, я удалил все элементы пользовательского интерфейса из файла index.html глобуса и отредактировал скрипт инициализации, чтобы он выглядел следующим образом:

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 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

Говоря о маркерах, работая с глобусом и сушей, я начал работу над метками. Я решил использовать HTML-элементы в стиле CSS для маркеров, чтобы упростить создание и стилизацию маркеров, а также потенциально повторно использовать маркеры в 2D-карте, над которой работала команда. В то время я также не знал простого способа сделать маркеры WebGL кликабельными и не хотел писать дополнительный код для загрузки/создания моделей маркеров. Оглядываясь назад, можно сказать, что маркеры CSS работали хорошо, но имели тенденцию иногда сталкиваться с проблемами производительности, когда наборщики и средства визуализации браузера находились в периоде изменений. С точки зрения производительности использование маркеров в WebGL было бы лучшим вариантом. Опять же, CSS-маркеры сэкономили много времени на разработку.

Маркеры CSS состоят из пары элементов div, позиционированных в абсолютном значении с помощью свойства преобразования CSS. Фон маркеров представляет собой градиент CSS, а треугольная часть маркера представляет собой повернутый элемент div. У маркеров есть небольшая тень, позволяющая выделить их из фона. Самая большая проблема с маркерами заключалась в том, чтобы заставить их работать достаточно хорошо. Как бы печально это ни звучало, рисование нескольких десятков элементов div, которые перемещаются и меняют свой z-индекс в каждом кадре, — довольно хороший способ вызвать всевозможные ошибки рендеринга в браузере.

Способ синхронизации маркеров с 3D-сценой не слишком сложен. Каждому маркеру соответствует Object3D в сцене Three.js, который используется для отслеживания маркеров. Чтобы получить координаты экранного пространства, я беру матрицы Three.js для глобуса и маркера и умножаю на них нулевой вектор. Отсюда я получаю положение маркера в сцене. Чтобы получить положение маркера на экране, я проецирую положение сцены через камеру. Результирующий проецируемый вектор имеет координаты экранного пространства для маркера, готовые для использования в 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, не удаляя их, когда они уходили за земной шар. Мы также экспериментировали с использованием 3D-преобразований вместо z-индексов, но по какой-то причине в приложении это не работало (но в сокращенном тестовом примере это работало, поймите), и до запуска оставалось несколько дней. на тот момент пришлось оставить эту часть на обслуживание после запуска.

Когда вы нажимаете на маркер, он разворачивается в список кликабельных топонимов. Это все обычные HTML-материалы DOM, поэтому писать их было очень легко. Все ссылки и рендеринг текста работают без каких-либо дополнительных усилий с нашей стороны.

Сжатие размера файла

Поскольку демо-версия работала и была подключена к остальной части сайта «Чудеса Света», оставалась еще одна большая проблема, которую нужно было решить. Размер сетки в формате JSON для суши земного шара составлял около 3 мегабайт. Не подходит для главной страницы сайта-витрины. Хорошо, что сжатие сетки с помощью gzip позволило уменьшить ее размер до 350 КБ. Но эй, 350 КБ все еще немного великоваты. Спустя пару писем нам удалось привлечь Вон Чуна, который работал над сжатием огромных сеток Google Body, чтобы он помог нам со сжатием сетки. Он сжал сетку из большого плоского списка треугольников, представленных в виде координат JSON, в сжатые 11-битные координаты с индексированными треугольниками и уменьшил размер файла до 95 КБ в сжатом виде.

Использование сжатых сеток не только экономит полосу пропускания, но и ускоряет анализ сеток. Преобразование 3 МБ строковых чисел в собственные числа требует гораздо больше работы, чем анализ ста КБ двоичных данных. И в результате уменьшение размера страницы на 250 КБ очень изящно, поскольку начальное время загрузки становится менее секунды при соединении со скоростью 2 Мбит / с. Быстрее и меньше, потрясающий соус!

В то же время я возился с загрузкой исходных шейп-файлов Natural Earth, из которых получена сетка GlobeTweeter. Мне удалось загрузить шейп-файлы, но для их рендеринга в виде плоской суши требуется их триангуляция (естественно, с отверстиями для озер). Я получил триангуляцию фигур с помощью утилит THREE.js, но не дырок. И полученные сетки имели очень длинные края, что требовало разделения сетки на более мелкие треугольники. Короче говоря, мне не удалось заставить его работать вовремя, но самое интересное заключалось в том, что дополнительно сжатый формат Shapefile дал бы вам модель суши размером 8 КБ. Ох, ну, может быть, в следующий раз.

Будущая работа

Одна вещь, над которой нужно немного поработать, — это сделать анимацию маркеров более приятной. Теперь, когда они уходят за горизонт, эффект немного безвкусный. Кроме того, было бы неплохо иметь классную анимацию открытия маркера.

С точки зрения производительности не хватает двух вещей: оптимизации алгоритма разделения сетки и ускорения работы маркеров. Кроме того, дела обстоят шикарно. Ура!

Краткое содержание

В этой статье я рассказал, как мы построили 3D-глобус для проекта Google World Wonders. Надеюсь, вам понравились примеры, и вы попробуете создать собственный виджет глобуса.

Рекомендации