Наши приложения для Android представляют собой живые обои, поэтому очень важно, чтобы они были легкими. Чтобы добиться максимальной производительности, минимального объема памяти и энергопотребления, мы постоянно улучшаем наши приложения, уменьшая размер ресурсов и используя различные способы сжатия, поддерживаемые оборудованием.

Последнее обновление 3D Будда Live Wallpaper представило более компактное хранение 3D-объектов для экономии памяти и повышения производительности. Мы обновили его аналог WebGL demo таким же образом, и в этой статье мы опишем процесс этой оптимизации.

Компактные типы данных в OpenGL ES / WebGL

Раньше в наших приложениях мы использовали только числа с плавающей запятой для хранения всей информации по вершинам - положение, нормаль, цвета и т. Д. Это стандартные 32-битные значения с плавающей запятой IEEE-754, которые достаточно универсальны, чтобы хранить любой тип информации, начиная от вершины координаты к цветам.

Однако не все типы данных требуют точности 32-битных чисел с плавающей запятой. А в OpenGL ES 2.0 / WebGL есть другие, менее точные, но более компактные типы данных, которые можно использовать вместо 32-битных чисел с плавающей запятой.

Во-первых, OpenGL поддерживает 16- и 8-битные целые числа со знаком и без знака. Так как же целочисленное значение может заменить число с плавающей запятой? Есть два варианта: использовать целочисленные значения в шейдере как есть и преобразовать их в числа с плавающей запятой или нормализовать их. Нормализация означает, что драйвер / графический процессор выполняет преобразование из целого числа в значение с плавающей запятой, а вершинный шейдер получает готовое к использованию значение с плавающей запятой. Нормализация преобразует целочисленные значения в диапазон [0, 1] или [-1, 1], в зависимости от того, являются ли они целыми числами без знака или со знаком. Точность нормализованного значения определяется диапазоном исходного целочисленного значения - чем больше битов в исходном целочисленном, тем выше точность.

Так, например, значение 128 байта без знака будет нормализовано до 0,5, а короткое со знаком -16383 будет нормализовано до -0,5. Вы можете прочитать больше о преобразовании нормализованных целых чисел на этой странице OpenGL wiki.

Чтобы использовать нормализованные целые числа, вы должны установить нормализованный параметр glVertexAttribPointer на true, и шейдер будет получать нормализованные числа с плавающей запятой.

Типичные значения, хранящиеся в байтах без знака, являются цветами, потому что нет необходимости иметь точность более 1/256 для компонентов цвета - 3 или 4 байта без знака идеально подходят для хранения цветов RGB или RGBA соответственно. Два шорта могут использоваться для хранения UV-координат типичной 3D-модели, предполагая, что они находятся в диапазоне [0, 1], а повторяющиеся текстуры не используются на сетках. Они обеспечивают достаточную точность для этих нужд - например, unsigned short обеспечит точность субтекселя даже для текстуры с размером 4096, поскольку ее точность составляет 1/65536.

Более новый OpenGL ES 3.0 (и основанный на нем WebGL 2) представляет новые компактные типы данных:

  • Half-float для данных вершин - это 16-битные числа с плавающей запятой IEEE-754. Они используют 2 байта, как и GL_SHORT, но их диапазон и точность не так ограничены, как нормализованные значения.
  • 4-байтовый упакованный формат INT_2_10_10_10_REV, который содержит 4 целочисленных значения, которые могут быть нормализованы до числа с плавающей запятой. Три из этих целых чисел имеют точность 10 бит, а одно - только 2 бита. Этот формат описан в разделе 2.9.2 Спецификации OpenGL ES 3.0.

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

Размер шага, смещения и отступы

В нашем конвейере мы используем двухэтапный подход - сначала генерируем, а затем сжимаем данные вершин. Сначала исходные файлы OBJ и FBX преобразуются в готовые к использованию массивы графического процессора - индексы вершин и данные атрибутов чередующихся вершин (шаги). Следующим шагом будет преобразование значений с плавающей запятой в более компактные типы данных. Это делается с помощью утилиты командной строки, написанной на JavaScript, работающей на Node.js. Вы можете получить его с GitHub.

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

  • Согласно официальной документации Apple iOS OpenGL ES, размер шага должен быть кратным 4 байтам для достижения наилучшей производительности и уменьшения нагрузки на драйверы. По всей видимости, это связано с архитектурой чипов Apple, а в их основе лежат графические процессоры PowerVR компании Imagination Technologies.
  • В официальном документе Рекомендации по производительности PowerVR расплывчато сказано, что для некоторого оборудования может быть полезен прогресс, согласованный с 16-байтовыми границами.
  • ARM в своем Руководстве по оптимизации приложений рекомендует выравнивать данные по 8 байтам для оптимальной производительности на графических процессорах Mali.
  • Официальных рекомендаций по выравниванию данных вершин для графических процессоров Qualcomm Adreno нет.

Наш инструмент выравнивает данные на 4 байта, чтобы сэкономить больше памяти (в наших приложениях мы не используем модели с чрезмерным количеством вершин, поэтому доступ к данным вершин не является узким местом).

Затем, когда вы используете смешанные типы данных в данных чередующихся вершин, необходимо, чтобы данные каждого атрибута были правильно выровнены в пределах шага. Это указано в разделе 2.10.2 Спецификации OpenGL ES 3.0 - смещения атрибутов должны быть кратны соответствующим размер типа данных. Если вы не выполняете это требование, то поведение OpenGL ES на Android и WebGL будет отличаться. OpenGL ES не выдает никаких ошибок, и результат зависит от оборудования (и, возможно, драйверов) - графические процессоры Adreno, похоже, обрабатывают такие искаженные данные, не генерируя никаких ошибок, в то время как графические процессоры Mali ничего не отображают. Реализации WebGL, с другой стороны, обнаруживают несовпадающие чередующиеся атрибуты, и вы найдете либо ошибку, либо предупреждение об этом в консоли.

Chrome выдает следующую ошибку:

GL_INVALID_OPERATION: смещение должно быть кратным переданному типу данных.

Firefox выдает это предупреждение:

Предупреждение WebGL: vertexAttribI? Указатель: `stride` и` byteOffset` должны удовлетворять требованию выравнивания `type`.

Наш инструмент может добавлять пустые байты заполнения для правильного выравнивания любых типов данных.

Как упоминалось ранее, OpenGL ES 3.0 и WebGL 2 поддерживают специальные упакованные структуры INT_2_10_10_10_REV, которые содержат три 10-битных и одно 2-битное целое число со знаком. Этот тип данных обеспечивает немного лучшую точность, чем байт, при этом только на 1 байт больше, чем на 3 отдельных байта. Наш инструмент может преобразовать 3 числа с плавающей запятой в этот тип упакованных данных. Обратите внимание, что даже если вы используете только 3 компонента из этой структуры, вы должны указать размер 4 для glVertexAttribPointer при ее использовании (в шейдере вы все равно можете использовать vec3 униформы, компоненты w будут проигнорированы).

Вот три разных примера сжатых и выровненных шагов. Исходный размер каждого шага, состоящего из 32-битных чисел с плавающей запятой, составляет 40 байтов (10 чисел с плавающей запятой) - 3 числа с плавающей запятой для координат вершин, 4 для двух наборов UV-координат (диффузные и световые карты) и 3 для нормалей. Вот примеры одних и тех же данных, сжатых тремя разными способами до 16 байтов (на 60% меньше, чем исходные) на вершину без визуально ощутимой потери качества.

Исходный шаг:

Различные варианты сжатых шагов:

Цветовые коды для типов данных:

В первом случае нормали не требуют выравнивания, потому что они используют нормализованный тип GL_UNSIGNED_BYTE. Во втором случае для большей точности используются все нормальные значения, упакованные в единую структуру INT_2_10_10_10_REV. Обратите внимание, что для этого необходимо, чтобы он был выровнен по нескольким из 4 границ. Для этого выравнивания добавляются 2 неиспользуемых байта заполнения, сдвигая нормали к смещению 12. Полезный размер данных в первом случае составляет 13 байтов с 3 байтами заполнения для выравнивания общего размера шага, а во втором случае используется 14 байтов с 2 неиспользованными байтами для внутреннего выравнивания. . Оба они умещаются в 16 байтах (кратное 4), чтобы графические процессоры могли более эффективно получать целые шаги.

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

Разница в размере, производительности и качестве

Мы сжали данные вершин для модели статуи Будды, используя половинные поплавки для позиций, байты без знака для диффузных координат и UV-координат карты освещения и байты со знаком для нормалей. Это привело к уменьшению размера несжатых (до gzip) данных с 47 кБ до 18 кБ.

Несмотря на то, что мы использовали наименее точную точность для UV-координат, этого достаточно, потому что в этой модели мы не используем текстуры больше 256x256. А для нормалей достаточно нормализованных байтов со знаком. Тестовая визуализация нормалей не показывает визуальных различий между различными типами данных, только перцепционная разница может выявить незначительную разницу между определенными пикселями. Medium не поддерживает анимированный WebP, поэтому вы можете использовать эту ссылку на Google Диск для предварительного просмотра.

Чтобы точно измерить, как оптимизация повлияла на использование памяти, мы использовали Snapdragon Profiler для сбора средних значений для двух метрик данных вершин в реальном времени. На Google Pixel 3 мы имеем следующие результаты:

Это существенное изменение, которое снижает общее потребление ОЗУ приложением, а также снижает общую пропускную способность памяти. Уменьшение нагрузки на графический процессор позволяет более плавно отображать пользовательский интерфейс системы на живых обоях и улучшает использование батареи.

Результат

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