DirectX для начинающих. Часть третья. Считывание и запись

Материал из DRKB


Привет всем, кто интересуется программированием под DirectX на языке Object Pascal!

Как и обещал, я продолжаю искать новый материал по DirectX, переводить его на язык Object Pascal и представлять всеобщему вниманию. Недавно у меня появилась идея снятия скриншотов с экрана DirectDraw-программы и записи изображения в простой bmp-файл - некоторые игры позволяют это делать, и я решил последовать их примеру. Потом я наткнулся на другой интересный материал - речь шла о загрузке изображения из bmp-файла без использования функции LoadImage(). Поэтому тема статьи всецело посвящена работе с bmp-файлами на "низком уровне". Замечу, что это немного сложные вещи, но мы ведь сложностей не боимся, правда? Иначе непонятно, зачем тогда заниматься изучением DirectX вообще.

Замечания[править | править код]

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

Немного изменилась реализация модуля ddutils.pas - теперь функция CreateSurface() не требует адрес главного интерфейса IDirectDraw, а создаёт и уничтожает локальный интерфейс. Возможно, более практично с точки зрения Pascal-программирования было бы объявить глобальный для модуля ddutils.pas интерфейс IDirectDraw, а для создания и удаления интерфейса воспользоваться секциями initialization и finalization модуля.

Также немного изменился стиль написания программ, теперь он совсем грешит С-подобным кодом :-)

FASTFILE1[править | править код]

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

  1. С помощью функции LoadImage() загружался растр и нам сообщался идентификатор загруженного растра в виде переменной типа HBITMAP;
  2. Функцией SelectObject() в контекст-источник выбирался созданный растр;
  3. Методом IDirectDrawSurface7.GetDC() создавался GDI-совместимый дескриптор контекста-приёмника и осуществлялась блокировка поверхности;
  4. Функцией GDI BitBlt() содержимое контекста-источника копировалось на контекст-приёмник.
  5. Методом IDirectDrawSurface7.ReleseDC() удалялся созданный контекст и осуществлялась разблокировка поверхности.

В чём недостатки такого подхода? В универсальности. Не скажу, что функция LoadImage() очень медленная, но и очень быстрой она не является. Во всяком случае, программисты, писавшие её, не ставили своей задачей обеспечить максимальную скорость загрузки. Посмотрите справку по этой функции (файл win32sdk.hlp) - обилие параметров и констант, задаваемых при вызове, наводят на мысль о том, что она довольно "тяжеловесна". В частности, сказано, что с её помощью можно загружать не только растры из bmp-файлов разных форматов (в их число входят монохромные, 16- и 256-цветные палитровые файлы, а также беспалитровые 24-битные файлы), но и файлы иконок Windows и даже файлы, содержащие изображения курсоров.

Естественно, всё это отрицательно сказывается на скорости загрузки - метод получается простым, но не самым эффективным. Поэтому часто программисты пишут собственные быстрые функции для загрузки файлов какого-то определённого формата. В этом примере я реализовал отдельную функцию, которая предназначена для загрузки данных из 24-битного беспалитрового файла формата bmp.

Прежде чем приступить к рассмотрению работы функции, необходимо в общих чертах представить себе, каким образом записывается информация в bmp-файле. На рис.1 показана структура беспалитрового 24-битного файла.

Dx bmp.gif

Рис.1. Структура файла BMP, не содержащего палитру


Хранящийся на диске файл DIB, обычно с расширением .bmp, как видно, начинается со структуры BITMAPFILEHEADER, позволяющей начать работу с файлом. Вот как эта структура описана в файле windows.pas:

tagBITMAPFILEHEADER = packed record
  bfType: Word;      // Тип файла. Должен содержать 'BM' ($4d42)
  bfSize: DWORD;     // Размер файла в байтах
  bfReserved1: Word; // Зарезервировано, должен быть нуль
  bfReserved2: Word; // Зарезервировано, должен быть нуль
  bfOffBits: DWORD;  // Смещение от начала файла до графических данных
end;
BITMAPFILEHEADER = tagBITMAPFILEHEADER;


Следом за структурой BITMAPFILEHEADER следует структура BITMAPINFO:

tagBITMAPINFO = packed record
  bmiHeader: TBitmapInfoHeader; // Структура BITMAPINFOHEADER
  bmiColors: array[0..0] of TRGBQuad; // RGB-триплекс
end;
BITMAPINFO = tagBITMAPINFO;


Фактически, структура BITMAPINFO включает в себя ещё одну структуру - BITMAPINFOHEADER:

tagBITMAPINFOHEADER = packed record
  biSize: DWORD; // Размер самой структуры в байтах
  biWidth: Longint; // Ширина растра в пикселях
  biHeight: Longint; // Высота растра в пикселях
  biPlanes: Word; // Количество плоскостей (всегда 1)
  biBitCount: Word; // Количество бит на 1 пиксель
  biCompression: DWORD; // Тип сжатия (BI_RGB - без сжатия)
  biSizeImage: DWORD; // Размер изображения в байтах (обычно 0)
  biXPelsPerMeter: Longint; // А эти данные
  biYPelsPerMeter: Longint; // нам вообще
  biClrUsed: DWORD; // никогда не
  biClrImportant: DWORD; // понадобятся :)
end;
BITMAPINFOHEADER = tagBITMAPINFOHEADER;


Эта структура для нас наиболее интересна, так как опираясь на её данные, и будет производиться загрузка растра. Несмотря на обилие полей, нам понадобятся только некоторые - это biWidth, biHeight и ещё поле biBitCount - для проверки, является ли файл 24-битным.

После этих структур начинаются графические данные. В 24-битном файле каждый пиксель кодируется 3 байтами - на каждую составляющую R, G, B - по одному байту. Значение каждой составляющей может варьироваться от 0 до 255.

Откройте файл проекта и найдите функцию LoadData(). Она вызывает другую функцию - LoadBitMap(). Я разместил её в файле ddutils.pas, вот её прототип

function LoadBitMap(name: PChar; pbi: PBITMAPINFO): Pointer;

Первым параметром передаётся имя загружаемого файла, вторым - адрес структуры BITMAPINFO, структура понадобится после вызова функции LoadBitMap().

Для считывания данных с диска я использую API-функции, предоставляемые ОС, а не библиотечные функции Delphi. Причина - немного более высокое быстродействие, при том, что сами функции просты в обращении и предоставляют некоторые средства контроля при чтении-записи.

Вот как, например, открывается файл:

hFile := CreateFile(name, GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0);
if hFile = INVALID_HANDLE_VALUE then
  Exit;


Переменная hFile - это дескриптор открытого файла. Проверить, открыт ли он в самом деле можно, сравнив дескриптор с константой INVALID_HANDLE_VALUE. Далее считывается структура BITMAPFILEHEADER:

ReadFile(hFile, bfh, SizeOf(BITMAPFILEHEADER), dwBytesRead, nil);

Замечу, что вторым параметром функции ReadFile() передаётся сама структура, куда будут записаны данные, третьим - количество байт, которые надо прочитать. Четвёртые параметр должен присутствовать обязательно, в него функция запишет количество реально прочитанных байт. Для пущей надёжности можно сравнить это значение с размером структуры BITMAPFILEHEADER, и если значения не совпадают, объявить об ошибке.

Далее считывается структура BITMAPINFOHEADER:

ReadFile(hFile, bi, SizeOf(BITMAPINFOHEADER), dwBytesRead, nil);

Думаю, надо объяснить, почему здесь мы читаем только данные структуры BITMAPINFOHEADER, и не считываем массив bmiColors. Дело в том, что этот массив в структуре BITMAPINFO, там, куда мы её передадим позже, всё равно не используется. Однако он входит в состав общих графических данных, поэтому мы считаем его вместе с ними, а в структуре bi массив bmiColors оставим пустым.

Далее идёт считывание графических данных. Прежде всего необходимо определить, какой размер они имеют:

// Определяем размер DIB
dwDIBSize := GetFileSize(hFile, nil) - SizeOf(BITMAPFILEHEADER) - SizeOf(BITMAPINFOHEADER);

То есть от размера bmp-файла отнимаются размеры описанных выше структур. Замечу, что для палитровых файлов придётся учитывать ещё и размер палитры. Далее, выделяется участок в оперативной памяти нужной длины и получается указатель на него:

// Выделяем участок памяти
Result := PByte(GlobalAllocPtr(GMEM_MOVEABLE, dwDIBSize));

После этого в память считываются битовые данные, формирующие картинку, и файл закрывается:

// Читаем данные DIB
ReadFile(hFile, Result^, dwDIBSize, dwBytesRead, nil);
// Закрываем файл
CloseHandle(hFile);


Описанная функция работает только с 24-битными несжатыми растрами. Использование 256-цветных палитровых файлов я считаю нецелесообразным, т. к. качество изображения в них не удовлетворяет требованиям современной компьютерной графики.

Итак, функция LoadBitMap() загрузила в оперативную память битовые данные, формирующие изображение и вернула указатель на них как результат функции. Вернёмся теперь обратно к функции LoadData(). Первый шаг сделан - произведена максимально быстрая загрузка данных из файла (я не вижу способа, как можно ещё как-нибудь ускорить этот процесс). Теперь надо сделать второй шаг. В чём он состоит? Для ускорения загрузки в играх и других программах все графические данные объединяются в один или несколько больших файлов. Такую реализацию, например, можно увидеть в игре Donuts из DirectX SDK 7-8. Такое объединение очень полезно при условии, что файл на жестком диске нефрагментирован. Данный метод, безусловно, уменьшает время загрузки, но как будет видно далее, добавляет лишних хлопот программисту.

Я подготовил простой bmp-файл, в котором хранится изображение для фона и десять "кадров", которые будут последовательно сменять друг друга. Как же загрузить эти данные на поверхности DirectDraw? Есть два пути:

  1. Воспользоваться методами Lock() и Unlock() интерфейса IDirectDrawSurface7, и осуществлять прямое копирование данных функцией CopyMemory(). Это решение оптимально по скоростным характеристикам, но уж очень сложное: необходимо учитывать абсолютно все нюансы формата поверхности, на которую копируются данные - а их очень много, ведь формат меняется в зависимости от глубины цвета текущего видеорежима - 8, 16, 24, 32 бит. К тому же, выигрыш в этом случае может оказаться совсем небольшим.
  2. Использовать функцию GDI StretchDIBits(). Она предназначена для копирования в контекст данных, расположенных не в другом контексте, а находящихся просто в указанном участке памяти. Может сложиться впечатление, что эта функция достаточно медленна - из-за угрожающей приставки "Stretch". Однако если будут копироваться участки битов, одинаковые по высоте и ширине, то в этом случае функция, думается, должна работать быстрее.

Я решил использовать второй способ. Итак, первым делом создадим поверхность для фона:

CreateSurface(g_pWallpaper, 640, 480);

После этого получим контекст для поверхности и осуществим копирование функцией StretchDIBits(). В файле справки о методе IDirectDrawSurface7.GetDC() сказано, что он является надмножеством над методом IDirectDrawSurface7.Lock() - т. е. осуществляет те же операции, которые мы бы проделали при прямом копировании данных. Различие в том, что здесь DirectDraw учитывает особенности формата поверхности при создании контекста-приёмника. Думаю, нет необходимости дублировать эти операции - выигрыш в скорости может оказаться весьма сомнительным, т.к. код в библиотеке DirectDraw и без того максимально быстр.

if g_pWallpaper.GetDC(DC) = DD_OK then
begin
  // Копируем битовый массив в контекст
  StretchDIBits(DC,
    0, 0, 640, 480,
    0, 64, 640, 480,
    pBits, bi,
    0, SRCCOPY);
  g_pWallpaper.ReleaseDC(DC);
end;


Заметьте, что растр в файле (и памяти) хранится в перевёрнутом виде, поэтому ось Y битовой карты направлена вверх. Это необходимо учитывать при задании области копирования. Для копирования массива битов функции StretchDIBits() необходимо передать адрес массива в памяти, а также адрес структуры BITMAPINFO - опираясь на неё, она сможет правильно произвести копирование.

Далее 10 раз осуществляется копирование в отдельные поверхности массива g_pMovie. Опять же, необходимо учитывать, что строки растра перевёрнуты. После этого необходимо освободить участок системной памяти, где хранится битовый массив:

// Освободили битовый массив!
pBits := nil;


Вот и всё, можно приступать к отрисовке экрана.

Вообще такая схема объединения всех данных в один большой или несколько больших файлов оправдана в крупных программах и играх - там, где набор различных изображений достигает сотен штук. На этапе черновой разработки целесообразно загружать растры из отдельных файлов, а в конце, перед релизом программы, в процессе доводки и оптимизации, объединить всё в один большой файл. При этом придётся немного поработать в графическом редакторе, разместив отдельные растры оптимальным способом, не оставляя "пустых" мест. Также целесообразно подготовить массив типа TRect, который будет описывать область каждой картинки в этом растре, и пользоваться им в функции загрузки.


FASTFILE2[править | править код]

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

Чтобы не копировать каждый раз содержимое нового участка памяти на отдельную поверхность DirectDraw функцией StretchDIBits(), я решил все данные из памяти скопировать на одну большую поверхность DirectDraw, а потом копировать её содержимое по участкам на другие поверхности методом IDirectDrawSurface7.BltFast(). Казалось бы, такое двойное копирование - из памяти на общую поверхность, а потом с этой поверхности на отдельные поверхности - довольно долгий процесс. Однако если память видеокарты достаточно большая (32-64 Мб), можно позволить программе разместить все созданные поверхности в памяти видеокарты, и тогда копирование методом IDirectDrawSurface7.BltFast() будет происходить очень быстро. При большом объёме графических данных этот способ предпочтителен. К тому же данные на общей поверхности DirectDraw хранятся в нормальном, а не перевёрнутом виде, что облегчает программисту перенос.

Этот способ и демонстрирует данный проект. Всё остальное осталось без изменений.

Наконец, существует ещё один, наиболее эффективный путь. Можно не заниматься копированием растра с общей на отдельные поверхности, а переносить растр на дополнительный буфер прямо с общей поверхности.

Например:

g_pBackBuffer.BltFast(x, y, g_pMovie[frame], nil, DDBLTFAST_WAIT);

Однако можно третьим параметром указать общую data-поверхность, а четвертым - не nil, а область на этой поверхности:

g_pBackBuffer.BltFast(x, y, g_pDataSurface, arrayRect[FRAME_01], DDBLTFAST_WAIT);

Тогда можно не создавать отдельные поверхности и не заниматься копированием данных. Однако есть и недостатки. Например, память видеокарты должна быть достаточно большой - если памяти не хватит для размещения всей data-поверхности, DirectDraw разместит её в системной памяти, и процесс вывода изображения резко замедлится - вот вам и оптимизация! Также могут возникнуть проблемы с цветовыми ключами и корректным отображением спрайтов. В общем, решение половинчатое.


PRINTSCREEN[править | править код]

Заманчиво, когда в программе имеется возможность делать "снимки" экрана и сразу записывать их в файл. Этот пример ничем не отличается от предыдущих, за исключением того, что при нажатии на клавишу "Пробел" делается запись содержимого экрана в файл screen.bmp. Функция, которая проделывает эту работу, находится в файле pscreen.pas. Рассмотрим её.

Первым делом создаётся новый файл или открывается для перезаписи старый:

// создаём файл с заданным именем, в него будет производиться запись
hFile := CreateFile(szFileName, GENERIC_WRITE, FILE_SHARE_READ, nil, CREATE_ALWAYS, 0, 0);
if hFile = INVALID_HANDLE_VALUE then
begin
  CloseHandle(hFile);
  exit;
end;

// Затем нам необходимо получить данные о поверхности(здесь в функцию передан
// дополнительный буфер):

// подготавливаем структуру TDDSurfaceDesc2
ZeroMemory(@ddsd2, SizeOf(TDDSurfaceDesc2));
ddsd2.dwSize := SizeOf(TDDSurfaceDesc2);

// получаем формат поверхности
pSurface.GetSurfaceDesc(ddsd2);
dwWidth := ddsd2.dwWidth;
dwHeight := ddsd2.dwHeight;
dwBPP := ddsd2.ddpfPixelFormat.dwRGBBitCount;

// Структура ddsd2 используется дополнительно в методе
// Lock() поверхности. Заблокировав поверхность, можно обратится к её содержимому
// для чтения данных:

// блокируем поверхность DirectDraw
if (FAILED(pSurface.Lock(nil, ddsd2, DDLOCK_WAIT, 0))) then
  Exit;


Затем необходимо выделить достаточное количество памяти под массив пикселей. Число три в конце выражения - это потому, что вывод будет осуществляться в 24-битный файл:

pPixels := PByte(GlobalAllocPtr(GMEM_MOVEABLE, dwWidth * dwHeight * 3));


Затем начинается главное. Т. к. формат пикселя поверхности в каждом из графических режимов различается, необходимо предусмотреть все особенности размещения данных. Бессмысленно подробно описывать все операции - они запутанны и сложны. Мне понадобилось некоторое количество времени, чтобы правильно перевести все операции с указателями с языка C++ в контекст Object Pascal. Операции с указателями на этом языке получаются довольно путаными, малейшая оплошность приводит к тому, что обычно в файл записывается не тот участок памяти (получается мешанина из пикселей), или запись вообще не происходит. Обратите внимание на такую строку:

pixel := PDWord(DWord(ddsd2.lpSurface) + i * 4 + j * ddsd2.lPitch)^;

Здесь определяется цвет нового пикселя поверхности. ddsd2.lpSurface - это указатель на начало данных поверхности, а ddsd2.lPitch - шаг поверхности, учитывать его нужно обязательно. После того, как данные скопированы в массив, поверхность обязательно нужно разблокировать. Теперь можно начать запись данных в файл.

Для начала необходимо вручную подготовить структуры BITMAPFILEHEADER и BITMAPINFOHEADER. В последней надо указать ширину и высоту растра, а также разрядность пикселя. Тип сжатия должен быть BI_RGB - т. е. без сжатия. После этого с помощью API-функций Windows последовательно в файл записываются структуры BITMAPFILEHEADER, BITMAPINFOHEADER и далее - подготовленные данные из памяти. После записи файл необходимо закрыть, а память - освободить:

// закрываем файл
CloseHandle(hFile);
pPixels := nil;


Функция получилась громоздкой, согласен. Однако иного способа не существует - во всём виноват формат поверхности. Кстати, я не учёл режим в 256 цветов - опять же по причине анахронизма.

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

Скачать примеры: DirectX3.zip (95K)


Author: Виктор Кода
Source: Взято с http://delphiworld.narod.ru
ID: 03918