DirectX для початківців. Зчитування і запис


Автор: Віктор Коду

Привіт всім, хто цікавиться програмуванням під 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-бітного файлу.


Рис.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)Зауважу, що це трохи складні речі, але ж ми труднощів не боїмося, правда?
У чому недоліки такого підходу?
У чому він полягає?
Як же завантажити ці дані на поверхні DirectDraw?

Дополнительная информация

rss
Карта