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-файла методом, отличным от того, что применялся в предыдущих уроках. Напомню, как в общих чертах выглядела схема загрузки ранее:
- С помощью функции LoadImage() загружался растр и нам сообщался идентификатор загруженного растра в виде переменной типа HBITMAP;
- Функцией SelectObject() в контекст-источник выбирался созданный растр;
- Методом IDirectDrawSurface7.GetDC() создавался GDI-совместимый дескриптор контекста-приёмника и осуществлялась блокировка поверхности;
- Функцией GDI BitBlt() содержимое контекста-источника копировалось на контекст-приёмник.
- Методом IDirectDrawSurface7.ReleseDC() удалялся созданный контекст и осуществлялась разблокировка поверхности.
В чём недостатки такого подхода? В универсальности. Не скажу, что функция LoadImage() очень медленная, но и очень быстрой она не является. Во всяком случае, программисты, писавшие её, не ставили своей задачей обеспечить максимальную скорость загрузки. Посмотрите справку по этой функции (файл win32sdk.hlp) - обилие параметров и констант, задаваемых при вызове, наводят на мысль о том, что она довольно "тяжеловесна". В частности, сказано, что с её помощью можно загружать не только растры из bmp-файлов разных форматов (в их число входят монохромные, 16- и 256-цветные палитровые файлы, а также беспалитровые 24-битные файлы), но и файлы иконок Windows и даже файлы, содержащие изображения курсоров.
Естественно, всё это отрицательно сказывается на скорости загрузки - метод получается простым, но не самым эффективным. Поэтому часто программисты пишут собственные быстрые функции для загрузки файлов какого-то определённого формата. В этом примере я реализовал отдельную функцию, которая предназначена для загрузки данных из 24-битного беспалитрового файла формата bmp.
Прежде чем приступить к рассмотрению работы функции, необходимо в общих чертах представить себе, каким образом записывается информация в bmp-файле. На рис.1 показана структура беспалитрового 24-битного файла.
Рис.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? Есть два пути:
- Воспользоваться методами Lock() и Unlock() интерфейса IDirectDrawSurface7, и осуществлять прямое копирование данных функцией CopyMemory(). Это решение оптимально по скоростным характеристикам, но уж очень сложное: необходимо учитывать абсолютно все нюансы формата поверхности, на которую копируются данные - а их очень много, ведь формат меняется в зависимости от глубины цвета текущего видеорежима - 8, 16, 24, 32 бит. К тому же, выигрыш в этом случае может оказаться совсем небольшим.
- Использовать функцию 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