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

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

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

BC1:

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

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

    1. Если color_0 > color_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. color_0 <= color_1

      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> // для ComPtr

using 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, так что вам всё уже знакомо.

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

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

Комментарии (2)


  1. AnSa8
    30.06.2026 15:50

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

    Да кто же для таких изображений формат PNG использует...


  1. AnSa8
    30.06.2026 15:50

    Для цикла ваших статей очень не хватает репозитория на Github со всеми примерами.