Несколько дней назад мы выпустили новое приложение на природную тематику — Исландия 3D Живые Обои. У него также есть интерактивная демонстрация WebGL, которую вы можете найти здесь.

Ландшафт основан на этой красивой и детализированной 3D-модели Сергея Куйдина. Интересно, что это не настоящий пейзаж какой-то части Исландии. Несмотря на то, что он выглядит как настоящий, на самом деле он создан в World Machine. Проанализировав модель в Sketchfab, мы решили создать с ней живые обои, добавив динамическое время суток. Вам стоит ознакомиться с работами Сергея, у него также есть качественные модели и 3D-сканы.

Композиция сцены

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

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

Сцена визуализируется за 35 вызовов отрисовки. Порядок рендеринга тщательно выбирается для эффективного использования отбраковки z-буфера. Ближайшие объекты рисуются первыми, самые дальние - последними. После этого рендерим прозрачные объекты:

Все фактические вызовы отрисовки выполняются в методе drawSceneObjects() файла MountainsRenderer.ts. Давайте проанализируем, как они отображаются.

Птицы рендерятся первыми, потому что они могут перекрывать как землю, так и небо. Они рендерятся за 12 вызовов отрисовки.

Далее мы визуализируем ландшафт. Исходная высокополигональная модель упрощается в Blender с помощью модификатора Decimate до ~30 000 треугольников, что дает достаточно детализированную геометрию.

И, конечно же, для создания обширного, огромного горного ландшафта путем повторного использования одной модели местности мы используем ту же технику юбки местности, что и в обоях Дюны (описанную в нашей предыдущей статье здесь, а оригинальная реализация — в Halo Wars). Основная идея этого метода состоит в том, чтобы нарисовать один и тот же тайл ландшафта, зеркально отраженный на каждом краю основного ландшафта. Однако у Дюны живые обои был в этом один недостаток. На зеркальных тайлах тени от предварительно обработанных карт освещения были на неправильных склонах — освещенных солнцем. Из-за общей простоты рельефа дюн и низкого размещения камеры он был скрыт и практически незаметен. Я должен отдать должное u/icestep из Reddit, который нашел это и предложил исправить для создания 4 разных карт освещения для 4 возможных ориентаций тайлов. Но из-за того, что горы имеют глубокие и резкие тени, этот дешевый трюк становится хорошо видимым практически из любого места сцены, поэтому нам пришлось реализовать это исправление. К счастью, при грамотном размещении солнца (вдоль одной из осей) нам нужно отрендерить всего 2 карты освещения — для солнечного света в правильном и перевернутом направлении. В то время как реальная плитка все еще зеркальна (камеры избегают определенных ракурсов, где швы слишком очевидны), правильное освещение несколько скрывает этот дешевый трюк с геометрией от человеческого глаза.

Здесь вы можете видеть, что с правильными картами освещения тени появляются на правильной стороне как перевернутого, так и обычного тайла:

После ландшафта базовым DiffuseShader рисуем объект полусферы неба, а затем рисуем 11 спрайтов облаков. Затем рисуем солнечный спрайт. Эти прозрачные объекты визуализируются без записи в буфер глубины. Облака и солнце имеют урезанную геометрию для меньшего перерисовки. Вы можете прочитать об этой оптимизированной технике спрайтов здесь. Мы решили не использовать мягкие частицы для облаков, потому что размер сцены позволял нам размещать их так, чтобы они не пересекались с другими геометрическими объектами, но при этом частично перекрывали некоторые вершины. Отказ от использования мягких частиц выгоден для производительности, поскольку они требуют дополнительного прохода рендеринга для рендеринга глубины сцены.

Разбивка шейдеров ландшафта

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

Terrain Shader применяет к базовому диффузному цвету следующие эффекты:

  • Отражение воды
  • Запеченная карта освещения
  • Туман

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

Таким образом, в дополнение к диффузной текстуре и двум картам освещения (для обычного и перевернутого тайлов) для этого требуется отдельный зеркальный канал для воды. И эти текстуры действительно большие — 4096x4096 пикселей, так что данных довольно много. Для оптимального хранения этой информации мы используем всего две большие текстуры и одну маленькую вспомогательную. Первая текстура обязательно является диффузной картой. Второй — это комбинированная карта освещения, которая содержит две карты освещения для обычного и перевернутого тайлов в красном и зеленом каналах. Синий канал используется для хранения карты зеркального отражения воды. Но подождите, скажете вы, в сценах восхода и заката ясно видно, что лайтмапы цветные! Как данные RGB могут храниться в одном канале? Вот почему мы используем эту вспомогательную текстуру. Это небольшая цветовая шкала — градиент 256x1 для раскрашивания карты освещения в оттенках серого.

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

Давайте посмотрим на исходный код шейдера. Он находится в файле TerrainWaterShader.ts. В самом конце кода фрагментного шейдера вы можете раскомментировать одну из 6 строк, чтобы визуализировать промежуточные проходы, показанные на GIF выше. Вы можете заметить, что шейдер не использует нормали ни от какого атрибута, а вместо этого при расчете зеркального отражения мы используем постоянную нормаль. Это еще одна оптимизация для уменьшения размера геометрии — у геометрии действительно нет нормалей, потому что вода находится в почти идеально плоской части ландшафта, а точную нормаль вершин можно заменить постоянной восходящей нормалью.

Для рельефа юбки мы используем упрощенную версию шейдера без отражения от воды — TerrainShader.ts.

В MountainsRenderer в методе initShaders() видно, что мы создаем пару каждого шейдера ландшафта — с водой и упрощенный, как обычный, так и перевернутый.

Точность шейдера

Вы можете заметить, что фрагментный шейдер ландшафта для юбки имеет уменьшенную точность с плавающей запятой. Для основного ландшафта нам нужна точность highp для правильного рендеринга воды, а поскольку версия с юбкой не имеет этих отражений, mediumpдостаточно.

На первый взгляд это может показаться незначительной оптимизацией, однако на самом деле это очень важная оптимизация, поскольку она работает заметно быстрее, то есть снижается нагрузка на GPU. Даже при тестировании на не самом современном телефоне Pixel 3 обе точности дают стабильные 60 кадров в секунду. Однако снижение нагрузки на графический процессор означает, что у него больше свободной мощности для более плавного отображения пользовательского интерфейса и снижения общего энергопотребления, что очень важно для живых обоев.

В целом инструкции highp на современных мобильных графических процессорах выполняются в два раза медленнее, чем mediump или lowp. Очевидно, что шейдер должен выполнять множество других нематематических инструкций, так как же влияет снижение точности? Хотя это значение отличается для разных графических процессоров, мы можем использовать некоторые инструменты для его измерения. Например, автономный компилятор шейдеров PowerVR можно использовать для анализа этого конкретного оборудования. А для графических процессоров PowerVR Series6 мы получаем 18 циклов для highp и 13 циклов для mediump шейдеров. Это 28% прироста производительности для шейдера, который используется для отрисовки достаточно значительной части фрагментов сцены.

Ориентация на разные версии OpenGL ES для Android

Это наши первые живые обои для Android, которые вообще не поддерживают OpenGL ES 2.0. Только 10% Android-устройств ограничены OpenGL ES 2.0, и это должны быть действительно старые, устаревшие устройства. Поэтому мы поддерживаем только OpenGL ES 3.0 и выше — приложение имеет два набора ресурсов для ES 3.0 и ES 3.2. Для устройств с ES 3.0 мы используем текстуры ETC2, которые обеспечивают приемлемое качество изображения при том же размере, что и ETC1. Однако сжатия по-прежнему недостаточно, чтобы текстуры оставались маленькими, поэтому нам пришлось уменьшить их разрешение для ES 3.0. На устройствах с ES 3.2 мы используем более продвинутое сжатие ASTC для текстур с лучшим качеством и лучшим сжатием. Это позволяет нам использовать текстуры высокого разрешения на современных устройствах. Вот несколько примеров размеров текстур:

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

Обновление: примерно через неделю после выпуска приложения я сравнил диффузную текстуру, сжатую блоками ASTC 8x8 и 10x10. Очевидно, что более высокое сжатие определенно вносит некоторые искажения, вызванные экстремальным сжатием. Однако на таких нечетких изображениях, как аэрофотоснимки местности, очень трудно отличить артефакты сжатия от фактических случайных особенностей местности. Вы можете увидеть очень похожие результаты при сжатии различных изображений в JPEG среднего качества, который также использует фиксированные блоки 8x8 пикселей для сжатия изображений. Изображения с тонкими четкими линиями (например, текст и диаграммы) будут иметь пресловутые блочные артефакты JPEG, но вы не заметите разницы между сжатыми и оригинальными фотографиями природы. Поэтому я обновил приложение, чтобы использовать еще более сжатую диффузную текстуру.

Для геометрии и вершинные, и текстурные координаты используют половинные числа с плавающей запятой. Этой точности достаточно для координат вершин, а поскольку мы используем текстуры значительно больше 256, мы не можем использовать байты для координат текстуры — 8-битная точность для диффузной текстуры 4096x4096 будет равна 16 текселям.

Конечный результат

Полный исходный код доступен на GitHub здесь, а демо-страница — здесь. Нажмите на сцену, чтобы изменить время суток (загрузка текстур может занять пару секунд), а нажав Enter, вы можете войти в режим свободной камеры. Нажмите и удерживайте правую кнопку мыши, чтобы посмотреть, и используйте WASD для перемещения.

И, конечно же, вы можете получить приложение для Android с живыми обоями из Google Play здесь, это бесплатно.