[Перевод] Сжатие текстур в Android: сравнение форматов и примеры кода

Назовите самый лучший формат сжатия текстур. Может это PNG, ETC, PVRTC, S3TC, или ещё какой-нибудь? Вопрос непростой, но очень важный. От ответа зависят качество визуального оформления, скорость работы и размеры Android-приложений. Дело осложняется тем, что универсального «самого лучшего формата» попросту не существует. Всё зависит от потребностей разработчика.

d4cc34e055d04cc998b466609c4aae19.jpg

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

Предварительные сведения о работе с текстурами и о форматах их хранения


Наложение текстур (texture mapping) — это метод «наклеивания» изображения на поверхности фигур или многоугольников. Для того чтобы было понятнее, фигуру можно сравнить с коробкой, а текстуру — с узорной обёрточной бумагой, в которую эту коробку заворачивают для того, чтобы положить в неё что-нибудь хорошее и кому-нибудь подарить. Поэтому в англоязычной литературе наложение текстур называют ещё и «texture wrapping», что можно перевести как «обёртывание текстурами».

5de20cb4ad686aa488c8274fcaec0992.png
Первый танк — это полигональная модель, а второй — та же модель, на которую наложены текстуры.

MIP-карты (Mipmaps) — это оптимизированные группы изображений, которые генерируются для основной текстуры. Обычно их создают для того, чтобы увеличить скорость рендеринга картинки и для сглаживания изображений (anti-aliasing), то есть — для избавления от эффекта «ступенчатых» линий. Каждый уровень карты (его называют «mip», фактически — это одно из растровых изображений, из них состоит набор текстур, входящих в MIP-карту) — это версия исходной текстуры с пониженным разрешением.

Такое изображение используется в случаях, когда текстурированный объект виден с большого расстояния, или когда его размеры уменьшены. Идея использования MIP-карт строится на том факте, что мы попросту не можем различить мелкие детали объекта, который находится далеко от нас или имеет маленькие размеры. Основываясь на этой идее, различные фрагменты карты можно использовать для представления различных частей текстуры, основываясь на размерах объекта. Это увеличивает скорость рендеринга за счёт того, что уменьшенные варианты основной текстуры имеют намного меньше текселей (пикселей текстуры), то есть GPU приходится обрабатывать меньше данных для вывода текстурированной модели. Кроме того, так как MIP-карты обычно подвергаются сглаживанию, серьёзно уменьшается количество заметных артефактов. Здесь мы рассмотрим MIP-карты в форматах PNG, ETC (KTX), ETC2 (KTX), PVRTC, и S3TC.

f89a9803407b96a436e33cef5a0cd947.jpg


MIP-карта

Portable Network Graphics (PNG)


PNG — это растровый формат хранения изображения, особенно заметный тем, что в нём используется алгоритм сжатия графических данных без потерь информации. Он поддерживает цветные индексированные изображения (24 бита RGB или 32 бита RGBA), полноцветные и полутоновые изображения, а так же — альфа-канал.

Преимущества


  • Формат использует сжатие графических данных без потерь, как результат, PNG-изображения весьма качественны.
  • Поддерживает и 8-ми битную и 16-битную прозрачность.


Недостатки


  • Файлы имеют большие размеры. Это увеличивает размер приложений и потребление памяти.
  • Сравнительно высокая потребность в вычислительных ресурсах (что ведет к ухудшению производительности).


Ericsson Texture Compression (ETC)


Ericsson Texture Compression — это формат сжатия текстур, который оперирует блоками пикселей размером 4×4. Изначально Khronos использовал ETC как стандартный формат для Open GL ES 2.0. (эта версия ещё называется ETC1). В результате этот формат доступен практически на всех Android-устройствах. С выходом OpenGL ES 3.0. в качестве нового стандарта использован формат ETC2 — переработанная версия ETC1. Основное различие между этими двумя стандартами заключается в алгоритме, который оперирует пиксельными группами. Улучшения в алгоритме привели к более высокой точности вывода мелких деталей изображений. Как результат, качество изображений улучшилось, а размер файлов — нет.

ETC1 и ETC2 поддерживают сжатие 24-битных RGB-данных, но они не поддерживают сжатие изображений с альфа-каналом. Кроме того, есть два разных формата файлов, относящихся к алгоритму ETC: это KTX и PKM.

KTX — это стандартный формат файла Khronos Group, он предоставляет контейнер, в котором можно хранить множество изображений. Когда MIP-карта создаётся с использованием KTX, генерируется единственный KTX-файл. Формат PKM-файла гораздо проще, такие файлы, в основном, используют для хранения отдельных изображений. Как результат, при использовании PKM в ходе создания MIP-карты получатся несколько PKM-файлов вместо единственного KTX. Поэтому для хранения MIP-карт использовать формат PKM не рекомендуется.

Преимущества


  • Размер ETC-файлов, заметно меньше чем размер PNG-файлов.
  • Формат поддерживает аппаратное ускорение практически на всех Android-устройствах.


Недостатки


  • Качество не так высоко, как у PNG (ETC — это формат сжатия изображений с потерями информации).
  • Нет поддержки прозрачности.


Для сжатия изображений в ETC можно использовать Mali GPU Texture Compression Tool и ETC-Pack Tool.

PowerVR Texture Compression (PVRTC)


PowerVR Texture Compression — это формат компрессии графических данных с потерями, с фиксированным уровнем сжатия, который используется, преимущественно, в устройствах Imagination Technology PowerVR MBX, SGX и Rogue. Он применяется в качестве стандартного метода сжатия изображений в iPhone, iPod, iPad.

В отличие от ETC и S3TC, алгоритм PVRTC не работает с фиксированными блоками пикселей. В нём используется билинейное увеличение и смешивание с низкой точностью двух изображений низкого разрешения. В дополнение к уникальному процессу сжатия, PVRTC поддерживает формат RGBA (с прозрачностью) и для варианта 2-bpp (2 бита на пиксель), и для варианта 4-bpp (4 бита на пиксель).

Преимущества


  • Поддержка альфа-каналов.
  • Поддержка RGBA для варианта 2-bpp (2 бита на пиксель) и для варианта 4-bpp (4 бита на пиксель).
  • Размер файлов намного меньше, чем у PNG.
  • Поддержка аппаратного ускорения на GPU PoverVR.


Недостатки


  • Качество не так высоко, как при использовании PNG (PVRTC — это формат сжатия изображений с потерями).
  • PVRTC поддерживается только на аппаратном обеспечении PoverVR.
  • Обеспечивается поддержка квадратных POT-текстур, то есть текстур, ширина и высота которых являются степенью числа 2, хотя в некоторых случаях имеется поддержка прямоугольных текстур.


  • Сжатие текстур в этот формат может быть медленным.


Для сжатия можно использовать PVRTexTool.

S3 Texture Compression (S3TC) или DirectX Texture Compression (DXTC)


S3 Texture Compression — это формат сжатия графических данных с потерями, с фиксированным уровнем сжатия. Его особенности делают этот формат идеальным для сжатия текстур, используемых в 3D-приложениях, рассчитанных на использование графического ускорителя. Интеграция S3TC с Microsoft DirectX 6.0 и OpenGL 1.3 способствовала его широкому распространению. Существует как минимум 5 различных вариантов формата S3TC (от DXT1 до DXT5). Приложение-пример поддерживает чаще всего используемые варианты (DXT1, DXT3 и DXT5).

DXT1 обеспечивает наиболее сильное сжатие. Каждый входной 16-пиксельный блок конвертируется в 64-битный блок, состоящий из двух 16-битных RGB 5:6:5 цветовых значений и 2-х битной таблицы подстановок размером 4×4. Поддержка прозрачности ограничена одним цветом (1-битная прозрачность).

DXT3 конвертирует каждый блок из 16 пикселей в 128 бит, 64 бита приходятся на данные альфа-канала, 64 — на цветовую информацию. DXT3 очень хорошо подходит для изображений или текстур с резкими переходами между прозрачными и непрозрачными областями. Однако если градаций прозрачности нет, а прозрачные участки в изображении имеются, стоит рассмотреть использование DXT1.

DXT5, как и DXT3, конвертирует каждый блок из 16 пикселей в 128 бит, 64 бита приходятся на данные альфа-канала, 64 — на цветовую информацию. Однако, в отличие от DXT3, DXT5 подходит для изображений или текстур с плавными переходами между прозрачными и непрозрачными областями.

Преимущества


  • Размер файла значительно меньше аналогичного PNG-файла.
  • Достойное качество, низкий процент артефактов в виде полосок, связанных с наложением цветов.
  • Хорошая скорость кодирования и декодирования.
  • Аппаратное ускорение на множестве GPU. На настольных системах поддерживается практически всеми решениями, постепенно распространяется и на платформе Android.


Недостатки


  • Качество ниже, чем у PNG (S3TC — это формат сжатия изображений с потерями информации).
  • Поддерживается не на всех Android-устройствах.


Для работы с этим форматом можно пользоваться DirectX Texture Tool из DirectX (включен в DX SDK)

Доступ к данным текстур


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

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

Обратите внимание


Заголовок PVRTC упакован с учётом наличия члена данных 64-битного пиксельного формата (mPixelFormat в примере). В коде, скомпилированном для ARM, проводится выравнивание заголовка с добавлением к нему 4 дополнительных байтов, в итоге он, из исходного 52-байтового, становится 56-байтовым. Это приводит к тому, что при выводе на ARM-устройствах изображение искажается. В коде, скомпилированном для процессоров от Intel, подобного не происходит. Упаковка заголовка решает проблему с выравниванием на ARM-устройствах, в итоге текстура отображается правильно как на ARM-устройствах, так и на Intel-устройствах.

fa733adde9419c1b77f24263a185a731.png
Вот как выглядит искажение изображения на ARM-устройстве, вызванное выравниванием заголовка

О приложении-примере


Пример Android Texture Compression, фрагменты которого будут приведены ниже, позволяет всем желающим быстро сравнивать качество текстур пяти форматов. А именно, это Portable Network Graphics (PNG), Ericsson Texture Compression (ETC), Ericsson Texture Compression 2 (ETC2), PowerVR Texture Compression (PVRTC), и S3 Texture Compression (S3TC), который иногда называют DirectX Texture Compression (DXTC).

В примере показано, как загружать и использовать текстуры этих форматов с помощью OpenGL ES в Android. Изображения, хранящиеся в разных форматах, располагаются рядом друг с другом, что позволяет сравнить их размер и качество. Выбор наиболее подходящего под конкретный проект формата хранения сжатых текстур позволяет разработчику найти верный баланс между размером приложения, визуальным качеством картинки и производительностью.

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

Рассматриваемый здесь пример основан на коде, который создал Уильям Гуо (William Guo). Кристиано Феррейра (Christiano Ferreira), специалист по графическим приложениям Intel, дополнил его примером использования сжатия текстур ETC2. Загрузить код можно здесь.

ce08277b2c6bc0766b9f4ddb3f1c0f1c.png
Форматы сжатия текстур: размеры и качество

Загрузка PNG


С MIP-картами в формате PNG можно работать с помощью простой функции glGenerateMipmap из Khronos OpenGL, которая была создана специально для этой цели. Мы, для чтения и загрузки PNG-файлов, воспользовались кодом, подготовленным Шоном Барретом (Sean Barret), stb_image.c, который находится в открытом доступе. Так же этот код используется для нахождения и выборки участка текстуры, который нужно обработать.

  // Инициализация текстуры
    glTexImage2D( GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, pData );
    // Поддержка MIP-карты
    glGenerateMipmap( GL_TEXTURE_2D );


Загрузка ETC / ETC2


Как было упомянуто выше, ETC-текстуры могут храниться в файлах формата KTX и PKM. KTX — это стандартный формат сжатия, используемый как контейнер для нескольких изображений, он идеально подходит для создания MIP-карт. В свою очередь, PKM создан для хранения отдельных сжатых изображений, поэтому создание на его основе MIP-карт приводит к необходимости генерировать множество файлов, а это неэффективно. Поддержка MIP-карт для ETC в примере ограничена форматом KTX.

Khronos предоставляет библиотеку с открытым кодом, написанную на C (libktx), в которой поддерживается загрузка MIP-карт из KTX-файлов. Мы этой библиотекой воспользовались и реализовали код в функции LoadTextureETC_KTX, ответственной за загрузку текстур. Функция, которая непосредственно загружает KTX-файлы, называется ktxLoadTextureM. Она позволяет загружать нужную текстуру из данных в памяти. Эта функция — часть библиотеки libktx, документацию по ней можно найти на сайте Khronos.

Вот фрагмент кода, который инициализирует текстуру и предоставляет поддержку MIP-карт для формата ETC (KTX).

// Создание обработчика (handle) и загрузка текстуры
    GLuint handle = 0;
    GLenum target;
    GLboolean mipmapped;
        KTX_error_code result = ktxLoadTextureM( pData, fileSize, &handle, &target, NULL, &mipmapped, NULL, NULL, NULL );
    if( result != KTX_SUCCESS )
    {
        LOGI( "KTXLib couldn't load texture %s. Error: %d", TextureFileName, result );
        return 0;
    }
    // Привязка текстуры
    glBindTexture( target, handle );


Загрузка PVRTC


Поддержка MIP-карт для PVRTC-текстур — задачка чуть посложнее. После чтения заголовка определяется смещение, которое равняется сумме размеров заголовка и метаданных. Метаданные идут следом за заголовком, они не являются частью изображения. Для каждого сгенерированного уровня карты пиксели группируются в блоки (различия зависят от того, применяется ли кодировка 4 бита на пиксель или 2 бита — и тот и другой варианты подходят для PVRTC). Далее, происходит поиск границ, фиксируется ширина и высота блоков. Затем вызывается функция glCompressedTexImage (), она идентифицирует двумерное изображение в сжатом формате PVRTC. Далее, вычисляется размер пиксельных данных и то, что получилось, добавляется к ранее найденному смещению для того, чтобы сгруппировать набор пикселей для следующего фрагмента карты. Этот процесс повторяется до тех пор, пока не будут обработаны все текстуры, из которых состоит карта.

// Инициализация текстуры
    unsigned int offset = sizeof(PVRHeaderV3) + pHeader->mMetaDataSize;
    unsigned int mipWidth = pHeader->mWidth;
    unsigned int mipHeight = pHeader->mHeight;
    unsigned int mip = 0;
    do
    {
        // Определение размера (ширина * высота * bbp/8), минимальный размер равен 32
        unsigned int pixelDataSize = ( mipWidth * mipHeight * bitsPerPixel ) >> 3;
        pixelDataSize = (pixelDataSize < 32) ? 32 : pixelDataSize;

        // Выгрузка текстурных данных для фрагмента карты
        glCompressedTexImage2D(GL_TEXTURE_2D, mip, format, mipWidth, mipHeight, 0, pixelDataSize, pData + offset);
        checkGlError("glCompressedTexImage2D");
        // Следующий фрагмент имеет в два раза меньший размер, минимум – 1 
        mipWidth  = ( mipWidth >> 1 == 0 ) ? 1 : mipWidth >> 1;
        mipHeight = ( mipHeight >> 1 == 0 ) ? 1 : mipHeight >> 1;
        // Переходим к следующему фрагменту
        offset += pixelDataSize;
        mip++;
    } while(mip < pHeader->mMipmapCount);


Загрузка S3TC


После загрузки файла, хранящего S3TC-текстуры, определяется его формат и выполняется чтение MIP-карты, расположенной за заголовком. Производится обход фрагмента карты, пиксели группируются в блоки. Затем, для идентификации двумерного изображения в сжатых данных, вызывается функция glCompressedTexImage (). Общий размер блока затем добавляется к смещению для того, чтобы можно было найти начало следующего фрагмента карты и выполнить те же действия. Это повторяется до тех пор, пока не будут обработаны все уровни карты. Вот фрагмент кода, который инициализирует текстуру и предоставляет поддержку MIP-карт для формата S3TC.

// Инициализация текстуры
    // Выгрузка текстурных карт
    unsigned int offset = 0;
    unsigned int width = pHeader->mWidth;
    unsigned int height = pHeader->mHeight;
    unsigned int mip = 0;
    do
    {
        // Определение размера
        // В расширении определено: size = ceil(/4) * ceil(/4) * blockSize
        unsigned int Size = ((width + 3) >> 2) * ((height + 3) >> 2) * blockSize;
         glCompressedTexImage2D( GL_TEXTURE_2D, mip, format, width, height, 0, Size, (pData + sizeof(DDSHeader)) + offset );
        checkGlError( "glCompressedTexImage2D" );
        offset += Size;
        if( ( width <<= 1 ) == 0) width = 1;
        if( ( height <<= 1 ) == 0) height = 1;
        mip++;
    } while( mip < pHeader->mMipMapCount );


Выводы


В зависимости от конкретной ситуации, выбор наиболее подходящего формата хранения сжатых текстур может улучшить внешний вид изображений, серьёзно уменьшить размер приложения и значительно повысить производительность. Тщательный подбор оптимального метода сжатия текстур способен дать разработчикам и их приложениям серьёзные конкурентные преимущества. В приложении-примере Android Texture Compression показано, как работать с текстурами наиболее популярных в Android-среде форматов. Загружайте код и добавляйте в свои проекты поддержку наиболее подходящих для них форматов сжатия текстур.

© Habrahabr.ru