Идея портирования Android-приложения на WebGL
Сейчас WebGL поддерживается практически любым устройством и работает достаточно стабильно и быстро даже на мобильных устройствах, поэтому было весьма интересно попробовать реализовать что-нибудь на этой технологии. У нас уже имеется большой опыт работы с OpenGL ES 2.0 в Android — создали довольно много различных трехмерных живых обоев.
Эти приложения мы создавали без использования готовых сторонних движков (например, Unity) или высокоуровневых фреймворков для работы с OpenGL (таких как libGDX). Ввиду того что код не обременен ограничениями сторонних фреймворков, приложения получаются очень маленькими, и быстрыми, а также мы имеем возможность полностью оптимизировать рендеринг под потребности каждого приложения.
WebGL основан на OpenGL ES 2.0, а поэтому процесс портирования обоев достаточно прост и прямолинеен.
Фреймворк рендеринга
Также как и в оригинальном Java коде, реализованы базовые классы BaseRenderer и BaseShader. Было решено использовать классы ECMAScript 2015 так как эта нотация упрощает читаемость кода, а единственный браузер, которыq не поддерживает JS классы это IE11. BaseRenderer содержит код для создания WebGL контекста, инициализации, отслеживания изменения размера окна, и т. П. Также в нем есть пустые “заглушки” для рендеринга сцены. BaseShader содержит код для компиляции и использования шейдеров. Собсвенно сам рендеринг сцены и загрузка данных для нее реализованы в BitcoinRenderer.
Загрузка готовых данных
Большинство WebGL движков и демок загружают данные из файлов в JSON, OBJ, или других форматах. С одной стороны, это удобно — достаточно просто экспортировать модели из Blender или 3ds Max и использовать их в сцене. Однако с другой стороны, этот подход требует дополнительной обработки исходных данных на клиенте для создания буферов с данными, готовыми для использования видеокартой. Также данные в этих форматах часто содержат много избыточной информации, которая хотя и не используется но все равно занимает большую часть исходного файла, что существенно увеличивает объем передаваемых данных. Вместе эти два недостатка приводят к тому что зачастую даже простенькие WebGL демки грузятся и запускаются довольно долго.
В Java версии нашего фреймворка мы используем двоичные данные, готовые к непосредственной загрузке в OpenGL буферы, и в JS версии мы применяем тот же подход. XMLHttpRequest Level 2 поддерживает работу с двоичными данными в JavaScript. Для упрощения работы с XHR2 создан простой класс BinaryDataLoader.
Класс FullModel обеспечивает работу с мешами. Метод load() загружает два буфера для модели — с индексами и данными (координаты вертексов, UV-координаты и т. п.). Эти буферы содержат двоичные данные, готовые для использования видеокартой. Также класс имеет метод bindBuffers(), который собственно привязывает буферы и его надо вызывать непосредственно перед glDrawElements().
Сжатые текстуры в формате ETC1
Для экономии видеопамяти в живых обоях мы используем различные сжатые текстуры. Наш Java фреймворк поддерживает форматы ETC1, ETC2, PVRTC и ASTC и использует наиболее подходящие текстуры исходя из возможностей конкретного устройства. В WebGL реализованы только ETC1 и несжатые RGB текстуры.
В OpenGL ES 2.0 ETC1 является обязательной частью стандарта и поддерживается на всех без исключения устройствах. Однако, в WebGL поддержка ETC1 сжатия не обязательна, и необходимо проверять наличие расширения WEBGL_compressed_texture_etc1. Все десктопные браузеры кроме IE11 и Edge поддерживают это расширение. Для браузеров Microsoft приходится использовать несжатые текстуры.
Проверить какие форматы текстур поддерживаются браузером можно с помощью этой удобной странички.
Благодаря использованию ETC1 текстур мы используем намного меньше памяти и также удалось ускорить процесс загрузки текстур. Ведь несжатые текстуры перед тем как попасть в видеопамять, должны быть сперва декодированы из исходного формата (PNG, JPEG, WebP, GIF, и т. п.) в битмэп (RGBA или RGB, с альфа-каналои или без него) и только потом переданы драйверу для загрузки в видеокарту. ETC1 текстуры не требуют никакой предварительной обработки — они уже готовы для непосредственного использования видеокартой и поэтому загружаются намного быстрее.
Если говорить об экономии памяти, то к примеру несжатая RGB текстура размером 512х512 пикселей занимает 768 кб, в то время как такая же ETC1 текстура занимает всего 128 кб. Однако, ETC1 не идеален и вызывает некоторые артефакты сжатия. Эти артефакты почти не заметны на диффузных картах и картах освещения, однако весьма заметны на картах нормалей (искажения в виде блоков 4х4 пикселя) и картах отражения (неточная цветопередача). Так что мы используем как сжатые, так и несжатые текстуры в зависимости от требований к качеству.
В Android загрузка ETC1 текстур очень проста — есть стандартная утилита ETC1Util, которая выполняет всю работу по загрузке текстуры из файла в формате PKM. Ввиду того, что WebGL не предоставляет никаких средств для загрузки сжатых текстур из известных форматов, пришлось создать свой загрузчик ETC1 текстур из файлов в формате PKM. PKM это весьма простой формат, он состоит из заголовка в 16 байт, за которым следуют двоичные данные, готовые для загрузки в видеокарту. Больше информации о заголовке можно найти здесь и здесь. При написании кода получения размеров тектуры столкнулись с определенным ограничением JavaScript. Эти значения хранятся в виде 16-битных big-endian целых чисел. Однако использовать Int16Array для получения этих чисел не получится, так как JavaScript не предоставляет способа задавать порядок байтов буфера, и пришлось читать байты используя Uint8Array и вычислять 16-битные значения вручную из полученных пар младших и старших байт.
Шейдеры
В приложении используется всего два шейдера: один для поверхности стола, а другой для модели монет.
Шейдер для стола — это простая реализация lightmap-ов. Он использует две текстуры: одна карта цвета (diffuse) для задания текстуры дерева, а вторая — карта освещенности (lightmap). Результат работы шейдера — это просто перемножение цветов из этих текстур.
Шейдер монет более сложный, он содержит в себе следующие функции:
- Сферичаская карта отражений (т.н. sphere mapping или matcap)
- Карта освещения (lightmap) с усилением цвета
- Карта нормалей (normal map)
Сферическая карта отражений хранит информацию в картинке, которая выглядит как снимок хромированного шара:
По сравнению с другими техниками отражения, сферическая карта имеет следующие преимущества:
+ Простой код шейдера и высокая производительность
+ Использование только одной текстуры
+ Текстура с отражением простая в обработке в графических редакторах, например в Photoshop
— Плохо работает на ровных поверхностях
— Часть текстурного пространства расходуется впустую (углы не используются)
Наибольшее преимущество сферической карты — это простота реализации. Когда у вас уже есть нормаль рассчитанная в пространстве экрана, вам достаточно взять ее (x;y) компоненту для чтения текстуры. Выдержка из кода шейдера:
vec4 sphereColor = texture2D(sphereMap, vec2(vNormal2.x, vNormal2.y));
Отсюда же следует и наибольший недостаток данной техники: так как в расчетах участвует только экранная нормаль, то большие ровные полигоны будут заполнены одним и тем же цветом (так как будут иметь одинаковую нормаль по всей поверхности). А модель монеты как раз и состоит в основном из больших плоских поверхностей. Для борьбы с этим недостатком мы добавили карту нормалей и сделали ее как можно более разнообразной: добавили шума, надписей, цифр и т.д. Этот прием лишает модель ровных поверхностей и сферические отражения работают идеально.
При освещении монет был использован дополнительный трюк. Для затенения монет использована карта освещения (lightmap). Однако, простое умножение цвета на карту освещения дает хоть и более-менее корректный, но скучный результат: затемненные места становятся просто темнее. В дополнение к этому, в темных местах мы умножаем цвет на самого себя, используя функцию pow(). Степень тем выше, чем темнее карта освещенности. Это воспроизводит эффект того как свет попадает в “ловушку” в замкнутом пространстве и усиливает свой цвет ввиду многократного отражения от металлических поверхностей. В результате получаем более реалистичную металлическую поверхность:
Результат
Готовую демку можно просмотреть на этой странице. Все исходники доступны на GitHub и вы можете использовать их в своих проектах на условиях лицензии MIT.