Фронтенд Средиземья, часть первая

Оживление мира Средиземья с помощью мобильного WebGL

Так уж сложилось, что создание интерактивного, нагруженного с точки зрения мультимедиа, веб-проекта для смартфонов и планшетов является непростой задачей. Основными сдерживающими факторами были производительность, доступность API, ограничения в HTML5 аудио и отсутствие непрерывного воспроизведения видео.

В начале 2013-го года мы с друзьями из Google и Warner Bros. начали проект, нацеленный в первую очередь на мобильный устройства, чтобы рассказать о новом фильме про хоббита («Хоббит: Пустошь Смауга»). Создание комплексного мобильного мультимедиа-эксперимента для Chrome было действительно вдохновляющей и сложной задачей.

Этот сайт оптимизирован под мобильный Chrome на новых устройствах семейства Nexus, где доступен WebGL и Web Audio. Однако большая часть сайта доступна и на устройствах и в браузерах, не поддерживающих WebGL благодаря аппаратному ускорению композитинга и CSS анимаций.

Основа проекта — это карта Средиземья с локациями и персонажами из фильма «Хоббит». Использование WebGL дало возможность инсценировать богатый мир трилогии Хоббита и позволить пользователям интерактивно исследовать его.

Сложности при работе с WebGL на мобильных устройствах

Во-первых, термин «мобильные устройства», очень широк. Технические возможности устройств очень сильно разнятся. Как разработчик вы должны решить, хотите ли вы поддерживать больше устройств с простыми эффектами или, как мы, ограничить поддерживаемые устройства теми, которые способны отобразить более реалистичный трёхмерный мир. Для «Путешествия через Средиземье» мы сосредоточились на устройствах серии Nexus и пяти популярных смартфонах на базе Android.

В этом эксперименте мы использовали three.js, так как уже делали с его помощью несколько проектов использующих WebGL. Сначала был создан первоначальный вариант игры Trollshaw, который должен был хорошо работать на планшете Nexus 10. После первоначального тестирования на устройстве, у нас появился список пунктов оптимизации, которые были не менее актуальны и для слабых ноутбуков:

После того, как игра была оптимизирована, мы смогли получить стабильные 30 кадров в секунду. Теперь наша цель была в том, чтобы улучшить внешний вид без негативного влияния на частоту кадров. Мы попробовали разные подходы: некоторые оказали действительно большое влияние на производительность, а некоторые — нет.

Использование низкополигональных моделей

Давайте начнём с моделей. Использование низкополигональных моделей, безусловно, улучшает время загрузки, а также время, необходимое для инициализации сцены. Мы обнаружили, что можно значительно увеличить сложность моделей без ощутимого ухудшения производительности. Модели троллей, которые мы используем в этой игре, имеют около 5 тысяч граней, а сама сцена — примерно 40 тысяч, и этого вполне достаточно.

Один из троллей леса Trollshaw

В игре на одной из локаций, которая ещё не выпущена (на момент написания статьи - прим. перев.), мы увидели большее влияние сокращения количества полигонов на производительность. В этом случае для компьютеров мы загружали обычные объекты, а для мобильных устройств — менее детализированные. Создание различных наборов 3D-моделей требует дополнительной работы и это не всегда необходимо. Всё действительно зависит от того, насколько сложны ваши модели изначально.

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

Использование текстур низкого разрешения

Чтобы уменьшить время загрузки на мобильных устройствах, мы решили загружать различные текстуры в размере, в два раза меньше тех, которые мы использовали для компьютеров. Оказалось, что все устройства могут справиться с текстурами размером до 2048x2048px и большинство из них может обрабатывать и файлы размеров 4096x4096px. Поиск отдельных текстур не является проблемой после того, как они загружены в GPU. Общий объём текстур должен помещаться в ограничения графической памяти, чтобы избежать постоянного загрузки и выгрузки, но, скорее всего, это не основная проблема для веб-приложений. Однако объединение текстур в наименьшее количество спрайтов очень важно для сокращения вызовов отрисовки — это как раз то, что оказывает на мобильных устройствах большое влияние на производительность.

Текстура для одного из троллей из леса Trollshaw (оригинальный размер 512x512px)

Упрощение материалов и освещения

Выбор материалов может значительно повлиять на производительность на мобильном устройстве и к этому нужно подходить с умом. Использование MeshLambertMaterial (расчёт света по каждой вершине) в three.js вместо MeshPhongMaterial (расчёт света по каждому текселю) — один из способов, который мы использовали для оптимизации. В основном мы попытались использовать наиболее простые шейдеры с минимальным количеством расчетов освещения.

Чтобы увидеть, как используемые материалы влияют на производительность в сцене, вы можете переопределить материалы сцены с помощью MeshBasicMaterial. Это очень удобно для сравнения.

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

Оптимизировать Javascript

При разработке игр для мобильных устройств, GPU — не всегда самое большая проблема. Много времени требует CPU, в особенности на физику и скелетные анимации. Один из трюков, который иногда помогает, в зависимости от симуляции — это делать «дорогие» перерасчеты каждый второй кадр. Вы также можете использовать общие методы оптимизации JavaScript, когда дело доходит до группирования объектов, в частности сбор мусора и создание объекта.

Один из важных шагов для избежания ошибок при сборе мусора — это обновление существующих объектов в замыканиях вместо создания новых.

Рассмотрим, например, такой код:

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;
  //or
  currentPos.set(originPos.x+offsetX,originPos.y,originPos.z);
}

Обработчики событий по возможности должны обновлять только свойства, а обновления сцены оставить для цикла requestAnimationFrame.

Предварительный расчёт рэйкастинга (ray-casting) или его оптимизация — ещё один из хороших способов уменьшить задержки. Например, если вам надо присоединить объект к полигональной сетке во время движения по статичной траектории, то можно «записать» позиции во время первого прохода и потом считывать эти данные вместо повторения рэйкастинга по сетке. Ещё можно следить за движениями мышки по более простой (с меньшим количеством полигонов) невидимой сетке, как мы сделали в секции Rivendell. Учёт столкновений на высокополигональной сетке является очень медленным и в целом этого стоит избегать при разработке игр.

Рендерить WebGL вполовину размера и масштабировать canvas через CSS

Размер холста WebGL является, пожалуй, наиболее эффективным параметром, которые можно регулировать для оптимизации производительности. Чем больше canvas, которые вы используете для отрисовки трёхмерной сцены, тем больше пикселей нужно рендерить для каждого кадра. Это, конечно, влияет на производительность. Nexus 10 с его дисплеем повышенной плотности и разрешением в 2560x1600 пикселей, должен показывать в 4 раза больше пикселей, чем обычный планшет. Поэтому мы решили использовать хитрость, который заключается в создании canvas-а вполовину размера (50%) и последующим масштабировании его до нужного размера (100%) с принудительным аппаратным ускорением 3D-преобразований CSS. Обратной стороной этого является «мозаичное» изображение, где тонкие линии могут стать проблемой, но на экране с высоким разрешением этот эффект практически не заметен. Улучшение производительности, которое можно получить при этом, того стоит.

Одна и та же сцена без масштабирования холста на Nexus 10 (16FPS) и с масштабированием до 50% (33FPS).

Объекты как строительные блоки

Для того, чтобы создать большой лабиринт в замке Дол Гулдур замка и никогда не заканчивающуюся долину Ривенделл мы сделали набор трёхмерных моделей, которые мы повторно используем. При повторном использовании объектов мы точно знаем, что объекты были созданы и загружены в начале демонстрации, а не в его середине.

Трёхмерные «строительные блоки», используемые в лабиринте Дол Гулдура.

В Ривенделл у нас есть некоторое количество основных секций, которые мы постоянно перемещаем по Z-глубине, по ходу движения пользователя. Когда пользователь проходит секцию, она перемещается далеко вперёд.

Для замка Дол Гулдура мы хотели, чтобы лабиринт создавался заново для каждой игры. Для этого мы создали скрипт, который генерирует лабиринт.

Нажмите на лабиринт, чтобы сгенерировать его заново

Слияние всей структуры в один большой объект в начале игры привело бы к созданию очень большой сцены и сильному падению производительности. Мы решили бороться с этой проблемой, показывая только те блоки, которые находятся в поле видимости. С самого начала мы собирались использовать скрипт для двумерного рэйкастинга, но в конце концов мы решили использовать встроенный в three.js скрипт усечения. Мы повторно использовали этот скрипт для привлечения внимания на «опасности», с которой сталкивается игрок.

Скрипт для рэйкастинга, используемый в лабиринте Дол Гулдура.

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

Тактильное взаимодействие на мобильных устройствах

Добавить поддержку тач-событий не так уж сложно. Есть много материалов по этой теме. Но есть пара мелочей, которые могут все усложнить.

Пользователь может использовать мышь вместе с сенсорным экраном. Chromebook Pixel и другие ноутбуки с сенсорным вводом имеют и мышь и тач-интерфейс. Одна из распространенных ошибок — это добавлять обработчики только для тач-событий, если обнаружена поддержка сенсорного ввода, и не добавлять их для мыши.

Не обновляйте рендеринг в обработчиках событий. Сохраняйте тач-события в переменных, а реакцию на них — в requestAnimationFrame. Это повышает производительность, а также позволяет объединять конфликтующие события. Убедитесь, что вы повторно используете объекты, а не создаёте новые при обработке событий.

Помните, что это мультитач: event.touches представляет собой массив из всех касаний. В некоторых случаях будет более полезно обратиться к event.targetTouches или event.changedTouches и только реагировать на интересующие вас касания. Чтобы отличать тап от свайпа мы используем задержку и проверяем, произошло ли движение касания (свайп) или нет (тогда это тап). Чтобы определить щипок мы измеряем расстояние между двумя начальными прикосновениями и, то как они изменились со временем.

Пример обнаружения свайпа для игры Trollshaw. Намжите и проведите курсором мышки, либо сделайте свайп, если у вас у вас тачскрин, по серой области.

В трёхмерном мире нужно решить, как камера будет реагировать на мышь, и как будет реагировать на свайп. Один из распространенных способов задавать движение камеры — это следование за движением мыши. Это может быть сделано либо с прямым контролем, считывая позицию мыши или с относительным движением (изменение позиции). На мобильном устройстве поведение не всегда должно быть идентично поведению в браузере компьютера. Мы провели множество тестов, чтобы определить оптимальный вариант для каждой ситуации.

При работе с небольшими и/или сенсорными экранами, вы неминуемо столкнётесь с тем, что пальцы пользователя и графика контролов управления находятся на том месте, которое вы и хотите показать. Это то, к чему мы привыкли при разработке нативных приложений, но обычно не задумывались раньше при создании веб- приложений. Это настоящий вызов для дизайнеров, в особенности, тех, которые работают над взаимодействием.

Нашим общим впечатлением от этого проекта: WebGL на мобильных работает очень хорошо, особенно на новых устройствах высокого класса. Если говорить о производительности, то можно сказать, что количество полигонов и размер текстур влияют больше на время загрузки и подготовки, а материалы, шейдеры и размер холста WebGL — это участки, требующие основной оптимизации на мобильных устройствах. Однако быстродействие достигается за счет множества составляющих, так что оптимизируйте все, что сможете.

Ориентация на мобильные устройства также означает, что нужно думать о сенсорном взаимодействии, и речь не только о размере в пикселях, а и о физическом размере экрана. В некоторых случаях нам приходилось перемещать 3D-камеру ближе, чтобы на самом деле видеть, что происходит.

Сайт запущен, и это было фантастическое путешествие. Надеюсь, вам понравится!

Хотите попробовать? Совершите собственное путешествие в Средиземье.