Pull to refresh
0
Playrix
Разработчик мобильных free-to-play-игр

Сжатие мобильной графики в формат ETC1 и открытая утилита

Reading time 9 min
Views 17K
При развитии free-to-play мобильной игры вместе с новыми фичами регулярно добавляется и новая графика. Часть ее включается в дистрибутив, часть скачивается в ходе игры. Для возможности запуска приложения на устройствах с небольшим размером оперативной памяти разработчики применяют аппаратно сжатые текстуры.



Формат ETC1 обязателен к поддержке на всех Android-устройствах с OpenGL ES 2.0 и является хорошей отправной точкой оптимизации потребляемой оперативной памяти. По сравнению с форматами PNG, JPEG, WebP загрузка текстур ETC1 осуществляется без интенсивных расчетов обычным копированием памяти. Также улучшается производительность игры по причине меньших размеров данных текстур пересылаемых из медленной памяти в быструю.

На любом устройстве с OpenGL ES 3.0 возможно использование текстур в формате ETC1, являющимся подмножеством улучшенного формата ETC2.

Использование сжатых текстур в формате ETC1


Формат ETC1 содержит только компоненты цвета RGB, поэтому он подходит для непрозрачных фонов, которые рекомендуется рисовать с отключенным Alpha-blending.

Что делать с прозрачной графикой? Для нее задействуем две текстуры ETC1 (далее — 2xETC1):

— в первой текстуре храним исходный RGB;
— во второй текстуре храним исходную альфу (далее — A), скопировав ее в компоненты RGB.

Тогда в пиксельном шейдере 2xETC1 восстановим цвета таким образом:

uniform sampler2D u_Sampler;
uniform sampler2D u_SamplerAlpha;

varying vec2 v_TexCoords;
varying vec4 v_Color;

void main() {
    vec4 sample = texture2D(u_Sampler, v_TexCoords);
    sample.a = texture2D(u_SamplerAlpha, v_TexCoords).r;
    gl_FragColor = sample * v_Color;
}

Особенности подготовки атласов перед сжатием в формат ETC1


Формат ETC1 использует независимые блоки 4x4 пикселя, поэтому положение помещаемых в атлас элементов желательно выравнивать на 4 пикселя, чтобы исключить попадание разных элементов в общий блок.

Все элементы при помещении в атлас слегка увеличиваются по площади, потому что нуждаются в дополнительной защитной рамочке толщиной 1-2 пикселя. Это связано с дробными координатами отрисовки (при плавном движении спрайтов) и с билинейной фильтрацией текстур. Математическое обоснование причин происходящего заслуживает отдельной статьи.

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

Чем можно качественно сжать в формат ETC1?


Имеется выбор среди бесплатных утилит:

ETC2Comp;
Mali GPU Texture Compression Tool;
PVRTexTool;
rg-etc1.

Для качественного сжатия графики приходится задавать perceptual метрику, учитывающую особенности восприятия, а также выбирать медленные режимы best и slow. Один раз попробовав качественно сжать текстуру 2048x2048 понимаешь, что это долгий процесс… Возможно поэтому многие разработчики ограничиваются быстрыми альтернативами medium и fast. Можно ли сделать лучше?

История создания с нуля собственной утилиты EtcCompress одним из программистов Playrix берет начало в январе 2014 года, когда финальное сжатие графики в формат ETC1 превысило по длительности трехчасовой поход в гости.

Идеи качественного сжатия в формат ETC1


Формат ETC1 является форматом с независимыми блоками. Поэтому мы используем классический подход сжатия отдельных блоков, который хорошо распараллеливается. Конечно, можно пытаться улучшить стыковку блоков, рассматривая наборы блоков, но в таком случае потребуется информация о принадлежности элементам атласа и резко возрастает вычислительная сложность задачи.

Для сравнения результатов сжатия подходит утилита dssim.

Для каждого блока придется перебрать все 4 возможные режима кодирования, чтобы найти наилучший, в коде функция CompressBlockColor:

— две полоски 2x4, каждая имеющая свой базовый 4-битный цвет, в коде вызовы CompressBlockColor44(…, 0);
— две полоски 4x2, каждая имеющая свой базовый 4-битный цвет, в коде вызовы CompressBlockColor44(…, 1);
— две полоски 2x4, первая имеющая базовый 5-битный цвет, вторая отличающаяся базовым цветом от первой в диапазоне 3-бит, в коде вызовы CompressBlockColor53 (…, 2);
— две полоски 4x2, первая имеющая базовый 5-битный цвет, вторая отличающаяся базовым цветом от первой в диапазоне 3-бит, в коде вызовы CompressBlockColor53 (…, 3).
2x4, 444+444 4x2, 444+444 2x4, 555+333 4x2, 555+333

Кстати об ошибке, во многих утилитах используется классический PSNR. Мы тоже используем эту метрику. Выберем весовые коэффициенты из таблицы.

PixelError = 0.715158 * (dstG - srcG)^2 + 0.212656 * (dstR - srcR)^2 + 0.072186 * (dstB - srcB)^2

Перейдем к целочисленным значениям, умножив коэффициенты на 1000 и округлив. Тогда начальная ошибка блока 4x4 составит kUnknownError  = (255^2) * 1000 * 16 + 1, где 255 — максимальная ошибка цветовой компоненты, 1000 – фиксированная сумма весов, 16 — количество пикселей. Такая ошибка укладывается в int32_t. Можно заметить, что целочисленное квадрирование близко по смыслу учету гаммы 2.2.

У PSNR есть слабые места. Например, кодирование заливки цветом c0 выбором из палитры c1 = c0 - d и c2 = c0 + d вносит одинаковую ошибку d^2. Это означает случайный выбор между c1 и c2 влекущий всевозможные шашки.

Для улучшения результата финальный расчет в блоке выполним по SSIM. В коде это делается в функции ComputeTableColor с использованием макросов SSIM_INIT, SSIM_UPDATE, SSIM_CLOSE, SSIM_OTHER, SSIM_FINAL. Идея в том, что для всех решений с наилучшим PSNR (в найденном режиме кодирования) выбирается решение с наибольшим SSIM.

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

Функция GuessColor4 выполняет перебор отклонений и компонент базового цвета:

for (int q = 0; q < 8; q++)
    for (int c0 = 0; c0 < c0_count; c0++) // G, c0_count <= 16
        for (int c1 = 0; c1 < c1_count; c1++) // R, c1_count <= 16
            for (int c2 = 0; c2 < c2_count; c2++) // B, c2_count <= 16
                ComputeErrorGRB(c, q);

В случае зависимых базовых цветов возрастает алгоритмическая сложность из-за двойной вложенности циклов полосок. Функция CompressBlockColor53 выполняет перебор отклонений.

for (int qa = 0; qa < 8; qa++)
    for (int qb = 0; qb < 8; qb++)
        AdjustColors53(qa, qb);

Функция AdjustColors53 выполняет перебор компонент двух базовых цветов:

for (int a0 = 0; a0 < a0_count; a0++) // G, a0_count <= 32
    for (int a1 = 0; a1 < a1_count; a1++) // R, a1_count <= 32
        for (int a2 = 0; a2 < a2_count; a2++) // B, a2_count <= 32
            ComputeErrorGRB(a, qa);

            for (int d0 = Ld0; d0 <= Hd0; d0++) // G, d0_count <= 8
                for (int d1 = Ld1; d1 <= Hd1; d1++) // R, d1_count <= 8
                    for (int d2 = Ld2; d2 <= Hd2; d2++) // B, d2_count <= 8
                        b = a + d;
                        ComputeErrorGRB(b, qb);

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

В случае графики 2xETC1 полностью прозрачные пиксели в общем случае могут иметь произвольный цвет RGB, который будет умножен на нулевую альфу.


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

Поэтому сделаем трафарет, в котором ноль будет означать незначащий пиксель, а положительная величина покажет значимый пиксель. Трафарет создается на основе канала A применением обводки, чаще размера 1 или 2 пикселя, в коде это функция OutlineAlpha.

Как показала практика, при использовании трафарета улучшаются сжатые границы объектов, а невидимые блоки быстро принимают хорошо пакуемый zip черный цвет. Именно идея трафарета дает заметный выигрыш по качеству в сравнении с раздельным сжатием RGB и A, в том числе перечисленными утилитами.
                         

Таким образом, сжатие 2xETC1 можно представить следующими шагами, реализованными в функции EtcMainWithArgs:

1) сжимаем канал A в формат ETC1;
2) распаковываем сжатый канал A обратно;
3) делаем обводку видимого, где A > 0, получая трафарет;
4) сжимаем каналы RGB в формат ETC1 с учетом трафарета.

Идеи ускорения качественного сжатия в формат ETC1


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

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

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

Последующие шаги должны пытаться улучшить имеющееся решение алгоритмами по возрастанию сложности. Поэтому сначала вызываются быстрые CompressBlockColor44, лишь затем медленные CompressBlockColor53. Такая цепочечная конструкция в перспективе позволит интегрировать сжатие в формат ETC2.

Перед началом перебора вложенными циклами есть смысл найти решение в разрезе цветовых компонент. Дело в том, что наилучшее решение не может иметь ошибку меньше, чем суммарная ошибка наилучших решений для каждой из компонент G, R, B. Часто результирующая ошибка будет существенно больше, что характеризует нелинейность и сложность алгоритма ETC1.

Решения в разрезе цветовых компонент представлены структурами GuessStateColor и AdjustStateColor. Для каждого значения из таблицы отклонений g_table рассчитываются ошибки полосок Half и сохраняются в поля node0, node1, node2. Причем в GuessStateColor в индексах [0x00..0x0F] хранятся рассчитанные ошибки для всех возможных базовых цветов g_colors4, а в индексе [0x10] наилучшее решение. Для AdjustStateColor наилучшее решение хранится в индексе [0x20], все возможные базовые цвета берутся из g_colors5.

Расчет ошибки по компонентам цвета осуществляется функциями ComputeLevel, GuessLevels, AdjustLevels на основе таблиц g_errors4, g_errors5, предварительно рассчитанных функцией InitLevelErrors.

Перебор цветовых компонент есть смысл сделать в порядке возрастания вносимой ими ошибки, для этого осуществляется сортировка полей node0, node1, node2 функциями SortNodes10 и SortNodes20.

Для ускорения самой сортировки применяются сортирующие сети, рассчитанные на тематическом сайте.

Перед выполнением сортировки есть смысл отбросить большие ошибки, превышающие найденное решение. При этом заметно уменьшается количество элементов в полях node0, node1, node2, что существенно ускоряет сортировку и дальнейший перебор.

Третий вложенный цикл по цветовым компонентам G, R, B можно попытаться отсечь, найдя наилучшее решение для текущих G, R функцией ComputeErrorGR, которая в 2 раза быстрее функции ComputeErrorGRB. Это, кстати, горячие места в профилировщике.

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

Этим занимаются функции Walk и Bottom.

64 вызова функции AdjustColors53 могут привести к повторным вызовам функций ComputeErrorGR и ComputeErrorGRB с одинаковыми параметрами базового цвета, поэтому будем кэшировать результаты вызовов. В свою очередь, для быстрой инициализации кэша можно использовать ленивые вычисления по третьему цветовому компоненту.

В структуре AdjustStateColor поля ErrorsG, ErrorsGR и поле ErrorsGRB очищаемое LazyGR дают существенный прирост производительности.

После различных алгоритмических улучшений пришло время использовать SIMD, в данном случае опубликовано решение на целочисленном SSE4.1. Данные одного пикселя храним как int32x4_t.

Команды _mm_adds_epu8 и _mm_subs_epu8 удобны для расчета четырехцветной палитры из базового цвета и отклонений.

В функциях ComputeErrorGRB и ComputeErrorGR сначала применяются частично развернутые циклы, оптимизированные командой _mm_madd_epi16, так как в большинстве случаев достаточно ее разрядности. В случае же больших погрешностей работает второй цикл на «медленных» командах _mm_mullo_epi32.

Функция ComputeLevel рассчитывает ошибку сразу для четырех значений базового цвета.

Для сжатия одного канала A можно упростить полученный код сжатия RGB. Будет заметно меньше вложенных циклов и повысится производительность.

Достигнутые результаты


Изложенные подходы позволяют уменьшить требования к оперативной памяти в Android-версиях игр за счет использования сжатых текстур в аппаратном формате ETC1.

В скриптах формирования атласов и самой утилите сжатия уделяется внимание вопросам предотвращения артефактов и повышения качества сжатой графики.

На удивление, вместе с повышением качества сжатой графики удалось ускорить само сжатие! В нашем проекте Gardenscapes сжатие атласов в формат ETC1 на процессоре Intel Core i7 6700 занимает 24 секунды. Это быстрее генерации самих атласов и в несколько раз быстрее предыдущей утилиты сжатия в режиме fast. Предложенное инкрементальное сжатие происходит за 19 секунд.

В заключение приведу пример сжатия текстуры 8192x8192 RGB представленной утилитой EtcCompress под Win64 на процессоре Intel Core i7 6700:

x:\>EtcCompress
Usage: EtcCompress [/retina] src [dst_color] [dst_alpha] [/debug result.png]

x:\>EtcCompress 8192.png 1.etc /debug 1.png
Loaded 8192.png
Image 8192x8192, Texture 8192x8192

Compressed 4194304 blocks, elapsed 10988 ms, 381716 bps
Saved 1.etc
Texture RGB wPSNR = 42.796053, wSSIM_4x2 = 0.97524678

Saved 1.png

x:\>EtcCompress 8192.png 1.etc /debug 2.png
Loaded 8192.png
Image 8192x8192, Texture 8192x8192

Loaded 1.etc
Compressed 4194304 blocks, elapsed 6487 ms, 646570 bps
Saved 1.etc
Texture RGB wPSNR = 42.796053, wSSIM_4x2 = 0.97524678

Saved 2.png

x:\>fc /b 1.png 2.png
Сравнение файлов 1.png и 2.png
FC: различия не найдены

Надеемся, что утилита поможет качественно и быстро сжимать мобильную графику.
Tags:
Hubs:
+25
Comments 15
Comments Comments 15

Articles

Information

Website
plrx.me
Registered
Employees
1,001–5,000 employees
Location
Ирландия