Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.16.29.209] |
|
Сообщ.
#1
,
|
|||||
|
Компоненты DirectX обеспечивают не только прямой доступ к устройствам компьютера: они избавляют программиста от тяжелого труда программирования на языке Assembler, решают проблему с драйверами устройств, незаменимы при создании трёхмерных и сетевых игр. До появления DirectX хороших сетевых игр было не так уж много по причине трудности их программирования. На сегодняшний день последняя версия DirectX носит порядковый номер 8.1, что позволяет задуматься, сколько утекло времени. Библиотека полностью обеспечивает разработчика всем необходимым инструментарием для разработки качественных игр, поддерживает все современные аппаратные средства и стала де-факто стандартом в игровой индустири игр для персонального компьютера IBM PC.DirectX 8 состоит из следующих компонентов:
Я не ставил своей целью переписать главы книг по DirectX при составлении сопроводительных лекций - поэтому дал только самые общие разъяснения, которые, надеюсь, помогут ответить на немые вопросы новичков. Предполагается, что читатель владеет Delphi и умеет программировать на Object Pascal. Если вы до сих пор программировали только с использованием VCL, переход на API-подобный код может оказаться болезненным - забудьте про размещение компонент на форме подобно набору DelphiX и т. п. - сразу скажу, всё это глупости.Иногда я говорю "необходимо передать адрес переменной" - но в некоторых случаях символ @ перед переменной не ставится, а иногда он есть. Смотрите интерфейсы функций в заголовочных файлах - вы встретите зарезервированные слова var, out и const, которые указывают на необходимость работы с адресами.
Скачать примеры: DirectX_Smpl.zip (125 K) Иногда неоходимо узнать, какое оборудование установлено в компьютере, удовлетворяет ли оно предъявленным требованиям и т. д. Например, к компьютеру может быть подключено несколько игровых контроллеров, и нужно дать возможность игроку выбрать для управления одно из них, а не первое попавшееся. К тому же, их вообще может не быть, и об этом тоже надо знать. Если установлены две звуковые карты, то одна из них може иметь аппаратную поддержку DirectX, а другая - нет. Одним словом, нужно иметь список наличествующего оборудования. DirectX и его компоненты могут обеспечить такое перечисление разных устройств для их выбора. В этом проекте в простой форме осуществляется перечисление установленных видеоадаптеров, звуковых карт и устройств ввода. Надеюсь, вы разберётесь с этими возможностями DirectX самостоятельно. Прежде чем начать знакомиться с примерами по DirectInput, желательно изучить общий механизм работы этого API, т .к. он одинаков в общих чертах для всех устройств ввода. Впоследствии я не буду останавливаться на повторяющихся приёмах, т. к. объясню их суть здесь. Этот пример демонстрирует вывод изображения средствами DirectDraw. Он построен без использования VCL, но я надеюсь это не помешает вам. Одним из достоинств компоненты DirectSound является то, что она имеет прямой (ну, почти прямой) доступ к аппаратному обеспечению звуковой карты. Среди интересных возможностей следует выделить размещение данных в памяти звуковой платы, использование аппаратного микширования звука, возможность создавать объёмный звук, используя специальные алгоритмы (это нужно для 3D-игр) - всё это мультимедиа-средства Windows обеспечить не в состоянии |
Сообщ.
#2
,
|
|||||||||||||||||||||||||||||||||||||||||
|
Небольшое отступление Прошёл месяц с тех пор как я написал первую часть ( http://www.delphikingdom.com/helloworld/directx.htm ) статьи по использованию DirectX в среде Delphi. У меня накопилось ещё несколько примеров, которые, надеюсь, послужат наглядным руководством для начинающих.Прежде, чем описывать предложенные общему вниманию программы, хочу сообщить о некоторых изменениях в их коде по сравнению с примерами первой статьи, чтобы не останавливаться впоследствии на этих мелких деталях. Вызовы _AddRef() и _Release() больше не используются - в конце концов я посчитал это бессмысленной тратой времени при наборе кода. К тому же, как выяснилось, что вызов именно этих методов привёл к неработоспособности одного из примеров предыдущей статьи - если кто интересовался, знает, что это был пример опроса клавиатуры с использованием DirectInput. После удаления вызовов программа стал работать корректно. По-видимому, имело место некорректное взаимодействие с драйвером клавиатуры. Выражение вида if COM-объект <> nil then COM-объект := nil переписано с использованием процедуры следующего вида:procedure SAFE_DELETE( p: TInterfacedObject ); begin if p <> nil then p := nil end;Теперь достаточно написать SAFE_DELETE( @COM-объект ) - может, это покажется и излишним, но поверьте, в более крупных программах, где надо удалить 15-20 COM-интерфейсов, это становится удобным и сокращает код. Все эти соображения навеяны под влиянием примеров из MS SDK. Кстати, может, кто-то несогласен с правильностью описанной процедуры? Модуль basedd8.pas в проектах для DirectDraw переименован в basedd7.pas - всё-таки DirectDraw - это часть DirectX 7, в версий 8 он как таковой отсутствует. В функции LoadFiles() добавлен вызов DeleteObject() - как известно, после работы объекты GDI надо удалять, иначе они поглощают ресурсы системы. В данном случае именно такой объект создаётся при вызове функции GDI LoadImage() - казалось бы, тип HBITMAP - это всего лишь переопределение типа LongWord, копилятор самостоятельно удалит переменную этого типа после выхода из функции. На самом деле GDI при вызове LoadImage() (и других подобных функций) создаёт ресурс GDI и резервирует для него часть системной памяти, а переменная hBmp - всего лишь идентификатор этого ресурса в общем списке ресурсов Windows. Поэтому в процессе выполнения программы будет удаляться только идентификатор, а ресурс, на который он указывает, будет <висеть> в памяти. Именно поэтому следует вызвать DeleteObject() для удаления объекта GDI. В предыдушем примере я не сделал этого по причине недосмотра.Большая часть примеров в этой статье предназначена для работы с DirectDraw - как мне кажется, наиболее востребованному элементу DirectX (кроме, естественно, Direct3D). Надеюсь, мой стиль написания кода программ покажется удовлетворительным - он почти во всём подобен стилю, который использовали составители DirectX SDK. Вообще, многие пишут, как курица лапой - и предлагают свои творения на всеобщее обозрение. Ещё полезно заглянуть на страницу в нашем уважаемом Королевстве - http://www.delphikingdom.com/article/tassel.htm - это классика.Почему я не рекомендую использовать DelphiX Хочется поделиться с новичками своим мнением по поводу компонетов DelphiX и почему я не рекомендую их использовать.С одной стороны, DelphiX - это удобно - нет необходимости выполнять утомительный набор методов DirectX и длинных, как многоступенчатая ракета, констант наподобие DDENUMSURFACES_CANBECREATED. Однако давайте посмотрим - используется что-нибудь подобное в С++? Я не могу исследовать всю Сеть в поисках овета на такой вопрос, но, думается - нет. Почему? Такие наборы классов - это нестандартный подход. Допустим, вы потратили изрядно своего времени и досконально изучили DelphiX. Так вот, изучив всё ЭТО, вы в итоге не изучили сам DirectX. Второе - изученные классы обладают многим и позволяют писать реальные программы, но всё равно этот подход очень негибок - вы ограничены тем, что уже сделано. На этот счёт у меня есть веский аргумент - это Direct3D. Вот тут DelphiX уж точно не даст развернуться как следует - и это характерно и для других компонент DirectX, пусть и в меньшей мере. Третье - немного, но снижается быстродействие. Четвёртое - в классах DelphiX кроме непосредственных вызовов методов интерфейсов DirectX используются ещё и собственные функции - кто даст гарантию, что в них нет ошибок?В конце концов, именно за такие вот <примочки> Delphi не в почёте у С-программистов - они попросту надсмехаются над такими методами разработки программ. К сожалению, должен к ним присоединиться и я. Как же так, возмутятся многие. Компонентный подход - это ведь основа основ Delphi! Согласен, использование TMemo или TComboBox - это действительно удобный подход, да что там - превосходный, отличный подход! Но вот в случае с DirectX или чем-то подобным использовать такие средства разработки крайне нежелательно. Как бы вы отнеслись к компоненту TOpenGL? Или TWin32API? Вот так-то. DelphiX можно использовать как источник разных идей по реализации того или иного эффекта - перенося всё это в свою программу в виде отдельных функций или собственноручно написанных классов. Так что изучайте прямой API - для уверенности в завтрашнем дне и в собственной квалификации.
В предыдущей статье я предложил вниманию только один пример для DirectDraw - простая реализация вывода спрайта поверх фона в задний буфер и перенос построенной картинки на экран. Спрайт не мог выйти за пределы экрана - и это не спроста. Если убрать досадное ограничение, то выяснится, что как только часть спрайта выходит за границы экрана (а фактически границы заднего буфера), то сразу исчезает полностью. Возвращение в прежнюю позицию восстанавливает вывод. Когда я впервые столкнулся с этой проблемой, меня это неприятно поразило. Оказалось, что необходимо использовать интерфейс отсечения - IDirectDrawClipper. Однако он предназначен только для оконных приложений, в полноэкранном режиме от него нет никакого проку. Как уже было упомянуто, рекомендую программировать только полноэкранные приложения как наиболее быстродействующие, и забыть об оконных. Так как же быть - тупик? К сожалению, даже в SDK нет примеров решения этого вопроса. Впрочем ответить на него не так уж сложно - нужно лишь понять, что хочет DirectDraw и как это преподнести.Корень проблемы в том, что как только при копировании методом BltFast() часть поверхности выходит за край той поверхности, на которую она копируется (обычно задний буфер), вывод не осуществляется. В чём причина такого нелепого ограничения - думается, опять же в обеспечении наибольшего быстродействия. Например, вы планируете создать игру типа Tetris, а не скроллинговую стрелялку, и все ваши спрайты будут двигаться только в пределах экрана - но вот DirectDraw всё равно пришлось бы проверять их выход за границы, даже при отсутствии в этом необходимости. Хотя эту проблему можно было бы решить с помощью флагов при создании конкретной поверхности, но Microsoft этого не сделала. Ну что же, сделаем за неё эту работу. Обратите внимание на четвёртый параметр метода IDirectDrawSurface7.BltFast() - это адрес структуры типа TRect. Для чего он нужен? Как известно, назначение струтуры TRect в GDI API - указание положения и размера какой либо области путём задания левого верхнего и правого нижнего угла. Так вот, эта структура позволяет указать DirectDraw о необходимости вывести не всё изображение спрайта, а лишь его часть: Воспользуемся этой структурой и будем вместо всего спрайта выводить какую-то его облать - ту, которая будет видна на экране, а невидимая будет находиться вне области, описываемой структурой TRect. Т. о. будет создаваться лишь иллюзия пребывания спрайта за пределами экрана. Например, спрайт выходит за границы экрана, как это показано на самом первом рисунке. Тогда выводимая область должна быть такой (показана красным цветом): Теперь повоображайте и нарисуйте на бумаге, как будут выглядеть выводимые части спрайта при различных его положениях за границами экрана.Вот код, ответственный за вывод части изображения: // Предполагаем, что края спрайта не выходят за границы экрана SetRect( rRect, 0, 0, SPRITE_WIDTH, SPRITE_HEIGHT ); // Проверяем выход краёв, и если такая ситуация имеет место, то корректируем // положение области копирования на поверхности спрайта if nX < 0 then rRect.Left := - nX ; if nY < 0 then rRect.Top := - nY; if nX + SPRITE_WIDTH > SCREEN_WIDTH then rRect.Right := SCREEN_WIDTH - nX; if nY + SPRITE_HEIGHT > SCREEN_HEIGHT then rRect.Bottom := SCREEN_HEIGHT - nY;Где nX и nY - координаты левого верхнего угла спрайта. При выводе надо не забыть скорректировать их: nX + rRect.Left, nY + rRect.TopВот и всё. Запустите проект на выполнение - вы увидите, что теперь свободно отбражается даже часть спрайта. Выведите его полностью за пределы экрана - начнёт жужжать встроенный динамик компьютера - эта возможность введена для проверки правильности алгоритма. Кстати, если необходимо вывести всю поверхность изображения, вместо адреса структуры следует передать nil - как это сделано для фона. ScaleИногда при выводе может понадобится растянуть или сжать объект по осям или просто увеличить или уменьшить его - для подобных эффектов DirectDraw предоставляет метод IDirectDrawSurface.Blt(). Он является хотя и более медленным, чем BltFast() - однако при этом более функционален. Так вот, мы снова будем указывать с помощью структуры TRect область вывода изображения - но уже на поверхности-приёмнике данных. Изменяя её размеры, можно добиться пропорционального или непропорционального изменения масштаба изображения по осям X и Y. Думаю, нет надобности описывать действия которые происходят в процедуре OnDraw(). Замечу лишь, что на современных видеокартах с полной аппаратной поддержкой DirectDraw эффект масштабирования выглядит гораздо привлекательнее, чем на <ветеранах>. TransparentПри выводе изображения на экран нередко встаёт проблема затирания заднего фона, а говоря языком профессионалов - проблема вывода нерегулярных спрайтов. Нерегулярный спрайт - это обычный прамоугольный спрайт, который содержит маску для указания прозрачных участков изображения. Пиксели, принадлежащие этим участкам, при выводе игнорируются - создаётся иллюзия прозрачности. В GDI нет прямого способа вывести нерегулярный спрайт - для этого необходимо подготовить изображение и отдельно маску. В чём недостатки такого подхода? Их много. Во-первых, требуется иметь два изображения - это увеличивает объём данных в памяти и на диске. Во-вторых, скорость вывода данного изображения падает вдвое, а ведь GDI и без этого не славится своей скоростью. В третьих, это дополнительная забота того, кто готовит изображение в графическом редакторе. В своё время автор даже написал небольшую утилиту, которая создавала маску для выбранного изображения и записывала её в отдельный файл. Но теперь об этом можно забыть. DirectDraw предоставляет удобный инструмент для задания маски прозрачности. Цвета пикселей, которые игнорируются, называются <цветовыми ключами>. Каждая поверхность может иметь свои цветовые ключи, причём их может быть несколько.Следующий фрагмент кода создаёт и присоединяет к поверхности <цветовой ключ>, цвет которого - чёрный. var ddck: TDDCOLORKEY; ddck.dwColorSpaceLowValue := 0; ddck.dwColorSpaceHighValue := ddck.dwColorSpaceLowValue; pSprite.SetColorKey( DDCKEY_SRCBLT, @ddck );Для указания прозрачного цвета, как видно, используется структура TDDCOLORKEY. В её двух полях необходимо указать нижнюю и верхнюю границу диапазона <прозрачных> цветов. Замечу, что использование диапазона цветов возможно только в случае, если такая возможность поддерживается аппаратно. Поэтому лучше ограничиться каким-либо одним цветом, как это сделано выше. После заполнения структуры TDDCOLORKEY необходимо вызвать метод IDirectDrawSurface7.SetColorKey(), где первый параметр - один из возможных флагов, второй - адрес структуры TDDCOLORKEY. Обычно используется флаг DDCKEY_SRCBLT, который указывает, что при копировании изображения будет использоваться цветовой ключ поверхности-источника. Другие флаги можно узнать из справочной службы DirectX SDK. Теперь о главном. В приведённом выше фрагменте кода в качестве маски задаются пиксели чёрного цвета. Как известно, нулевое значение обозначает отсутствие цвета во всех графических режимах - 16 цветов, 256, 65535 и т.д. Поэтому можно смело присваивать 0 для чёрной маски в любом режиме. Однако, предположим, нам надо задать цветовой ключ в виде чистого синего цвета. Для 24- и 32-битного режима это можно сделать с помощью макроса (функции) из модуля windows.pas:function RGB(r, g, b: Byte): COLORREF; begin Result := (r or (g shl 8) or (b shl 16)); end;Зарезервированное слово shl относится к сдвиговым операциям и сдвигает содержимое на указанное значение влево. Т.к. в этих графических режимах каждый из трёх цветов (красный, синий и зелёный) кодируется одним байтом, то значение каждого параметра функции должно лежать в пределах от 0 до 255. Вот как это можно представить графически: Так, для задания цветового ключа в виде чистого синего цвета необходимо написать так:ddck.dwColorSpaceLowValue := RGB( 0, 0, 255 ); ddck.dwColorSpaceHighValue := ddck.dwColorSpaceLowValue;Ну и в том же духе, в полном соответствии с теорией цвета. А теперь попробуйте задать цветовой ключ для 16-битового режима. Ничего не получится. Почему? Дело в том, что цвет пикселя хранится в ячейке длиной в 16 бит, а цветовых составляющих - 3, появляется лишний бит, который чаще отдаётся зелёному цвету, а иногда просто не используется. Формат, где теряется лишний бит, обозначается 5-5-5 (на каждую цветовую составляющую по пять бит, а не одному байту), другой формат обозначается 5-6-5 (на зелёную составляющую выделяется 6 бит ). Понятно, что задание цвета с помощью функции RGB() для таких форматов ни к чему ни приведёт. В своё время я довольно долго промучился с этой проблемой, тем более что в имеющейся литературе ничего об этом не сказано. В конце концов решил, что необходимо написать аналогичную к RGB() функцию, но об этом немного позже.Давайте сначала выясним, какой же формат использует установленная на нашем компьютере видеокарта. DirectDraw позволяет узнать это с помощью функции IDirectDrawSurface7.GetPixelFormat(). Единственным параметром необходимо передать адрес структуры TDDPIXELFORMAT. Вот фрагмент соответствующего кода: var ddpf: TDDPIXELFORMAT; ZeroMemory( @ddpf, SizeOf( TDDPIXELFORMAT ) ); ddpf.dwSize := SizeOf( TDDPIXELFORMAT ); pSprite.GetPixelFormat( ddpf );Формат цветовых составляющих описывается в полях dwRBitMask, dwGBitMask и dwBBitMask структуры TDDPIXELFORMAT - но только в том случае, если битовое поле dwFlags содержит флаг DDPF_RGB - признак того, что поверхность создана в RGB-режиме. Значения полей dwRBitMask, dwGBitMask и dwBBitMask для режимов с разной глубиной палитры описываются в разделе dwRBitMask, dwGBitMask и dwBBitMask справочной службы DirectX SDK:
Запустите готовое приложение GetPixFormat из каталога DXCommon - и посмотрите, какой формат поверхности использует ваша карта в 16-битовом режиме. Скажу, что на компьютере с видеоакселератором GeForce 2 MX 420 получались значения из самой верхней ячейки - и это соответствует формату 5-6-5. По-моему, именно такой формат принят во всех современных видеокартах (заметьте, что во второй ячейке таблицы составляющие R и B переставлены местами). А вот, например, дедушка S3 Trio 3D/2X использует формат, описанный в нижней ячейке - опытным путём установлено, что это 5-5-5. Вот как должен быть переписан макрос для формата 5-6-5:function RGB565( r, g, b: Byte ): COLORREF; begin Result := ( ( r shl 11 ) or ( g shl 5 ) or b ); end;Графически битовая маска может быть представлена так: А вот как должен выглядеть макрос для формата 5-5-5: function RGB555( r, g, b: Byte ): COLORREF; begin Result := ( ( r shl 10 ) or ( g shl 5 ) or b ); end; Графически битовая маска может быть представлена так: Как видно, последний бит не используется. Обратите внимание, что группа битов, отвечающих за красную и синию составляющую, в 16-битовом режиме поменялись старшинством. А формат, описанный во второй ячейке таблицы, наоборот, по старшинству схож с 24- и 32-битовым режимами. Подозреваю, что используется этот формат довольно редко.Естетственно, что теперь максимальное значение, передаваемое в макросы (функции) RGB565() и RGB555(), соответствует значению 31, а для задания читого зелёного цвета в режиме 5-6-5 необходимо указать RGB565( 0, 63, 0 ), т. к. битов 6. Для того, чтобы наша DirectDraw-программа без проблем работала в обоих форматах, необходимо проверить текущий формат, запомнить его и при задании цветового ключа для поверхности вызвать соответствующий макрос. Всё это и делается в приложении Transparent - надеюсь при его разборе у вас не возникнет проблем. Не забудьте при копировании методом BltFast() указать флаг DDBLTFAST_SRCCOLORKEY. FpsЕщё один полезный пример - вывод текста на поверхность DirectDraw - в виде значения fps. Сама компонента не обладает такими средствами - DirectDraw изначально создавался лишь для максимально быстрого копирования одного изображения на другое. Для вывода текста необходимо использовать GDI. Как неоднократно упоминалось, GDI очень медленнен, и вывод текста - одна из функций, которая серьёзно может <притормозить> DirectDraw-программу. Поэтому необходимо пользоваться этой функцией как можно реже. Для взаимодействия DirectDraw c GDI введён простой метод IDirectDrawSurface7.GetDC(). Получив контектс, можно спокойно чертить в нём всеми мыслимыми функциями GDI. Метод IDirectDrawSurface7.ReleaseDC() переносит содержимое контекста в область памяти, занятую поверхностью DirectDraw и удаляет контекст. Откройте файл проекта fps.dpr. Т. к. функция TextOut() уже занята, функцию, отвечающую за вывод текста, пришлось назвать менее звучно - OutText(). Я не буду подробно описывать её, надеюсь, всё понятно. Для ускорения работы программы я поступил так: для вывода текста используется отдельная поверхность - именно на неё и выводится текст средствами GDI. Затем всё время поверхность просто копируется на задний буфер - это осуществляется гораздо быстрее, чем постоянный вывод текста на задний буфер, а когда появляется необходимость изменить текст - он снова выводится на нашу отдельную поверхность. Потребность изменить текст появляется лишь раз в секунду.Для вызова OutText() я использовал мультимедиа-таймер Windows. Значение fps наращивается при каждом построении кадра и обнуляется после вызова OutText(). И последнее. По-видимому, в операционной системе Windows 2000 функции GDI должны работать быстрее, т. к. эта ОС полностью 32-х разрядная. Но всё же рекомендую пользоваться описанным выше подходом. TextЕщё один пример вывода текста - но уже на задний буфер. Добавлен мною для полноты темы. Текст заданных размеров постоянно выводится на задний буфер без поверхности-посредника. При выводе текста я столкнулся с одной проблемой - это сглаживание краёв символов. В модуле windows.pas описана константа ANTIALIASED_QUALITY, но её задание в параметре fdwQuality функции CreateFont() ни к чему ни привело. Может быть, в Windows 9x и МЕ это значение не используется? Во всяком случае, константы ANTIALIASED_QUALITY и NONANTIALIASED_QUALITY в справке Delphi Help не описаны. Sound Эта программа - прямое продолжение моего первого примера по использованию DirectSound. Введено ряд усовершенствований:
var sound: TWave; sound := TWave.Create(); sound.OpenWaveFile( 'wavefile.wav' ); sound.Play(); Правда, просто? GetDXVer и GetDXVerSetupЯ решил заглянуть в некоторые области DirectX, до которых руки многих авторов книг по DirectX попросту <не доходят>. Например, написание программы для определения текущей версии DirectX. Иногда это может быть очень полезно. Первый пример, который я предлагаю вашему вниманию - это GetDXVer. Это аналог из DirectX SDK для Visual C++. Функция GetDXVersion() ответственна за получение намера текущей версии DirectX. Каким образом она действует? Механизм прост, но достаточно громозд. Сначала загружается нужная динамическая библиотека из комплекса DirectX, например DDRAW.DLL или DINPUT.DLL. Затем получают адреса функций, которые экспортируют эти библиотеки - это <создающие> функции наподобие DirectDrawCreate() или DirectInputCreateA(). Затем при помощи этих функций и создаются нужные интерфейсы вроде IDirectDraw и т.п. Если на каком-то шаге происходит сбой, это означает, что данная функция или интерфейс не поддерживаются. Зная, в какой версии появился тот или иной интерфейс, можно выяснить текущую версию DirectX.Ещё одна функция, GetDXRegVersion(), извлекает полный порядковый номер из реестра Windows. Кстати, эта функция может в принципе читать любой строковый параметр из реестра и делает ненужным использование класса TRegistry, что очень важно, если мы хотим получить маленький по размерам исходный модуль. Пример имеет два недостатка:
Недостаток приведенного метода в том, что вам придётся постоянно <таскать> библиотеку dsetup.dll вместе с исходной программой. ЗаключениеНаписание подобных статей - хороший стимул к тщательному изучению DirectX. Скажу, что чем больше работаю с этим комплексом, тем больше разных нюансов всплывает на поверхность. Надеюсь, что мои усилия хоть как-то помогут остальным желающим освоить этого игрового <монстра>. Я надеюсь продолжить изучение DirectX и как только получится создать что-то стоящее, попробую поделиться сделанным с остальными. Напоследок хочу выразить особую благодарность Антону Ржешевскому за его дельные советы в освоении DirectX и не только.Скачать примеры: Samples2.Zip (106 K) |
Сообщ.
#3
,
|
|||||
|
Привет всем, кто интересуется программированием под DirectX на языке Object Pascal! Как и обещал, я продолжаю искать новый материал по DirectX, переводить его на язык Object Pascal и представлять всеобщему вниманию. Недавно у меня появилась идея снятия скриншотов с экрана DirectDraw-программы и записи изображения в простой bmp-файл - некоторые игры позволяют это делать, и я решил последовать их примеру. Потом я наткнулся на другой интересный материал - речь шла о загрузке изображения из bmp-файла без использования функции LoadImage(). Поэтому тема статьи всецело посвящена работе с bmp-файлами на "низком уровне". Замечу, что это немного сложные вещи, но мы ведь сложностей не боимся, правда? Иначе непонятно, зачем тогда заниматься изучением DirectX вообще.
Немного изменилась реализация модуля ddutils.pas - теперь функция CreateSurface() не требует адрес главного интерфейса IDirectDraw, а создаёт и уничтожает локальный интерфейс. Возможно, более практично с точки зрения Pascal-программирования было бы объявить глобальный для модуля ddutils.pas интерфейс IDirectDraw, а для создания и удаления интерфейса воспользоваться секциями initialization и finalization модуля. Также немного изменился стиль написания программ, теперь он совсем грешит С-подобным кодом :-)
Естественно, всё это отрицательно сказывается на скорости загрузки - метод получается простым, но не самым эффективным. Поэтому часто программисты пишут собственные быстрые функции для загрузки файлов какого-то определённого формата. В этом примере я реализовал отдельную функцию, которая предназначена для загрузки данных из 24-битного беспалитрового файла формата bmp. Прежде чем приступить к рассмотрению работы функции, необходимо в общих чертах представить себе, каким образом записывается информация в bmp-файле. На рис. 1 показана структура беспалитрового 24-битного файла.Рис.1. Структура файла BMP, не содержащего палитру 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, которыи будет описывать область каждой картинки в этом растре, и пользоваться им в функции загрузки.
Чтобы не копировать каждый раз содержимое нового участка памяти на отдельную поверхность 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 разместит её в системной памяти, и процесс вывода изображения резко замедлится - вот вам и оптимизация! Также могут возникнуть проблемы с цветовыми ключами и корректным отображением спрайтов. В общем, решение половинчатое.
Функция, которая проделывает эту работу, находится в файле 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) |
Сообщ.
#4
,
|
|
|
Оригиналы статей были взяты из:
DirectX для начинающих. Часть 1 DirectX для начинающих. Часть 2 DirectX для начинающих. Часть 3 |