Стиснення мобільного графіки в формат ETC1 і відкрита утиліта

При розвитку 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;
GPU Mali 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 11372 ms, 368827 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 6580 ms, 637432 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: відмінності не знайдено

Сподіваємося, що утиліта допоможе якісно і швидко стискати мобільну графіку.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.