Direct 2D #14. Разбиение на блоки и сжатие. Оптимизация текстур

от автора

Всем здравствуйте! Желаю вам прекрасного дня!

Сегодняшняя тема дословно переводится как «Сжатие блоков». Что это такое?

Это набор алгоритмов для сжатия изображений (текстур и т. п.). Алгоритмы построены на идее разбиения изображения на блоки размером 4×4 (поэтому изображение должно быть кратно 4) и сжатии каждого блока. Как именно это происходит? Разберём на основе BC1 — алгоритма для сжатия без альфа-канала или с 1 битом на этот канал. Степень сжатия — x8.

BC1:

  1. В блоке выделяем два опорных цвета. Обычно самый яркий и тёмный и сохраняем их в формате. RGB565 (5 бит на красный, 6 на зелёный, 5 на синий) — это 2 байта на цвет.

  2. Строим палитру из 4 цветов(разветвление на то, есть альфа канал или нет):

    1. Если нет:

      1. color_2 = (2 * color_0 + color_1) / 3

      2. color_3 = (color_0 + 2 * color_1) / 3

      3. Итог. Палитра: [color_0, color_1, color_2, color_3] — 4 непрозрачных цвета.

    2. Если есть:

      1. color_2 = (color_0 + color_1) / 2

      2. Итог. Палитра: [color_0, color_1, color_2, Transparent (прозрачный)] — 3 цвета + прозрачность.

  3. Проходимся по каждому пикселю в блоке и выбираем самый близкий цвет из палитры, присваивая пикселю 2-битный индекс (0, 1, 2 или 3).

  4. Упаковываем в блок. Итоговый блок 8 байт содержит:

    1. 4 байта: два опорных цвета в RGB565.

    2. 4 байта: 16 двухбитных индексов для всех пикселей.

    3. Итог: Плотность 4 бита на пиксель (вместо 32), сжатие 8:1.

Думаю, вы понимаете, что на небольшом изображении тут же проявятся артефакты, да и места вы выиграете немного.
ВАЖНО! GPU автоматически декодирует сжатые данные при рендеринге, поэтому потерь в производительности нет.
Ниже — таблица форматов сжатия:

Формат

Назначение

Сжатие

BC1

Изображение без альфа-канала или с 1 бит на данный канал

8:1 (4 бита/пиксель)

BC2

Грубое сжатие альфа

4:1 (8 бит/пиксель)

BC3

Просто хорошее сжатие альфа

4:1 (8 бит/пиксель)

Перед тем как перейти к практике, я расскажу про BC2 и BC3:

BC2:

BC2 сжимает блок 4×4 в 16 байт. Сам алгоритм:

  1. Кодируем альфа-канал (8 байт). Каждый из 16 пикселей получает своё собственное значение альфы. Оно хранится с точностью 4 бита (16 градаций от 0 до 15). 16 пикселей × 4 бита = 64 бита = 8 байт.

  2. Кодируем цвет (8 байт). Цветовая информация (RGB) кодируется точно так же, как в BC1. Используются два опорных цвета, интерполяция и 2-битные индексы.

  3. Упаковываем в блок. Блок 16 байт состоит из двух частей:

    1. Первые 8 байт: «сырая» альфа-информация (по 4 бита на пиксель)

    2. Вторые 8 байт: обычный блок BC1 для цвета

  4. Итог: Плотность 8 бит на пиксель (4 бита альфа + 4 бита цвет), сжатие 4:1.

BC3:

Собственно, качество выше, потому что на альфа-канал отводится не 4 бита, а 8.

  1. Кодируем альфа-канал (8 байт). Здесь альфа сжимается по тому же принципу, что и цвет в BC1:

    1. Выбираются два опорных значения альфы (по 1 байту каждое)

    2. Между ними вычисляются промежуточные значения путём линейной интерполяции.

    3. Каждый пиксель получает 3-битный индекс (0–7), указывающий на одно из 8 возможных значений альфы в палитре.

    4. 16 пикселей × 3 бита = 48 бит + 2 байта опорных альф = 64 бита = 8 байт.

  2. Кодируем цвет (8 байт). Цветовая информация (RGB) кодируется точно так же, как в BC1.

  3. Упаковываем в блок. Блок 16 байт состоит из двух частей:

    1. Первые 8 байт: сжатая альфа-информация (8 значений в палитре + 3-битные индексы).

    2. Вторые 8 байт: обычный блок BC1 для цвета.

  4. Итог: Плотность 8 бит на пиксель, сжатие 4:1.

Собственно, от теории к практике. Я буду использовать библиотеку (от Microsoft) — DirectXTex.

В начале покажу оригинальное изображение в формате PNG весом 2378 килобайт и результат сжатия, а в конце — уже код.

Мяу ^-^

Мяу ^-^

Теперь сжатие BC1 , формат DDS размер 1013 килобайт.

Как вы могли заметить, разницы нет, так как BC1 не просто усредняет цвета, а анализирует каждый блок 4×4 и подбирает два опорных цвета так, чтобы минимизировать ошибку. Если в блоке все пиксели близки по цвету, то разница почти не заметна. Ну а ещё мы хуже различаем мелкие цветовые отличия, чем яркостные. BC1 специально выделяет больше бит под зелёный канал (6 бит) и меньше под красный и синий (по 5 бит) — это совпадает с чувствительностью глаза, а также изображение изначально Full HD, и каждый блок 4×4 занимает очень маленькую область на экране.

И остаётся показать вам BC2 и BC3.

BC2:

Формат тот же, но размер уже 2026 килобайт

BC3:

И формат тот же и размер

Собственно, альфа-канала особо и нет, и разницы нет.
Собственно, функция сжатия и функция сохранения файла:

#include <DirectXTex.h>#include <d2d1_1.h>#include <d3d11.h>#include <wincodec.h>#include <iostream>  // для вывода сообщений// Функция сжатия (ваша)HRESULT CompressImageToBC(    _In_ const DirectX::Image& srcImage, //исходное изображение    _In_ DXGI_FORMAT format, //формат сжатия    _Out_ DirectX::ScratchImage& compressedImage //Возвращение сжатого изображения){     //Проверка того что формат подходящий    if (!DirectX::IsCompressed(format)) {        return E_INVALIDARG;    }      DirectX::TEX_COMPRESS_FLAGS compressFlags = DirectX::TEX_COMPRESS_DEFAULT;    float threshold = DirectX::TEX_THRESHOLD_DEFAULT;    return DirectX::Compress(        srcImage,        format,        compressFlags,        threshold,        compressedImage    );}// Сохранение сжатого изображения в DDS-файлHRESULT SaveAsDDS(    _In_ const DirectX::ScratchImage& image,    _In_ LPCWSTR filename){    return DirectX::SaveToDDSFile(        image.GetImages(),        image.GetImageCount(),        image.GetMetadata(),        DirectX::DDS_FLAGS_NONE,        filename    );}

Про некоторые флаги:

  1. TEX_COMPRESS_RGB_DITHER / TEX_COMPRESS_A_DITHER — включают дизеринг (добавление шума) для цветов и альфа-канала. Это помогает скрыть артефакты сжатия на плавных градиентах, делая их менее заметными для глаза.

  2. TEX_COMPRESS_UNIFORM — отключает перцептивное взвешивание. По умолчанию алгоритм сильнее “бережёт” цвета, к которым глаз наиболее чувствителен. Это полезно для изображений, где все каналы равнозначны.

  3. TEX_COMPRESS_PARALLEL — включает многопоточное сжатие.

  4. TEX_COMPRESS_SRGB_IN / TEX_COMPRESS_SRGB_OUT — указывают, что входное или выходное изображение использует цветовое пространство sRGB. Это важно для корректной работы с гаммой, особенно на текстурах, которые будут использоваться для освещения.

  5. threshold — порог прозрачности для BC1. Этот параметр используется только для формата BC1. Напомню, что BC1 может хранить только 1 бит прозрачности (пиксель либо полностью прозрачный, либо нет). Параметр threshold (порог) определяет, какие пиксели станут прозрачными, а какие — нет. Значение каждого пикселя в альфа-канале (от 0.0 до 1.0) сравнивается с этим порогом. Результат: если значение альфы меньше порога, пиксель становится полностью прозрачным; если больше или равно — полностью непрозрачным.

Теперь — точка входа, ну и сам код использования функций:

int main(){    // Инициализируем COM (требуется для WIC)    CoInitialize(nullptr);     // 1. Загружаем исходное изображение    DirectX::ScratchImage srcImage;    HRESULT hr = DirectX::LoadFromWICFile(L"C:\\Users\\JoniDeep\\source\\repos\\CompressedImage\\x64\\Debug\\input.png", DirectX::WIC_FLAGS_NONE, nullptr, srcImage);    if (FAILED(hr))    {        std::wcerr << L"Не удалось загрузить input.png. Ошибка: " << std::hex << hr << std::endl;        CoUninitialize();        return 1;    }    std::wcout << L"Исходное изображение загружено: "        << srcImage.GetMetadata().width << L"x"        << srcImage.GetMetadata().height << std::endl;    // Получаем первый образ (без мип-уровней)    const DirectX::Image* pSrcImage = srcImage.GetImage(0, 0, 0);    if (!pSrcImage)    {        std::wcerr << L"Ошибка: не удалось получить образ изображения." << std::endl;        CoUninitialize();        return 1;    }    // 2. Сжимаем в три формата    DirectX::ScratchImage bc1Image, bc2Image, bc3Image;    hr = CompressImageToBC(*pSrcImage, DXGI_FORMAT_BC1_UNORM, bc1Image);    if (SUCCEEDED(hr))    {        // Сохраняем DDS        hr = SaveAsDDS(bc1Image, L"texture_BC1.dds");        if (SUCCEEDED(hr)) std::wcout << L"Сохранено: texture_BC1.dds" << std::endl;        // Сохраняем PNG для просмотра (распакованный)    }    else    {        std::wcerr << L"Ошибка сжатия BC1: " << std::hex << hr << std::endl;    }    hr = CompressImageToBC(*pSrcImage, DXGI_FORMAT_BC2_UNORM, bc2Image);    if (SUCCEEDED(hr))    {        hr = SaveAsDDS(bc2Image, L"texture_BC2.dds");        if (SUCCEEDED(hr)) std::wcout << L"Сохранено: texture_BC2.dds" << std::endl;    }    else    {        std::wcerr << L"Ошибка сжатия BC2: " << std::hex << hr << std::endl;    }    hr = CompressImageToBC(*pSrcImage, DXGI_FORMAT_BC3_UNORM, bc3Image);    if (SUCCEEDED(hr))    {        hr = SaveAsDDS(bc3Image, L"texture_BC3.dds");        if (SUCCEEDED(hr)) std::wcout << L"Сохранено: texture_BC3.dds" << std::endl;    }    else    {        std::wcerr << L"Ошибка сжатия BC3: " << std::hex << hr << std::endl;    }    std::wcout << L"\nГотово! Файлы созданы в папке с исполняемым файлом." << std::endl;    CoUninitialize();    return 0;}

Вообще есть алгоритмы и после BC3, но они уже для Direct3D, ну точнее для того, что он может использовать и размещать в видеопамяти. У вас, скорее всего, остаётся вопрос: «А как это теперь отрисовывать?» — а всё просто: WIC, который мы разбирали в прошлых статьях, умеет декодировать DDS в Bitmap, ну а дальше вы уже знаете, что делать (по прошлым статьям).

Собственно, функция загрузки (так как есть отличия):

#include <wincodec.h>#include <d2d1_1.h>#include <wrl/client.h> // для ComPtrusing Microsoft::WRL::ComPtr;HRESULT LoadDDSAsBitmap(    ID2D1DeviceContext* pD2DContext,    IWICImagingFactory* pWICFactory,    LPCWSTR filePath,    ID2D1Bitmap1** ppBitmap){    if (!pD2DContext || !pWICFactory || !ppBitmap) return E_INVALIDARG;    ComPtr<IWICBitmapDecoder> pDecoder;    HRESULT hr = pWICFactory->CreateDecoderFromFilename(        filePath,        nullptr,                           // Не используем предпочтения        GENERIC_READ,        WICDecodeMetadataCacheOnLoad,      // Кешируем метаданные        &pDecoder    );    if (FAILED(hr)) return hr;    ComPtr<IWICBitmapFrameDecode> pFrame;    hr = pDecoder->GetFrame(0, &pFrame);   // Берем первый кадр (0)    if (FAILED(hr)) return hr;    // Создаем битмап. Direct2D сам определит формат (сжатый или нет).    // Для сжатых форматов важно использовать альфа-режим Premultiplied[reference:1].    D2D1_BITMAP_PROPERTIES1 props = D2D1::BitmapProperties1(        D2D1_BITMAP_OPTIONS_NONE,        D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED)    );    hr = pD2DContext->CreateBitmapFromWicBitmap(        pFrame.Get(),        &props,        ppBitmap    );    return hr;}

Если вы знаете, что алгоритм сжатия — BC3, можете указать DXGI_FORMAT_BC3_UNORM, но если нет — оставьте DXGI_FORMAT_UNKNOWN. Подробно мы разбирали это в статье про WIC, так что вам всё уже знакомо.

На этом всё! Всем удачи и всего прекрасного!

При желании материально поддержать перевод и структурирование информации — средства можете отправить через сбор в ЮМани.

ссылка на оригинал статьи https://habr.com/ru/articles/1054070/