![](https://habrastorage.org/getpro/habr/post_images/d35/b77/b2c/d35b77b2c69087fc79f55bd429f4b059.jpg)
Формат 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.
— две полоски 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 |
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.
Функция 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);
Представленный полный перебор ничем не быстрее наилучших режимов сжатия аналогичных утилит, зато это наш полный перебор, который будет сильно ускорен далее.
![](https://habrastorage.org/files/b57/518/187/b5751818711a44cc89efdf522dee29ac.png)
Незначащие пиксели мы можем не учитывать, поэтому отфильтруем их в самом начале, в коде это вызовы FilterPixelsColor. С другой стороны, не всякий прозрачный пиксель является незначащим, вспомним хотя бы защитную рамочку в 1-2 пикселя и эффект отбеливания границ.
Поэтому сделаем трафарет, в котором ноль будет означать незначащий пиксель, а положительная величина покажет значимый пиксель. Трафарет создается на основе канала A применением обводки, чаще размера 1 или 2 пикселя, в коде это функция OutlineAlpha.
Как показала практика, при использовании трафарета улучшаются сжатые границы объектов, а невидимые блоки быстро принимают хорошо пакуемый zip черный цвет. Именно идея трафарета дает заметный выигрыш по качеству в сравнении с раздельным сжатием RGB и A, в том числе перечисленными утилитами.
![](https://habrastorage.org/files/441/ee4/efc/441ee4efc350433c900e874a4492e07f.png)
Таким образом, сжатие 2xETC1 можно представить следующими шагами, реализованными в функции EtcMainWithArgs:
1) сжимаем канал A в формат ETC1;
2) распаковываем сжатый канал A обратно;
3) делаем обводку видимого, где A > 0, получая трафарет;
4) сжимаем каналы RGB в формат ETC1 с учетом трафарета.
Идеи ускорения качественного сжатия в формат ETC1
Чтобы утилита нашла свое применение, помимо качества результата важно и время работы. Рассматриваемый переборный алгоритм сжатия блока достоин быстрой начальной эвристической оценки и полезных отсечений в ходе работы, в том числе на основе жадных алгоритмов.
В данном случае упаковщик пытается прочитать выходной файл, распаковать его и рассчитать имеющуюся ошибку, это будет начальным решением. Если же файла нет, то берется начальное решение из нулей. В коде это LoadEtc1, CompressBlockColor, MeasureHalfColor.
Последующие шаги должны пытаться улучшить имеющееся решение алгоритмами по возрастанию сложности. Поэтому сначала вызываются быстрые CompressBlockColor44, лишь затем медленные CompressBlockColor53. Такая цепочечная конструкция в перспективе позволит интегрировать сжатие в формат ETC2.
Решения в разрезе цветовых компонент представлены структурами 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, что существенно ускоряет сортировку и дальнейший перебор.
Этим занимаются функции Walk и Bottom.
В структуре AdjustStateColor поля ErrorsG, ErrorsGR и поле ErrorsGRB очищаемое LazyGR дают существенный прирост производительности.
Команды _mm_adds_epu8 и _mm_subs_epu8 удобны для расчета четырехцветной палитры из базового цвета и отклонений.
В функциях ComputeErrorGRB и ComputeErrorGR сначала применяются частично развернутые циклы, оптимизированные командой _mm_madd_epi16, так как в большинстве случаев достаточно ее разрядности. В случае же больших погрешностей работает второй цикл на «медленных» командах _mm_mullo_epi32.
Функция ComputeLevel рассчитывает ошибку сразу для четырех значений базового цвета.
Достигнутые результаты
Изложенные подходы позволяют уменьшить требования к оперативной памяти в 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: различия не найдены
Надеемся, что утилита поможет качественно и быстро сжимать мобильную графику.
Комментарии (14)
Temtaime
20.09.2016 17:16DXT не в почёте? Или на мобильных устройствах плохо с его поддержкой?
Andrew2016
21.09.2016 10:39Сравнивали утилитой dssim атласы сжатые в DXT5 и 2xETC1. Результаты у 2xETC1 были лучше. Не было заметно характерного для DXT5 шума.
OneHalf3544
21.09.2016 00:33+2Для своих игр использую утилиту TexurePacker уже пару лет, она поддерживает как сжатие для android так и для iOS (разные форматы ETC1_ALPHA, PVRTC4). Игра в изометрии, сжатие реально помогает сэкономить размер атласов.
Andrew2016
21.09.2016 10:41Ряд идей может быт успешно применен и на других форматах. Например, сжатие по трафарету, полировка по SSIM, современная весовая функция.
Fyfff
21.09.2016 11:19Текстуры лесной зоны с заглавной картинки, очень похожи на приглаженные текстуры из Warcraft 3 (вдохновлялись, ага)
beststream
27.09.2016 12:46Это можно как то с Unity подружить?
Andrew2016
27.09.2016 12:54+1Код открытый, возможно всё. Утилита понимает командную строку. Стоит доработать функции-заглушки LoadEtc1 и SaveEtc1 под нужный формат, например KTX/PVR.
Если есть конкретный пример командной строки и известен формат могу попробовать доработать.beststream
27.09.2016 13:13Я больше менеджер чем разработчик, так что технических деталей не подскажу. Если бы вы оформили это как плагин для Unity, даже платный, был бы спрос, я бы купил точно
Andrew2016
27.09.2016 13:34Решение для ETC1 получилось хорошее, поэкспериментирую с Unity…
А так витают мысли скрестить ETC1 от EtcCompress и ETC2 от etc2comp, был бы мощный инструмент.beststream
27.09.2016 15:40Еще вам задачка на миллион, решите — с руками оторвут. Там на iOS только pvrtc и он просто ужасен для спрайтов с прозрачностью. Адекватного решения я нигде не могу найти (( Советуют так же прозрачность хранить в отдельной текстуре, но как это подружить с Unity Sprite Packer не понятно. Знакомый, который собственный движок пилит, хранит в webp, но Unity такое тоже не умеет
Andrew2016
27.09.2016 16:00За последний год с PVRTC4BPP провел много экспериментов и тоже наблюдал проблемы со сжатием альфы. На некоторых проектах применяем сжатие аналогично 2xETC1 — когда RGB(PVRTC4) + A(PVRTC4), но это по метрикам слабее, чем 2xETC1. Пока не определились будет ли статья про PVRTC4. Тем не менее, успешно используем свою утилиту сжатия в PVRTC4, и качество заметно выше.
sb32
А на картинке реально существующая игра или нет?
fysx
Это GardenScapes