Титульная страница DelphiGFX Сделать закладку Написать письмо автору сервера 

  Главная - Документация - 2D Графика

Мастера Delphi | Corba  

Низкоуровневая загрузка растра

Copyright © 2002 Мироводин Дмитрий  

Содержание

Введение
Загрузка растра из ресурса
Защита ресурсов
Загрузка растра из файла
24 bit
8 bit
Защита файлов
Список ссылок

Введение

В статье рассматриваются два способа загрузки растра - из ресурсной секции PE файла (для тех кто не в курсе, это обычных исполняемый exe файл семейства Widows 9X, NT) и непосредственно из файла. Зачем я всё это описываю, если есть прекрасный, отработанный код

Var
Bitmap : Tbitmap;

Bitmap.LoadFromFile('somepicture.bmp');

… действительно, метод хорош, но есть несколько моментов, по которым использовать данный метод не желательно.

  1. Если в проекте Вы используете только API функции, соответственно нет доступа к базовым классам Delphi.
  2. Вы хотите создать один исполняемый файл, поместив всю графику в него.
  3. Вы пишите игру, или графическое приложение и вся графика (другая информация) должна размещаться в одном или нескольких ресурсных файлах.
  4. Вам нужно получить оптимизированный код для загрузки только 16 битных (или других) изображений.
  5. Вы хотите создать собственный формат хранения данных, защитить графику или компрессировать данные. Эти доводы особенно актуальны для создателей игр.

Итак если Вам не интересны приведенные выше доводы, можете смело закрывать окно браузера, а для оставшихся читателей продолжим. Для начала рассмотрим самый простой, с точки зрения реализации, метод - загрузка растра из ресурса.

Загрузка растра из ресурса

Перед тем как пытаться загружать растр разберёмся, как же поместить его в файл ресурса. Допустим, мы хотим подключить BMP файл с названием some24bit.bmp к нашему приложению. Создадим ресурсный файл samp.rc и добавим туда следующую строчку:

MYBITMAP BITMAP ..\Resource\some24bit.bmp

Компилируем файл samp.rc для получения бинарного файла ресурса samp.res, набрав в командной строке brcc32.exe samp.rc. Подключим в наш проект полученный ресурсный файл, добавив директиву компилятора {$R samp.res}.

Описание функция для загрузки растра из ресурсного файла выглядит слдедующим образом:

function LoadBitmap(hInstance: HINST; lpBitmapName: PAnsiChar): HBITMAP;

Функция имеет всего два параметра и возвращает нам указатель на структуру загруженного растра - HBitmap. В качестве hInstance передаем Instance нашего приложения, а в качестве параметра lpBitmapName используется идентификатор из RC файла, в нашем случае это MYBITMAP.

Кстати, если Вы не знаете, что такое Hbitmap, как его потом использовать или безуспешно пытаетесь заменить Hbitmap на Tbitmap - смело закрывайте браузер, статья не для Вас.

Как Вы могли заметить, это пожалуй самый простой способ инициализации и загрузки растра. Всё что от нас требуется это подключить растр к ресурсному файлу и вызвать функцию LoadBitmap. Причем функция LoadBitmap загружает любые растры, в не зависимости от количества бит цвета.

Пример использования функции описан ниже, а полный код примера находится в каталоге DIBFromRes.

var
Bitmap : HBitmap;

Bitmap := LoadBitmap(Hinstance, 'MYBITMAP');

Не лишним является проверка указателя Bitmap на 0, т.к. растра может по каким то причинам не загрузится.

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

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

Допустим, у вас существует по несколько десятков спрайтов для каждого персонажа игры (движения главного героя, пули, взрывы, текстуры фона, движения врагов) и все они хранятся в отдельных файлах. Это удобно для редактирования, но совсем не применимо для хранения - ведь придется подключать в ресурс сотни файлов, и потом их загружать. По этому лучше всего объединить сходную графику в одни файлы : hero.bmp, shot.bmp, expl.bmp и т.д.

В такой реализации существенно сокращается время загрузки программы и её размер, т.к. мы сокращаем количество заголовков BMP файла с сотни до 5-10.

Теперь поговорим о защищенности графических ресурсов - её просто нет :) Любой, более менее грамотный пользователь, всегда сможет без труда извлечь или изменить эти графические данные воспользовавшись программой типа Resource Workshop или PEExplorer.

Как можно предотвратить подобные нежелательные действия? Самый простой, но далеко не оптимальный путь, сжатие готового exe файла различными компрессорами типа ASPack, UPX и т.д. Самый хороший результат даёт ASPack 2.12, но во первых, незначительно увеличивается расход памяти, а во вторых, более пытливый пользователь (скажем так, не полный ламер:)) знает о таких программках как ASPackDie - весь ваш труд окажется напрасным.

Единственный и пожалуй самый действенный метод защиты от такого "беспредела" - хранение сжатых ресурсов и изменение заголовков.

Первый способ заключается в том, что к exe файлу подключается уже сжатый bmp файл. На мой взгляд самый оптимальный вариант использование библиотеки Zlib - она поставляется вместе с Delphi, имеет не плохую степень компрессии (причем изменяемую, можно легко варьировать степенью сжатия и временем загрузки) и самое главное самодостаточна. Т.е. нет необходимости таскать с приложением набор dll файлов для разархивации. Этот метод я попробую подробно изложить в продолжении данной статьи.

Изменение заголовков. Чтобы понять суть метода приведу небольшой пример. Допустим у нас есть 10 одинаковых BMP файлов, т.е. у них одинаковый размер (высота, ширина), палитра (если 256 цветные), количество цветов, следовательно мы можем хранить только 1 заголовок и данные для остальных файлов. Скажем так, это не только защита ресурсов Вашей программы, но и экстремальная оптимизация по размеру:) Этот метод я так же изложу в продолжении данной статьи.

Загрузка растра из файла

Рассмотренный выше способ довольно прост в реализации, но не очень гибок, хотя и применяется в большинстве коммерческих продуктов. Он не подходит для большинства больших игр - разве Вы видели файл типа Dialblo.exe размеров ~600 МБ :) … нет вот и я не видел, а вот файлы типа gamedata.dat размерами 1 Гб пожалуйста.

Для дальнейшего изучения материала необходимо знание внутренней структуры BMP файла, по этому я отправлю Вас на изучение моей статьи Обзор формата DIB и компонентов для работы с ним. Итак приступим.

Для начала необходимо наметить алгоритм загрузки файла: это подготовка структур, последовательное считывание структур из файла, создание растра. Т.е. в результате подобных действий у нас получится указатель на HBitmap.

В настоящее время широко используются растры двух форматов 24 битные и 8 битные. 24-х битные обычно применяются в качестве текстур, фонов и т.д., а 8-ми битные до сих пор применяю для хранения спрайтов. Да, да, 8 битные растры до сих пор в моде, хороший пример тому культовая игра Heroes III (на счет четвёрки не знаю).

По этому я напишу две разные процедуры для загрузки растров, хотя можно было бы объединить их в одну, но зачем на делать лишние проверки ? Ведь при загрузке мы точно знаем какой тип растра загружаем.

Первым делом объявим глобальную переменную для хранения загруженного растра:

Var

// Переменная для хранения растра
Bitmap : HBITMAP;

Так же в константы вынесем названия и пути файлов для загрузки:

const
Bitmap24bitFileName='..\Resource\some24bit.bmp';
Bitmap8bitFileName='..\Resource\some8bit.bmp';

Конечно можно передавать их самим процедурам загрузки, но это уже тонкости реализации. Сначала я рассмотрю процедуру для загрузки 24-х битного растра, т.к. у него нет палитры и это несколько упрощает дело. Объявляем необходимые переменные:

procedure LoadBitmap24bit(const hWindow: THandle);
var
// Файл
F : File;
// Заголовочные структуры

BitmapFileHeader : TBITMAPFILEHEADER;
BitmapInfoHeader : TBITMAPINFOHEADER;
BitmapInfo : TBitMapInfo;
// Указатель на массив битов
BitmapBits : Pointer;
// Счетчик чтения файла
ReadCount : DWORD;
// Контекст устройства
DC : HDC;

Сама процедура выглядит так:

// Открытие файла и установка на начало
AssignFile(F, Bitmap24bitFileName);
Reset(F, 1);
// Чтение заголовка TBitmapFileHeader
BlockRead(F, BitmapFileHeader, SizeOf(TBitmapFileHeader), ReadCount);
if (ReadCount <> SizeOf(TBitmapFileHeader)) then
begin
  MessageBox(hWnd, 'Ошибка чтения BITMAPFILEHEADER', 'DIB From File Sample', MB_OK+MB_ICONERROR);
  CloseFile(F);
  Exit;
end;
// Чтение заголовка TBitmapInfoHeader
BlockRead(F, BitmapInfoHeader, SizeOf(TBitmapInfoHeader), ReadCount);
if (ReadCount <> SizeOf(TBitmapInfoHeader)) then
begin
  MessageBox(hWnd, 'Ошибка чтения TBITMAPINFOHEADER', 'DIB From File Sample', MB_OK+MB_ICONERROR);
  CloseFile(F);
  Exit;
end;
// Выделение памяти под массив битов
GetMem(BitmapBits, BitmapInfoHeader.biSizeImage);
// Чтение массива битов, т.е. непосредственно битового образа
BlockRead(F, BitmapBits^, BitmapInfoHeader.biSizeImage, ReadCount);
if (ReadCount <> BitmapInfoHeader.biSizeImage) then
begin
  MessageBox(hWnd, 'Ошибка чтения данных файла.', 'DIB From File Sample', MB_OK+MB_ICONERROR);
  CloseFile(F);
  Exit;
end;
// TBitmapInfo нужна только для функции CreateDIBitmap, по этому приходится инициализировать и её
BitmapInfo.bmiHeader:=BitmapInfoHeader;
// Получение контекста устройства
// Вотзачем нам потребовалось передавать дискриптор окна !
DC:=GetDC(hWindow);
// Создание битмапа из инициализорованных структур
Bitmap:=CreateDIBitmap(DC, BitmapInfoHeader, CBM_INIT, BitmapBits, BitmapInfo, DIB_RGB_COLORS);
// Освобождаем контекста устройства
ReleaseDC(hWindow, DC);
// Небольшая проверочка напоследок
if Bitmap=0 then
  MessageBox(hWnd, 'Ошибка создания растра.', 'DIB From File Sample', MB_OK+MB_ICONERROR);
CloseFile(F);

В принципе, ничего лишнего. Единственно что можно оптимизировать, так это получение контекста устройства внутри процедуры, отключение проверок и попробовать считывать TBitmapInfo, а то он лежит у нас практически до конца процедуры "мертвым грузом".

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

procedure LoadBitmap8bit(const hWindow: THandle);
var
// Пока без изменений
F : File;
BitmapFileHeader : TBITMAPFILEHEADER;
BitmapInfoHeader : TBITMAPINFOHEADER;
BitmapInfo : TBitMapInfo;
BitmapBits : Pointer;
ReadCount : DWORD;
DC : HDC;
// Массив под палитру
BitmapPal : array [0..255] of TRGBQUAD;
I : byte;

 

// Пока без изменений
AssignFile(F, Bitmap8bitFileName);
Reset(F, 1);
BlockRead(F, BitmapFileHeader, SizeOf(TBitmapFileHeader), ReadCount);
if (ReadCount <> SizeOf(TBitmapFileHeader)) then
begin
  MessageBox(hWnd, 'Ошибка чтения BITMAPFILEHEADER', 'DIB From File Sample', MB_OK+MB_ICONERROR);
  CloseFile(F);
  Exit;
end;
BlockRead(F, BitmapInfoHeader, SizeOf(TBitmapInfoHeader), ReadCount);
if (ReadCount <> SizeOf(TBitmapInfoHeader)) then
begin
  MessageBox(hWnd, 'Ошибка чтения TBITMAPINFOHEADER', 'DIB From File Sample', MB_OK+MB_ICONERROR);
  CloseFile(F);
  Exit;
end;
// Чтение палитры
BlockRead(F, BitmapPal, SizeOf(TRGBQUAD)*256, ReadCount);
// Далее все повторяется, как в предыдущей процедуре
GetMem(BitmapBits, BitmapInfoHeader.biSizeImage);

BlockRead(F, BitmapBits^, BitmapInfoHeader.biSizeImage, ReadCount);
if (ReadCount <> BitmapInfoHeader.biSizeImage) then
begin
  MessageBox(hWnd, 'Ошибка чтения данных файла.', 'DIB From File Sample', MB_OK+MB_ICONERROR);
  CloseFile(F);
  Exit;
end;

BitmapInfo.bmiHeader:=BitmapInfoHeader;
// Заполнение палитры
for I:=0 to 255 do
begin
  BitmapInfo.bmiColors[I].rgbBlue:=BitmapPal[I].rgbBlue;   BitmapInfo.bmiColors[I].rgbGreen:=BitmapPal[I].rgbGreen;   BitmapInfo.bmiColors[I].rgbRed:=BitmapPal[I].rgbRed;   BitmapInfo.bmiColors[I].rgbReserved:=BitmapPal[I].rgbReserved;
end;
// Создание растра
DC:=GetDC(hWindow);
Bitmap:=CreateDIBitmap(DC, BitmapInfoHeader, CBM_INIT, BitmapBits, BitmapInfo, DIB_RGB_COLORS);
ReleaseDC(hWindow, DC);

if Bitmap=0 then
  MessageBox(hWnd, 'Ошибка создания растра.', 'DIB From File Sample', MB_OK+MB_ICONERROR);

CloseFile(F);

Вот собственно и все ! Процедура готова. Необходимо остановится на возможностях её использования. В явном виде она аналогична примеру, приведенному в начале статьи Bitmap.LoadFromFile. В чем же её преимущество ? А в том, что с ее помощью можно последовательно считывать не 1 битмап из файла и сколько угодно. Структура файла может быть примерно следующая:

FileHeader
Заголовочная информация (строка типа SuperGame Resource File). Нужна только доля идентификации файла, а то знаете сколько различных *.dat форматов существует :)
FileVersion

Версия файла. Необходима для поддержки совместимости. Допустим если номер версии 1.0 используем загрузочную процедуру LoadProc1, а если 1.1 то LoadProc 2. Хотя в простом случаем и не обязательна.

ResourceCount
Количество растров в файле.
OffsetTable
Таблица смещений
DibHeader
Заголовок 1-го растра
DibBits
Данные 1-го растра
  ...
DibHeader
Заголовок N-го растра
DibBits
Заголовок N-го растра
  Далее может помещаться совершенно произвольная информация. Например звуковые ресурсы, текст, видео и т.д.

На первый взгляд громоздкая и не понятная структура. Но при детальном рассмотрении позволяет решить множество проблем. Опишу только несколько возможностей:

  1. Защищенность на достаточно высоком уровне. Во первых Вы получаете собственный формат хранения данных, т.е. простыми программами Ваш файл не просмотришь. Во вторых возможна компрессия ресурсов, т.е. каждая секция DibHeader и DibBits может быть заархивирована и/или зашифрована. Это пожалуй самая интересная часть.
  2. Практически мгновенная загрузка интересующего нас ресурса из огромного файла. Для этого нам необходимо только прочитать маленький заголовок и таблицу смещений. Из таблицы смещений по индексу (или по имени растра, если хранить и имена) получаем необходимое смещение и методом Seek мгновенно перемещаемся на начало данных необходимого растра. При желании, можно кэшировать таблицу смещений в оперативной памяти.
  3. Возможность оптимизации. При использовании одинаковых растров, их признаки я описывал выше, можно хранить только один DibHeader и множество DibBits. При большом количестве растров получается существенная экономия дискового пространства и скорости загрузки.

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

  1. Необходимо писать "компилятор" или "сшиватель" ресурсов.
  2. Подход сложно реализовать новичку, хотя я попытаюсь это исправить в последующей части статьи.

Список ссылок

Адрес автора
Исходные тексты примеров
Статья "Обзор формата DIB и компонентов для работы с ним"
Титульная страница DelphiGFX Сделать закладку Написать письмо автору сервера
Hosted by uCoz