Всем здравствуйте! Желаю вам прекрасного дня!
Сегодняшняя тема дословно переводится как "Сжатие блоков". Что это такое?
Это набор алгоритмов для сжатия изображений (текстур и т. п.). Алгоритмы построены на идее разбиения изображения на блоки размером 4×4 (поэтому изображение должно быть кратно 4) и сжатии каждого блока. Как именно это происходит? Разберём на основе BC1 - алгоритма для сжатия без альфа-канала или с 1 битом на этот канал. Степень сжатия - x8.
BC1:
В блоке выделяем два опорных цвета. Обычно самый яркий и тёмный и сохраняем их в формате. RGB565 (5 бит на красный, 6 на зелёный, 5 на синий) - это 2 байта на цвет.
-
Строим палитру из 4 цветов(разветвление на то, есть альфа канал или нет):
-
Если color_0 > color_1:
color_2 = (2 * color_0 + color_1) / 3
color_3 = (color_0 + 2 * color_1) / 3
Итог. Палитра: [color_0, color_1, color_2, color_3] - 4 непрозрачных цвета.
-
color_0 <= color_1
color_2 = (color_0 + color_1) / 2
Итог. Палитра: [color_0, color_1, color_2, Transparent (прозрачный)] — 3 цвета + прозрачность.
-
Проходимся по каждому пикселю в блоке и выбираем самый близкий цвет из палитры, присваивая пикселю 2-битный индекс (0, 1, 2 или 3).
-
Упаковываем в блок. Итоговый блок 8 байт содержит:
4 байта: два опорных цвета в RGB565.
4 байта: 16 двухбитных индексов для всех пикселей.
Итог: Плотность 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 байт. Сам алгоритм:
Кодируем альфа-канал (8 байт). Каждый из 16 пикселей получает своё собственное значение альфы. Оно хранится с точностью 4 бита (16 градаций от 0 до 15). 16 пикселей × 4 бита = 64 бита = 8 байт.
Кодируем цвет (8 байт). Цветовая информация (RGB) кодируется точно так же, как в BC1. Используются два опорных цвета, интерполяция и 2-битные индексы.
-
Упаковываем в блок. Блок 16 байт состоит из двух частей:
Первые 8 байт: "сырая" альфа-информация (по 4 бита на пиксель)
Вторые 8 байт: обычный блок BC1 для цвета
Итог: Плотность 8 бит на пиксель (4 бита альфа + 4 бита цвет), сжатие 4:1.
BC3:
Собственно, качество выше, потому что на альфа-канал отводится не 4 бита, а 8.
-
Кодируем альфа-канал (8 байт). Здесь альфа сжимается по тому же принципу, что и цвет в BC1:
Выбираются два опорных значения альфы (по 1 байту каждое)
Между ними вычисляются промежуточные значения путём линейной интерполяции.
Каждый пиксель получает 3-битный индекс (0–7), указывающий на одно из 8 возможных значений альфы в палитре.
16 пикселей × 3 бита = 48 бит + 2 байта опорных альф = 64 бита = 8 байт.
Кодируем цвет (8 байт). Цветовая информация (RGB) кодируется точно так же, как в BC1.
-
Упаковываем в блок. Блок 16 байт состоит из двух частей:
Первые 8 байт: сжатая альфа-информация (8 значений в палитре + 3-битные индексы).
Вторые 8 байт: обычный блок BC1 для цвета.
Итог: Плотность 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 ); }
Про некоторые флаги:
TEX_COMPRESS_RGB_DITHER / TEX_COMPRESS_A_DITHER - включают дизеринг (добавление шума) для цветов и альфа-канала. Это помогает скрыть артефакты сжатия на плавных градиентах, делая их менее заметными для глаза.
TEX_COMPRESS_UNIFORM - отключает перцептивное взвешивание. По умолчанию алгоритм сильнее “бережёт” цвета, к которым глаз наиболее чувствителен. Это полезно для изображений, где все каналы равнозначны.
TEX_COMPRESS_PARALLEL - включает многопоточное сжатие.
TEX_COMPRESS_SRGB_IN / TEX_COMPRESS_SRGB_OUT - указывают, что входное или выходное изображение использует цветовое пространство sRGB. Это важно для корректной работы с гаммой, особенно на текстурах, которые будут использоваться для освещения.
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)

AnSa8
30.06.2026 15:50Для цикла ваших статей очень не хватает репозитория на Github со всеми примерами.
AnSa8
Да кто же для таких изображений формат PNG использует...