Привет всем, кто интересуется программированием под DirectX на языке Object Pascal!
Как и обещал, я продолжаю искать новый материал по DirectX, переводить его на язык Object Pascal и представлять всеобщему вниманию. Недавно у меня появилась идея снятия скриншотов с экрана DirectDraw-программы и записи изображения в простой bmp-файл - некоторые игры позволяют это делать, и я решил последовать их примеру. Потом я наткнулся на другой интересный материал - речь шла о загрузке изображения из bmp-файла без использования функции LoadImage(). Поэтому тема статьи всецело посвящена работе с bmp-файлами на "низком уровне". Замечу, что это немного сложные вещи, но мы ведь сложностей не боимся, правда? Иначе непонятно, зачем тогда заниматься изучением DirectX вообще.
| Замечания |
Немного изменилась реализация модуля ddutils.pas - теперь функция CreateSurface() не требует адрес главного интерфейса IDirectDraw, а создаёт и уничтожает локальный интерфейс. Возможно, более практично с точки зрения Pascal-программирования было бы объявить глобальный для модуля ddutils.pas интерфейс IDirectDraw, а для создания и удаления интерфейса воспользоваться секциями initialization и finalization модуля.
Также немного изменился стиль написания программ, теперь он совсем грешит С-подобным кодом :-)
| FASTFILE1 |
Естественно, всё это отрицательно сказывается на скорости загрузки - метод получается простым, но не самым эффективным. Поэтому часто программисты пишут собственные быстрые функции для загрузки файлов какого-то определённого формата. В этом примере я реализовал отдельную функцию, которая предназначена для загрузки данных из 24-битного беспалитрового файла формата bmp.
Прежде чем приступить к рассмотрению работы функции, необходимо в общих чертах представить себе, каким образом записывается информация в bmp-файле. На рис. 1 показана структура беспалитрового 24-битного файла. Хранящийся на диске файл 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? Есть два пути: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 |
Чтобы не копировать каждый раз содержимое нового участка памяти на отдельную поверхность 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 |
Функция, которая проделывает эту работу, находится в файле 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) Найдено в Королевстве Дельфи!
Последние комментарии