Johnny_Depp1 час назад
Direct 2D #14. Разбиение на блоки и сжатие. Оптимизация текстур
Уровень сложностиПростойВремя на прочтение7 минОхват и читатели2KC++*Windows*Разработка игр*ТуториалВсем здравствуйте! Желаю вам прекрасного дня!
Сегодняшняя тема дословно переводится как "Сжатие блоков". Что это такое?
Это набор алгоритмов для сжатия изображений (текстур и т. п.). Алгоритмы построены на идее разбиения изображения на блоки размером 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, так что вам всё уже знакомо.
На этом всё! Всем удачи и всего прекрасного!
При желании материально поддержать перевод и структурирование информации - средства можете отправить через сбор в ЮМани.Теги:• C++
• Windows
• Direct2D
• разработка играХабы:• C++
• Windows
• Разработка игр
Получайте больше инсайтов о систематизации бизнеса
Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности
💬 Подписаться на канал→ Оригинальная статья