Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум на Исходниках.RU > Hello, World! > DirectX для начинающих


Автор: Song 09.11.03, 10:57
[doHTML]<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
Введение</B></FONT></TD></TABLE></P>
<IMG SRC="http://www.delphikingdom.ru/images/author_drx.jpg" WIDTH="89" HEIGHT="120" HSPACE="10" VSPACE="10" BORDER="0" ALIGN="RIGHT" ALT="">
Фанаты игр часто встречаются с аббревиатурой "DirectX". На упаковках игр для Windows 95 она появилась в конце 1995 года примерно одновременно с выходом самой операционной системы. С тех пор качество игр резко ушло вверх, достигнув небывалых высот. Что же такое DirectX? Это набор специальных API, которые предоставляют работающей программе прямой доступ к аппаратной части компьютера, обеспечивая наивысшее быстродействие при выводе графики, звука, получения данных от устройств ввода и т. д. Сразу надо сказать, что библиотека создавалась исключительно для игр, т. к. именно они требуют от аппаратных средств все 100% производительности. Позднее, с выходом новых версий, DirectX нашёл применение и в мультимедиа-области.
<P>
Компоненты DirectX обеспечивают не только прямой доступ к устройствам компьютера: они избавляют программиста от тяжелого труда программирования на языке Assembler, решают проблему с драйверами устройств, незаменимы при создании трёхмерных и сетевых игр. До появления DirectX хороших сетевых игр было не так уж много по причине трудности их программирования.
</P>
На сегодняшний день последняя версия DirectX носит порядковый номер 8.1, что позволяет задуматься, сколько утекло времени. Библиотека полностью обеспечивает разработчика всем необходимым инструментарием для разработки качественных игр, поддерживает все современные аппаратные средства и стала де-факто стандартом в игровой индустири игр для персонального компьютера IBM PC.
<P>DirectX 8 состоит из следующих компонентов:</P>
<DL>
<DT><STRONG>Direct3D</STRONG><DD>содержит высокоуровневый интерфейс Retained Mode позволяющий легко выводить 3-хмерные графические обьекты, и низкоуровневый интерфейс Immediate Mode, предоставляющий полный конроль над рендерингом. В восьмой версии реализована новая технология вершинных шейдеров, позволяющая поднять уровень реалистичности изображения в новых играх. По своей функциональности вплотную приближается к OpenGL 1.2.
<DT><STRONG>DirectDraw</STRONG><DD>предназначен для отображения двумерной анимации на экране монитора. Обеспечивает прямой доступ к видеоадаптеру, за счёт чего достигается быстрый вывод графики, по скорости на порядок превышающий GDI.
Эти два компонента в восьмой версии объединены в один - DirectXGraphics. Однако, лично я не почувствовал сколь-нибудь характерных изменений (в DirectDraw API). Всё это скорее похоже на маркетинговый ход.
<DT><STRONG>DirectSound</STRONG><DD>создан для прямого доступа к аппаратной части звуковых плат, чтобы избавить воспроизведение звука от "тормозов", которые возникают при работе с Win32 API.
<DT><STRONG>DirectMusic</STRONG><DD> работает с музыкальными командами, посылаемыми звуковой карте.
DirectMusic и DirectSound восьмой версии объединены в один компонент - <STRONG>DirectXAudio</STRONG>.
<DT><STRONG>DirectInput</STRONG><DD> эта компонента отвечает за любые устройства ввода, например, аналоговые или цифровые джойстики, рули и педали, ручку управления полётом, световой карандаш или Touch-Screen. Некоторые возможности компоненты можно реализовать средствами Win32 API, получив такой же быстрый доступ, но в некоторых областях DirectInput незаменим (поддерживает технологию force-feedback и нестандартные устройства ввода).
<DT><STRONG>DirectPlay</STRONG><DD>упрощает жизнь программиста, решившегося добавить в своей программе возможность совместной работы (игры) по сети или по модему (это наверняка хорошо знакомо любому геймеру). В восьмой версии эта компонента значительно переработана и улучшена.
<DT><STRONG>DirectSetup</STRONG><DD>предназначен для установки DirectX.
<DT><STRONG>DirectShow</STRONG><DD> используется в мультимедиа-технологиях - можно выполнить высококачественое воспроизведение или захват видео и звука. Распространённость использования компоненты не слишком велика
</DL>
Все компоненты реализованы как внутрипоцессные серверы, т.е. размещаются в DLL. Архитектура доступа базируется на технологии COM. Значит, для использования DirectX нам понадобится среда программирования, поддерживающая обращение к функциям DLL из готовых программ и поддержка COM. Delphi легко реализует всё это, а значит, можно создавать такие же качественные программы, как и в Visual C++: понадобятся только файлы заголовков для DirectX 8.
<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
Как я изучал компьютерную графику </B></FONT></TD></TABLE></P>
Когда я только начинал заниматься графикой в Delphi, я применял всякие компоненты VCL вроде
<P></P>TBitmap и TImage. Надо сказать, дело шло: я самостоятельно разобрался с методами Canvas, догадался об необходимости буферизации при графическом выводе, сделал полноэкранную игру (правда, недоделал до конца :-(( ). Меня удивляло только то, что фигурки двигались довольно порывисто (разрешение составляло 800*600), и избавиться я от этого не мог. Постепенно я понял, что это не жизнь и начал изучать GDI. Через некоторое время я полностью закинул TBitmap'ы и мои программы работали исключительно через функции GDI. Но и здесь вывод был слишком медленнен и пригоден разве что при разрешении 400*300 и менее. Также большой проблемой было осуществление вывода нерегулярных спрайтов (картинок с прозрачными областями), т.к. в GDI для этого нужно подготовить маску и сам спрайт. Как ни странно, впервые с DirectX (DirectDraw) я познакомился в книге Михаила Краснова "OpenGL.Графика в проектах Delphi". На дискете с книгой поставлялся пример для изменения экранного разрешения с помощью DirectDraw и заголовочный файл. Начинал я с того, что просто заново набирал исходный текст в своих проектах, "заучивая" особенности DirectDraw API. Тогда всё это выглядело довольно странным и непонятным, даже QueryInterface() был полной загадкой. На самом деле всё становится достаточно прозрачно, но только после практики соответствующего объёма. DirectDraw решил проблемы медленного вывода, а также помог при выводе прозрачных спрайтов. Теперь мои программы работают максимально быстро, а уровень программирования достиг "профессионального", ведь я использую те же самые способы, что и разработчики серьёзных игровых программ.
<P></P>Когда я ещё не имел доступа в Сеть, работать пришлось с заголовочными файлами от Hiroyuki Hori для DirectX 6 - их я достал на CD-ROM. Но теперь я использую DirectX 8. Вообще, надо приучить себя двигаться в ногу со временем и использовать новейшие технологии, даже если не хочется "слезать" с насиженной старой. И хотя никаких скоростных преимуществ при использовании методов DirectDraw из SDK 8 по сравнению c SDK 5 или 6 уже, наверное, не получить, предпосылок к использованию старых версий нет. Тем более что Microsoft стремится сделать отличия между API разных версий минимальными, а новейшая runtime-часть DirectX установлена уже наверняка у 99% игроманов.
<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
На кого рассчитаны примеры</B></FONT></TD></TABLE></P>
Все примеры рассчитаны на новичков. Даже если вы переиграли во все мыслимые игры, они не сделали из вас гуру в DirectX. Мне очень не хотелось перегружать программные примеры необязательными подробностями, поэтому все они (за исключением Sound), максимально упрощены. Мне хорошо известно, насколько трудно разбирать программы, написанные кем-то другим, если они ещё и большие по размеру. Все возможности игрового комплекса не используются, в частности, не показано, как корректно выводить в DirectDraw спрайты, выходящие за границы экрана, как плавно гасить экран и т. д. Всё это только усложняет программу - суть, сердце механизма работы компоненты теряется в дебрях кода, обеспечивающего примеру лишь красочность.
<P></P>
Здесь вы не найдёте примеров для Direct3D и DirectMusic - я ещё сам только начинаю изучать трёхмерную графику (в основном пока OpenGL), а с MIDI-звуками связываться отказываюсь из-за примитивности технологии. DirectPlay тоже остался без внимания - это не мои интересы.
<P></P>
Всегда начинайте рассмотрение программы с самого начала - процедуры FormCreate() главной формы, это поможет начать изучение структуры алгоритма.
<P>Я не ставил своей целью переписать главы книг по DirectX при составлении сопроводительных лекций - поэтому дал только самые общие разъяснения, которые, надеюсь, помогут ответить на немые вопросы новичков.
</P>
Предполагается, что читатель владеет Delphi и умеет программировать на Object Pascal. Если вы до сих пор программировали только с использованием VCL, переход на API-подобный код может оказаться болезненным - забудьте про размещение компонент на форме подобно набору DelphiX и т. п. - сразу скажу, всё это глупости.

<P>Иногда я говорю "необходимо передать адрес переменной" - но в некоторых случаях символ @ перед переменной не ставится, а иногда он есть. Смотрите интерфейсы функций в заголовочных файлах - вы встретите зарезервированные слова var, out и const, которые указывают на необходимость работы с адресами.
</P>
<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
Что вам понадобится</B></FONT></TD></TABLE></P>
Все проекты строились в IDE Delphi 5. В версии 3 ошибки будут возникать уже на этапе компиляции, хотя бы из-за того, что в модуле DirectXGraphics.pas повсеместно используется тип LongWord, в третьей версии неопределённый. В версии 4 компилировать не пробовал, её у меня нет, возможно (не знаю точно), в среде не будут отображаться формы проектов.
Также необходимы файлы заголовков для DirectX. Хотя существует множество различных файлов, для того, чтобы следовать хоть какому-то стандарту, я использовал файлы заголовков, загруженные с сайта <A HREF="http://www.delphi-jedi.org/delphigraphics">www.delphi-jedi.org/delphigraphics</A>. Эти файлы разработаны энтузиастами и представляют собой переложение файлов DirectX SDK фирмы Microsoft на язык Object Pascal.
И, конечно же, нужны runtime-библиотеки DirectX 8. Их можно установить с компакт-диска DirectX SDK фирмы Microsoft. В крайнем случае возьмите какую-нибудь новую игру.
<P></P>
Если вы всё же решите изучать DirectX по моим примерам, то сперва прочитайте файлы readme.txt и BUGS.txt - в нём я описал некоторые возникшие проблемы и недоработки.

<P>Скачать примеры: <STRONG><A HREF="http://delphi.mtu-net.ru/zip/directx_smpl.zip">DirectX_Smpl.zip</A></STRONG> (125 K)<BR>
<STRONG>Описания примеров:</STRONG></P>
<LI><STRONG>Enum</STRONG><BR>
<BLOCKQUOTE>Иногда неоходимо узнать, какое оборудование установлено в компьютере, удовлетворяет ли оно предъявленным требованиям и т. д. Например, к компьютеру может быть подключено несколько игровых контроллеров, и нужно дать возможность игроку выбрать для управления одно из них, а не первое попавшееся. К тому же, их вообще может не быть, и об этом тоже надо знать. Если установлены две звуковые карты, то одна из них може иметь аппаратную поддержку DirectX, а другая - нет. Одним словом, нужно иметь список наличествующего оборудования. DirectX и его компоненты могут обеспечить такое перечисление разных устройств для их выбора.
<P></P>
В этом проекте в простой форме осуществляется перечисление установленных видеоадаптеров, звуковых карт и устройств ввода. Надеюсь, вы разберётесь с этими возможностями DirectX самостоятельно.</BLOCKQUOTE>
<LI><A HREF="directx_01.htm"><STRONG>DirectInput API</STRONG></A>
<BLOCKQUOTE>Прежде чем начать знакомиться с примерами по DirectInput, желательно изучить общий механизм работы этого API, т .к. он одинаков в общих чертах для всех устройств ввода. Впоследствии я не буду останавливаться на повторяющихся приёмах, т. к. объясню их суть здесь.</BLOCKQUOTE>
<LI><A HREF="directx_02.htm"><STRONG>Sprite</STRONG></A>
<BLOCKQUOTE>Этот пример демонстрирует вывод изображения средствами DirectDraw. Он построен без использования VCL, но я надеюсь это не помешает вам. </BLOCKQUOTE>
<LI><A HREF="directx_03.htm"><STRONG>Sound</STRONG></A>
<BLOCKQUOTE>Одним из достоинств компоненты DirectSound является то, что она имеет прямой (ну, почти прямой) доступ к аппаратному обеспечению звуковой карты. Среди интересных возможностей следует выделить размещение данных в памяти звуковой платы, использование аппаратного микширования звука, возможность создавать объёмный звук, используя специальные алгоритмы
(это нужно для 3D-игр) - всё это мультимедиа-средства Windows обеспечить не в состоянии</BLOCKQUOTE>
<P ALIGN="right"><A HREF="http://www.delphikingdom.ru/helloworld/asp/users.asp?ID=1146"><STRONG>Виктор Кода</STRONG></A></P>[/doHTML]

Автор: Song 09.11.03, 11:03
[doHTML]<P><STRONG>Небольшое отступление</STRONG></P>
Прошёл месяц с тех пор как я написал первую часть ( <A HREF="http://www.delphikingdom.com/helloworld/directx.htm">http://www.delphikingdom.com/helloworld/directx.htm</A> ) статьи по использованию DirectX в среде Delphi. У меня накопилось ещё несколько примеров, которые, надеюсь, послужат наглядным руководством для начинающих.
<P>Прежде, чем описывать предложенные общему вниманию программы, хочу сообщить о некоторых изменениях в их коде по сравнению с примерами первой статьи, чтобы не останавливаться впоследствии на этих мелких деталях.
</P>
Вызовы _AddRef() и _Release() больше не используются - в конце концов я посчитал это бессмысленной тратой времени при наборе кода. К тому же, как выяснилось, что вызов именно этих методов привёл к неработоспособности одного из примеров предыдущей статьи - если кто интересовался, знает, что это был пример опроса клавиатуры с использованием DirectInput. После удаления вызовов программа стал работать корректно. По-видимому, имело место некорректное взаимодействие с драйвером клавиатуры.
<P></P>
Выражение вида <TT>if COM-объект <> nil then COM-объект := nil</TT> переписано с использованием процедуры следующего вида:

<PRE>procedure SAFE_DELETE( p: TInterfacedObject ); begin if p <> nil then p := nil end;
</PRE>
Теперь достаточно написать SAFE_DELETE( @COM-объект ) - может, это покажется и излишним, но поверьте, в более крупных программах, где надо удалить 15-20 COM-интерфейсов, это становится удобным и сокращает код. Все эти соображения навеяны под влиянием примеров из MS SDK. Кстати, может, кто-то несогласен с правильностью описанной процедуры?
<P>Модуль basedd8.pas в проектах для DirectDraw переименован в basedd7.pas - всё-таки DirectDraw - это часть DirectX 7, в версий 8 он как таковой отсутствует.
</P>
В функции LoadFiles() добавлен вызов DeleteObject() - как известно, после работы объекты GDI надо удалять, иначе они поглощают ресурсы системы. В данном случае именно такой объект создаётся при вызове функции GDI LoadImage() - казалось бы, тип HBITMAP - это всего лишь переопределение типа LongWord, копилятор самостоятельно удалит переменную этого типа после выхода из функции. На самом деле GDI при вызове LoadImage() (и других подобных функций) создаёт ресурс GDI и резервирует для него часть системной памяти, а переменная hBmp - всего лишь идентификатор этого ресурса в общем списке ресурсов Windows. Поэтому в процессе выполнения программы будет удаляться только идентификатор, а ресурс, на который он указывает, будет <висеть> в памяти. Именно поэтому следует вызвать DeleteObject() для удаления объекта GDI. В предыдушем примере я не сделал этого по причине недосмотра.
<P>Большая часть примеров в этой статье предназначена для работы с DirectDraw - как мне кажется, наиболее востребованному элементу DirectX (кроме, естественно, Direct3D).
</P>
Надеюсь, мой стиль написания кода программ покажется удовлетворительным - он почти во всём подобен стилю, который использовали составители DirectX SDK. Вообще, многие пишут, как курица лапой - и предлагают свои творения на всеобщее обозрение. Ещё полезно заглянуть на страницу в нашем уважаемом Королевстве - <A HREF="http://www.delphikingdom.com/article/tassel.htm">http://www.delphikingdom.com/article/tassel.htm</A> - это классика.
<P><STRONG>Почему я не рекомендую использовать DelphiX</STRONG></P>
Хочется поделиться с новичками своим мнением по поводу компонетов DelphiX и почему я не рекомендую их использовать.
<P>С одной стороны, DelphiX - это удобно - нет необходимости выполнять утомительный набор методов DirectX и длинных, как многоступенчатая ракета, констант наподобие DDENUMSURFACES_CANBECREATED. Однако давайте посмотрим - используется что-нибудь подобное в С++? Я не могу исследовать всю Сеть в поисках овета на такой вопрос, но, думается - нет. Почему?
</P>
Такие наборы классов - это нестандартный подход. Допустим, вы потратили изрядно своего времени и досконально изучили DelphiX. Так вот, изучив всё ЭТО, вы в итоге не изучили сам DirectX. Второе - изученные классы обладают многим и позволяют писать реальные программы, но всё равно этот подход очень негибок - вы ограничены тем, что уже сделано. На этот счёт у меня есть веский аргумент - это Direct3D. Вот тут DelphiX уж точно не даст развернуться как следует - и это характерно и для других компонент DirectX, пусть и в меньшей мере. Третье - немного, но снижается быстродействие. Четвёртое - в классах DelphiX кроме непосредственных вызовов методов интерфейсов DirectX используются ещё и собственные функции - кто даст гарантию, что в них нет ошибок?
<BR>В конце концов, именно за такие вот <примочки> Delphi не в почёте у С-программистов - они попросту надсмехаются над такими методами разработки программ. К сожалению, должен к ним присоединиться и я. Как же так, возмутятся многие. Компонентный подход - это ведь основа основ Delphi! Согласен, использование TMemo или TComboBox - это действительно удобный подход, да что там - превосходный, отличный подход! Но вот в случае с DirectX или чем-то подобным использовать такие средства разработки крайне нежелательно. Как бы вы отнеслись к компоненту TOpenGL? Или TWin32API? Вот так-то.
DelphiX можно использовать как источник разных идей по реализации того или иного эффекта - перенося всё это в свою программу в виде отдельных функций или собственноручно написанных классов. Так что изучайте прямой API - для уверенности в завтрашнем дне и в собственной квалификации.
<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
Собственно, сами примеры</B></FONT></TD></TABLE></P>
<STRONG>Bounds</STRONG><BR>
В предыдущей статье я предложил вниманию только один пример для DirectDraw - простая реализация вывода спрайта поверх фона в задний буфер и перенос построенной картинки на экран. Спрайт не мог выйти за пределы экрана - и это не спроста. Если убрать досадное ограничение, то выяснится, что как только часть спрайта выходит за границы экрана (а фактически границы заднего буфера), то сразу исчезает полностью. Возвращение в прежнюю позицию восстанавливает вывод.
<DIV ALIGN="center"><IMG SRC="http://www.delphikingdom.ru/images/image001.gif" WIDTH="198" HEIGHT="155" BORDER="0" ALT=""></DIV>
<P>Когда я впервые столкнулся с этой проблемой, меня это неприятно поразило. Оказалось, что необходимо использовать <STRONG>интерфейс отсечения</STRONG> - IDirectDrawClipper. Однако он предназначен только для оконных приложений, в полноэкранном режиме от него нет никакого проку. Как уже было упомянуто, рекомендую программировать только полноэкранные приложения как наиболее быстродействующие, и забыть об оконных. Так как же быть - тупик?</P>
К сожалению, даже в SDK нет примеров решения этого вопроса. Впрочем ответить на него не так уж сложно - нужно лишь понять, что хочет DirectDraw и как это преподнести.
<P>Корень проблемы в том, что как только при копировании методом BltFast() часть поверхности выходит за край той поверхности, на которую она копируется (обычно задний буфер), вывод не осуществляется. В чём причина такого нелепого ограничения - думается, опять же в обеспечении наибольшего быстродействия. Например, вы планируете создать игру типа Tetris, а не скроллинговую стрелялку, и все ваши спрайты будут двигаться только в пределах экрана - но вот DirectDraw всё равно пришлось бы проверять их выход за границы, даже при отсутствии в этом необходимости. Хотя эту проблему можно было бы решить с помощью флагов при создании конкретной поверхности, но Microsoft этого не сделала. Ну что же, сделаем за неё эту работу.
</P>
Обратите внимание на четвёртый параметр метода IDirectDrawSurface7.BltFast() - это адрес структуры типа TRect. Для чего он нужен? Как известно, назначение струтуры TRect в GDI API - указание положения и размера какой либо области путём задания левого верхнего и правого нижнего угла. Так вот, эта структура позволяет указать DirectDraw о необходимости вывести не всё изображение спрайта, а лишь его часть:
<DIV ALIGN="center"><IMG SRC="http://www.delphikingdom.ru/images/image002.gif" WIDTH="198" HEIGHT="155" BORDER="0" ALT=""></DIV>
Воспользуемся этой структурой и будем вместо всего спрайта выводить какую-то его облать - ту, которая будет видна на экране, а невидимая будет находиться вне области, описываемой структурой TRect. Т. о. будет создаваться лишь иллюзия пребывания спрайта за пределами экрана. Например, спрайт выходит за границы экрана, как это показано на самом первом рисунке. Тогда выводимая область должна быть такой (показана красным цветом):
<DIV ALIGN="center"><IMG SRC="http://www.delphikingdom.ru/images/image003.gif" WIDTH="198" HEIGHT="155" BORDER="0" ALT=""></DIV>
Теперь повоображайте и нарисуйте на бумаге, как будут выглядеть выводимые части спрайта при различных его положениях за границами экрана.
<P>Вот код, ответственный за вывод части изображения:
</P>
<PRE>
<FONT COLOR="#0000FF">// Предполагаем, что края спрайта не выходят за границы экрана</FONT>
SetRect( rRect, 0, 0, SPRITE_WIDTH, SPRITE_HEIGHT );

<FONT COLOR="#0000FF">// Проверяем выход краёв, и если такая ситуация имеет место, то корректируем
// положение области копирования на поверхности спрайта</FONT>
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;
</PRE>
Где nX и nY - координаты левого верхнего угла спрайта. При выводе надо не забыть скорректировать их:
<PRE>nX + rRect.Left, nY + rRect.Top</PRE>
Вот и всё. Запустите проект на выполнение - вы увидите, что теперь свободно отбражается даже часть спрайта. Выведите его полностью за пределы экрана - начнёт жужжать встроенный динамик компьютера - эта возможность введена для проверки правильности алгоритма.
<P>Кстати, если необходимо вывести всю поверхность изображения, вместо адреса структуры следует передать nil - как это сделано для фона.
</P>
<STRONG>Scale</STRONG><BR>
Иногда при выводе может понадобится растянуть или сжать объект по осям или просто увеличить или уменьшить его - для подобных эффектов DirectDraw предоставляет метод IDirectDrawSurface.Blt(). Он является хотя и более медленным, чем BltFast() - однако при этом более функционален. Так вот, мы снова будем указывать с помощью структуры TRect область вывода изображения - но уже на поверхности-приёмнике данных. Изменяя её размеры, можно добиться пропорционального или непропорционального изменения масштаба изображения по осям X и Y.
<P>Думаю, нет надобности описывать действия которые происходят в процедуре OnDraw(). Замечу лишь, что на современных видеокартах с полной аппаратной поддержкой DirectDraw эффект масштабирования выглядит гораздо привлекательнее, чем на <ветеранах>.
</P>
<STRONG>Transparent</STRONG><BR>
При выводе изображения на экран нередко встаёт проблема затирания заднего фона, а говоря языком профессионалов - проблема вывода нерегулярных спрайтов. Нерегулярный спрайт - это обычный прамоугольный спрайт, который содержит маску для указания прозрачных участков изображения. Пиксели, принадлежащие этим участкам, при выводе игнорируются - создаётся иллюзия прозрачности.
<P>В GDI нет прямого способа вывести нерегулярный спрайт - для этого необходимо подготовить изображение и отдельно маску. В чём недостатки такого подхода? Их много. Во-первых, требуется иметь два изображения - это увеличивает объём данных в памяти и на диске. Во-вторых, скорость вывода данного изображения падает вдвое, а ведь GDI и без этого не славится своей скоростью. В третьих, это дополнительная забота того, кто готовит изображение в графическом редакторе. В своё время автор даже написал небольшую утилиту, которая создавала маску для выбранного изображения и записывала её в отдельный файл. Но теперь об этом можно забыть.
</P>
DirectDraw предоставляет удобный инструмент для задания маски прозрачности. Цвета пикселей, которые игнорируются, называются <цветовыми ключами>. Каждая поверхность может иметь свои цветовые ключи, причём их может быть несколько.
<P>Следующий фрагмент кода создаёт и присоединяет к поверхности <цветовой ключ>, цвет которого - чёрный.
</P>
<PRE>var ddck: TDDCOLORKEY;

ddck.dwColorSpaceLowValue := 0;
ddck.dwColorSpaceHighValue := ddck.dwColorSpaceLowValue;

pSprite.SetColorKey( DDCKEY_SRCBLT, @ddck );
</PRE>
Для указания прозрачного цвета, как видно, используется структура TDDCOLORKEY. В её двух полях необходимо указать нижнюю и верхнюю границу диапазона <прозрачных> цветов. Замечу, что использование диапазона цветов возможно только в случае, если такая возможность поддерживается аппаратно. Поэтому лучше ограничиться каким-либо одним цветом, как это сделано выше. После заполнения структуры TDDCOLORKEY необходимо вызвать метод IDirectDrawSurface7.SetColorKey(), где первый параметр - один из возможных флагов, второй - адрес структуры TDDCOLORKEY. Обычно используется флаг DDCKEY_SRCBLT, который указывает, что при копировании изображения будет использоваться цветовой ключ поверхности-источника. Другие флаги можно узнать из справочной службы DirectX SDK.
<P>Теперь о главном. В приведённом выше фрагменте кода в качестве маски задаются пиксели чёрного цвета. Как известно, нулевое значение обозначает отсутствие цвета во всех графических режимах - 16 цветов, 256, 65535 и т.д.
</P>
Поэтому можно смело присваивать 0 для чёрной маски в любом режиме. Однако, предположим, нам надо задать цветовой ключ в виде чистого синего цвета. Для 24- и 32-битного режима это можно сделать с помощью макроса (функции) из модуля windows.pas:
<PRE>function RGB(r, g, b: Byte): COLORREF;
begin
Result := (r or (g shl 8) or (b shl 16));
end;
</PRE>
Зарезервированное слово shl относится к сдвиговым операциям и сдвигает содержимое на указанное значение влево.
<P>Т.к. в этих графических режимах каждый из трёх цветов (красный, синий и зелёный) кодируется одним байтом, то значение каждого параметра функции должно лежать в пределах от 0 до 255. Вот как это можно представить графически:
</P>
<DIV ALIGN="center"><IMG SRC="http://www.delphikingdom.ru/images/image004.gif" WIDTH="243" HEIGHT="48" BORDER="0" ALT=""></DIV>
Так, для задания цветового ключа в виде чистого синего цвета необходимо написать так:
<PRE>ddck.dwColorSpaceLowValue := RGB( 0, 0, 255 );
ddck.dwColorSpaceHighValue := ddck.dwColorSpaceLowValue;
</PRE>
Ну и в том же духе, в полном соответствии с теорией цвета.
<P>А теперь попробуйте задать цветовой ключ для 16-битового режима. Ничего не получится. Почему? Дело в том, что цвет пикселя хранится в ячейке длиной в 16 бит, а цветовых составляющих - 3, появляется лишний бит, который чаще отдаётся зелёному цвету, а иногда просто не используется. Формат, где теряется лишний бит, обозначается 5-5-5 (на каждую цветовую составляющую по пять бит, а не одному байту), другой формат обозначается 5-6-5 (на зелёную составляющую выделяется 6 бит ). Понятно, что задание цвета с помощью функции RGB() для таких форматов ни к чему ни приведёт.
</P>
В своё время я довольно долго промучился с этой проблемой, тем более что в имеющейся литературе ничего об этом не сказано. В конце концов решил, что необходимо написать аналогичную к RGB() функцию, но об этом немного позже.
<P>Давайте сначала выясним, какой же формат использует установленная на нашем компьютере видеокарта. DirectDraw позволяет узнать это с помощью функции IDirectDrawSurface7.GetPixelFormat(). Единственным параметром необходимо передать адрес структуры TDDPIXELFORMAT. Вот фрагмент соответствующего кода:
</P>
<PRE>var ddpf: TDDPIXELFORMAT;

ZeroMemory( @ddpf, SizeOf( TDDPIXELFORMAT ) );
ddpf.dwSize := SizeOf( TDDPIXELFORMAT );

pSprite.GetPixelFormat( ddpf );
</PRE>
Формат цветовых составляющих описывается в полях dwRBitMask, dwGBitMask и dwBBitMask структуры TDDPIXELFORMAT - но только в том случае, если битовое поле dwFlags содержит флаг DDPF_RGB - признак того, что поверхность создана в RGB-режиме. Значения полей dwRBitMask, dwGBitMask и dwBBitMask для режимов с разной глубиной палитры описываются в разделе dwRBitMask, dwGBitMask и dwBBitMask справочной службы DirectX SDK:

<TABLE ALIGN="center" CELLPADDING="5" CELLSPACING="5">
<TR>
<TD VALIGN="top"><TT>DDPF_RGB</TT></TD>
<TD VALIGN="top"><TT>16</TT></TD>
<TD VALIGN="top"><PRE>R: 0x0000F800
G: 0x000007E0
B: 0x0000001F
A: 0x00000000</PRE></TD>
</TR>
<TR>
<TD VALIGN="top"><TT>DDPF_RGB</TT></TD>
<TD VALIGN="top"><TT>16</TT></TD>
<TD VALIGN="top"><PRE>R: 0x0000001F
G: 0x000007E0
B: 0x0000F800
A: 0x00000000</PRE></TD>
</TR>
<TR>
<TD VALIGN="top"><TT>DDPF_RGB</TT></TD>
<TD VALIGN="top"><TT>16</TT></TD>
<TD VALIGN="top"> <PRE>R: 0x00007C00
G: 0x000003E0
B: 0x0000001F
A: 0x00000000</PRE></TD>
</TR>
</TABLE>
<P>
Запустите готовое приложение GetPixFormat из каталога DXCommon - и посмотрите, какой формат поверхности использует ваша карта в 16-битовом режиме. Скажу, что на компьютере с видеоакселератором GeForce 2 MX 420 получались значения из самой верхней ячейки - и это соответствует формату 5-6-5. По-моему, именно такой формат принят во всех современных видеокартах (заметьте, что во второй ячейке таблицы составляющие R и B переставлены местами). А вот, например, дедушка S3 Trio 3D/2X использует формат, описанный в нижней ячейке - опытным путём установлено, что это 5-5-5.
</P>
Вот как должен быть переписан макрос для формата 5-6-5:
<PRE>function RGB565( r, g, b: Byte ): COLORREF;
begin
Result := ( ( r shl 11 ) or ( g shl 5 ) or b );
end;
</PRE>

Графически битовая маска может быть представлена так:
<DIV ALIGN="center"><IMG SRC="http://www.delphikingdom.ru/images/image005.gif" WIDTH="243" HEIGHT="48" BORDER="0" ALT=""></DIV>
А вот как должен выглядеть макрос для формата 5-5-5:

<PRE>function RGB555( r, g, b: Byte ): COLORREF;
begin
Result := ( ( r shl 10 ) or ( g shl 5 ) or b );
end;
</PRE>
<P>Графически битовая маска может быть представлена так:</P>
<DIV ALIGN="center"><IMG SRC="http://www.delphikingdom.ru/images/image006.gif" WIDTH="243" HEIGHT="48" BORDER="0" ALT=""></DIV>
Как видно, последний бит не используется. Обратите внимание, что группа битов, отвечающих за красную и синию составляющую, в 16-битовом режиме поменялись старшинством. А формат, описанный во второй ячейке таблицы, наоборот, по старшинству схож с 24- и 32-битовым режимами. Подозреваю, что используется этот формат довольно редко.
<P>Естетственно, что теперь максимальное значение, передаваемое в макросы (функции) RGB565() и RGB555(), соответствует значению 31, а для задания читого зелёного цвета в режиме 5-6-5 необходимо указать RGB565( 0, 63, 0 ), т. к. битов 6.
</P>
<P>Для того, чтобы наша DirectDraw-программа без проблем работала в обоих форматах, необходимо проверить текущий формат, запомнить его и при задании цветового ключа для поверхности вызвать соответствующий макрос. Всё это и делается в приложении Transparent - надеюсь при его разборе у вас не возникнет проблем. Не забудьте при копировании методом BltFast() указать флаг DDBLTFAST_SRCCOLORKEY.
</P>
<STRONG>Fps</STRONG><BR>
Ещё один полезный пример - вывод текста на поверхность DirectDraw - в виде значения fps. Сама компонента не обладает такими средствами - DirectDraw изначально создавался лишь для максимально быстрого копирования одного изображения на другое. Для вывода текста необходимо использовать GDI.
<P>Как неоднократно упоминалось, GDI очень медленнен, и вывод текста - одна из функций, которая серьёзно может <притормозить> DirectDraw-программу. Поэтому необходимо пользоваться этой функцией как можно реже. Для взаимодействия DirectDraw c GDI введён простой метод IDirectDrawSurface7.GetDC(). Получив контектс, можно спокойно чертить в нём всеми мыслимыми функциями GDI. Метод IDirectDrawSurface7.ReleaseDC() переносит содержимое контекста в область памяти, занятую поверхностью DirectDraw и удаляет контекст.
</P>
Откройте файл проекта fps.dpr. Т. к. функция TextOut() уже занята, функцию, отвечающую за вывод текста, пришлось назвать менее звучно - OutText(). Я не буду подробно описывать её, надеюсь, всё понятно. Для ускорения работы программы я поступил так: для вывода текста используется отдельная поверхность - именно на неё и выводится текст средствами GDI. Затем всё время поверхность просто копируется на задний буфер - это осуществляется гораздо быстрее, чем постоянный вывод текста на задний буфер, а когда появляется необходимость изменить текст - он снова выводится на нашу отдельную поверхность. Потребность изменить текст появляется лишь раз в секунду.
<P>Для вызова OutText() я использовал мультимедиа-таймер Windows. Значение fps наращивается при каждом построении кадра и обнуляется после вызова OutText().
</P>
<P>И последнее. По-видимому, в операционной системе Windows 2000 функции GDI должны работать быстрее, т. к. эта ОС полностью 32-х разрядная. Но всё же рекомендую пользоваться описанным выше подходом.
</P>
<STRONG>Text</STRONG><BR>
Ещё один пример вывода текста - но уже на задний буфер. Добавлен мною для полноты темы. Текст заданных размеров постоянно выводится на задний буфер без поверхности-посредника. При выводе текста я столкнулся с одной проблемой - это сглаживание краёв символов. В модуле windows.pas описана константа ANTIALIASED_QUALITY, но её задание в параметре fdwQuality функции CreateFont() ни к чему ни привело. Может быть, в Windows 9x и МЕ это значение не используется? Во всяком случае, константы ANTIALIASED_QUALITY и NONANTIALIASED_QUALITY в справке Delphi Help не описаны.
<P></P>
<STRONG>Sound</STRONG><BR>
Эта программа - прямое продолжение моего первого примера по использованию DirectSound. Введено ряд усовершенствований:

<OL>
<LI>Файл lowfunc.pas теперь полностью закончен и является практически прямым переводом файла wavread.cpp. Выражаю благодарность Max Morozov и iXania, которые помогли мне перевести некоторые сложные конструкции с языка C++ на Object Pascal, т. к. самому мне для этого не хватило квалификации. Теперь нет необходимости использовать отдельную динамическую библиотеку - весь код располагается в exe-файле. Всем спасибо.</LI>
<LI>Я решил написать небольшой класс TWave - он сам заботится об открытии звукового файла, чтении данных из него в звуковой буфер и проигрывании их. Функциональность класса не полная - это лишь пример. Благодаря ООП главный модуль main.pas серьёзно уменьшился, теперь для воспроизведения wav-файла средствами DirectSound</LI>
достаточно написать:
<PRE>var sound: TWave;

sound := TWave.Create();
sound.OpenWaveFile( <FONT COLOR="#996600">'wavefile.wav'</FONT> );
sound.Play();</PRE>
</OL>

<P>Правда, просто?</P>
<STRONG>GetDXVer и GetDXVerSetup</STRONG><BR>
Я решил заглянуть в некоторые области DirectX, до которых руки многих авторов книг по DirectX попросту <не доходят>.
<P>Например, написание программы для определения текущей версии DirectX. Иногда это может быть очень полезно.
</P>
Первый пример, который я предлагаю вашему вниманию - это GetDXVer. Это аналог из DirectX SDK для Visual C++. Функция GetDXVersion() ответственна за получение намера текущей версии DirectX. Каким образом она действует? Механизм прост, но достаточно громозд. Сначала загружается нужная динамическая библиотека из комплекса DirectX, например DDRAW.DLL или DINPUT.DLL. Затем получают адреса функций, которые экспортируют эти библиотеки - это <создающие> функции наподобие DirectDrawCreate() или DirectInputCreateA(). Затем при помощи этих функций и создаются нужные интерфейсы вроде IDirectDraw и т.п. Если на каком-то шаге происходит сбой, это означает, что данная функция или интерфейс не поддерживаются. Зная, в какой версии появился тот или иной интерфейс, можно выяснить текущую версию DirectX.
<P>Ещё одна функция, GetDXRegVersion(), извлекает полный порядковый номер из реестра Windows. Кстати, эта функция может в принципе читать любой строковый параметр из реестра и делает ненужным использование класса TRegistry, что очень важно, если мы хотим получить маленький по размерам исходный модуль.
</P>
Пример имеет два недостатка:
<OL>
<LI>Работает с некоторой задержкой. Для создания всех интерфейсов требуется некоторое время. Особо медленно создаётся интерфейс IDirectMusic.</LI>
<LI>Программа не способна определить номер версии, если он выше 8 - это принципиальный барьер.</LI>
</OL>
Ещё один пример - GetDXVerSetup - использует специальную функцию DirectXSetupGetVersion() из библиотеки dsetup.dll. эта библиотека не входит в стандартный run-time DirectX, а поставляется только с setup-программами установки DirectX на компьютер пользователя. При написании этого примера я столкнулся с двумя проблемами:

<OL>
<LI>В Help-службе DirectX SDK 7 указаны такие возможные значения, которые могут быть помещены в переменную pdwVersion при вызове функции DirectXSetupGetVersion():

<TABLE ALIGN="center" CELLPADDING="2">
<TH>DirectX version</TH><TH> Value pointed to by pdwVersion</TH>
<TR><TD>DirectX 1</TD><TD> 0x00040001</TD></TR>
<TR><TD>DirectX 2</TD><TD> 0x00040002</TD></TR>
<TR><TD>DirectX 3</TD><TD> 0x00040003</TD></TR>
<TR><TD>DirectX 5.0</TD><TD> 0x00040005</TD></TR>
<TR><TD>DirectX 6.0</TD><TD> 0x00040006</TD></TR>
<TR><TD>DirectX 7.0</TD><TD> 0x00040007</TD></TR>
</TABLE>

А вот в Help-службе DirectX SDK 8 указаны такие:
<TABLE ALIGN="center" CELLPADDING="2">
<TH>DirectX version</TH><TH> Value pointed to by pdwVersion</TH>
<TR><TD>DirectX 1</TD><TD> 0x00000000</TD></TR>
<TR><TD>DirectX 2</TD><TD> 0x00000000</TD></TR>
<TR><TD>DirectX 3</TD><TD> 0x00000000</TD></TR>
<TR><TD>DirectX 5.0</TD><TD> 0x00040005</TD></TR>
<TR><TD>DirectX 6.0</TD><TD> 0x00040006</TD></TR>
<TR><TD>DirectX 7.0</TD><TD> 0x00040007</TD></TR>
<TR><TD>DirectX 8.0</TD><TD> 0x00040008</TD></TR>
</TABLE>
Зачем понадобилось обозначить версии 1, 2 и 3 как отсутствие DirectX - непонятно. Может быть, Microsoft посчитала, что эти ранние версии уже слишком устарели и не обеспечивают пользователя нужными мультимедиа-средствами? А раз так, то может быть лучше вообще известить об отсутствии DirectX? Может статься, что это просто ошибка в файле справки. Второй вариант более правдоподобен, первый - забавен.
<LI>Используя файл directsetup.pas, мне не удалось экспортировать функцию DirectXSetupGetVersion() из библиотеки dsetup.dll. Дело в том, что она ищется в каталоге, по-видимому, указанном в ключе реестра HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\DirectXDrivers, но я не обнаружил такой ключ в своём реестре. Так что воспользоваться этим файлом не представилось возможным. К тому же структуры TDirectXRegisterAppW2 не существует - приехали!
</OL>
Я самостоятельно перевёл файл dsetup.h из SDK 8 в файл dsetup.pas, пытаясь максимально точно соблюдать синтаксис структур и параметро функций. Может быть, кто-то им воспользуется.
<P>Недостаток приведенного метода в том, что вам придётся постоянно <таскать> библиотеку dsetup.dll вместе с исходной программой.
</P>
<STRONG>Заключение</STRONG><BR>
Написание подобных статей - хороший стимул к тщательному изучению DirectX. Скажу, что чем больше работаю с этим комплексом, тем больше разных нюансов всплывает на поверхность.
<P>Надеюсь, что мои усилия хоть как-то помогут остальным желающим освоить этого игрового <монстра>. Я надеюсь продолжить изучение DirectX и как только получится создать что-то стоящее, попробую поделиться сделанным с остальными.
</P>
Напоследок хочу выразить особую благодарность <STRONG>Антону Ржешевскому</STRONG> за его дельные советы в освоении DirectX и не только.
<P>Скачать примеры: <STRONG><A HREF="http://delphi.mtu-net.ru/zip/samples2.zip">Samples2.Zip</A></STRONG> (106 K)
</P>
<P ALIGN="right"><STRONG><A HREF="http://www.delphikingdom.ru/asp/users.asp?ID=1146">Виктор Кода</A></STRONG></P>[/doHTML]

Автор: Song 09.11.03, 11:05
[doHTML]<P>Привет всем, кто интересуется программированием под DirectX на языке Object Pascal!
</P>
Как и обещал, я продолжаю искать новый материал по DirectX, переводить его на язык Object Pascal и представлять всеобщему вниманию. Недавно у меня появилась идея снятия скриншотов с экрана DirectDraw-программы и записи изображения в простой bmp-файл - некоторые игры позволяют это делать, и я решил последовать их примеру. Потом я наткнулся на другой интересный материал - речь шла о загрузке изображения из bmp-файла без использования функции LoadImage(). Поэтому тема статьи всецело посвящена работе с bmp-файлами на "низком уровне". Замечу, что это немного сложные вещи, но мы ведь сложностей не боимся, правда? Иначе непонятно, зачем тогда заниматься изучением DirectX вообще.
<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
Замечания</B></FONT></TD></TABLE></P>
Каждому примеру для нормальной работы необходимо, чтобы файл data.bmp находился в том же каталоге, что и исполняемый файл. Для экономии места в архиве я разместил этот файл в папке первого примера, поэтому вам надо будет скопировать его и в остальные папки.

<P>Немного изменилась реализация модуля ddutils.pas - теперь функция CreateSurface() не требует адрес главного интерфейса IDirectDraw, а создаёт и уничтожает локальный интерфейс. Возможно, более практично с точки зрения Pascal-программирования было бы объявить глобальный для модуля ddutils.pas интерфейс IDirectDraw, а для создания и удаления интерфейса воспользоваться секциями initialization и finalization модуля.
</P>
Также немного изменился стиль написания программ, теперь он совсем грешит С-подобным кодом :-)
<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
FASTFILE1</B></FONT></TD></TABLE></P>
В этом примере я попытаюсь показать, как произвести загрузку растра из bmp-файла методом, отличным от того, что применялся в предыдущих уроках. Напомню, как в общих чертах выглядела схема загрузки ранее:

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

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

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

Прежде чем приступить к рассмотрению работы функции, необходимо в общих чертах представить себе, каким образом записывается информация в bmp-файле. На рис. 1 показана структура беспалитрового 24-битного файла.
<DIV ALIGN="center"><IMG SRC="http://www.delphikingdom.ru/images/bmp.gif" WIDTH="300" HEIGHT="108" BORDER="0" ALT="Рис.1. Структура файла BMP, не содержащего палитру"><BR>
Рис.1. Структура файла BMP, не содержащего палитру</DIV>
Хранящийся на диске файл DIB, обычно с расширением .bmp, как видно, начинается со структуры BITMAPFILEHEADER, позволяющей начать работу с файлом. Вот как эта структура описана в файле windows.pas:
<PRE>tagBITMAPFILEHEADER = packed record
bfType: Word; <FONT COLOR="#0000FF">// Тип файла. Должен содержать 'BM' ($4d42)</FONT>
bfSize: DWORD; <FONT COLOR="#0000FF"> // Размер файла в байтах</FONT>
bfReserved1: Word; <FONT COLOR="#0000FF">// Зарезервировано, должен быть нуль</FONT>
bfReserved2: Word; <FONT COLOR="#0000FF">// Зарезервировано, должен быть нуль</FONT>
bfOffBits: DWORD; <FONT COLOR="#0000FF">// Смещение от начала файла до гафических данных</FONT>
end;
BITMAPFILEHEADER = tagBITMAPFILEHEADER;
</PRE>
Следом за структурой BITMAPFILEHEADER следует стуктура BITMAPINFO:

<PRE>tagBITMAPINFO = packed record
bmiHeader: TBitmapInfoHeader; <FONT COLOR="#0000FF">// Структура BITMAPINFOHEADER</FONT>
bmiColors: array[0..0] of TRGBQuad; <FONT COLOR="#0000FF">// RGB-триплекс</FONT>
end;
BITMAPINFO = tagBITMAPINFO;</PRE>
Фактически, стуктура BITMAPINFO включает в себя ещё одну структуру - BITMAPINFOHEADER:
<PRE>
tagBITMAPINFOHEADER = packed record
biSize: DWORD; <FONT COLOR="#0000FF">// Размер самой структуры в байтах</FONT>
biWidth: Longint; <FONT COLOR="#0000FF">// Ширина растра в пикселях</FONT>
biHeight: Longint; <FONT COLOR="#0000FF">// Высота растра в пикселях</FONT>
biPlanes: Word; <FONT COLOR="#0000FF">// Количество плоскостей (всегда 1)</FONT>
biBitCount: Word; <FONT COLOR="#0000FF">// Количество бит на 1 пиксель </FONT>
biCompression: DWORD; <FONT COLOR="#0000FF">// Тип сжатия (BI_RGB - без сжатия)</FONT>
biSizeImage: DWORD; <FONT COLOR="#0000FF"> // Размер изображения в байтах (обычно 0)</FONT>
biXPelsPerMeter: Longint; <FONT COLOR="#0000FF">// А эти данные </FONT>
biYPelsPerMeter: Longint; <FONT COLOR="#0000FF"> // нам вообще</FONT>
biClrUsed: DWORD; <FONT COLOR="#0000FF">// никогда не</FONT>
biClrImportant: DWORD; <FONT COLOR="#0000FF">// понадобятся</FONT> <FONT COLOR="#FF0000">:)</FONT>
end;
BITMAPINFOHEADER = tagBITMAPINFOHEADER;</PRE>

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

<P>После этих структур начинаются графические данные. В 24-битном файле каждый пиксель кодируется 3 байтами - на каждую составляющую R, G, B - по одному байту. Значение каждой составляющей может варьироваться от 0 до 255.
</P>
Откройте файл проекта и найдите функцию LoadData(). Она вызывает другую функцию - LoadBitMap(). Я разместил её в файле ddutils.pas, вот её прототип

<PRE>function LoadBitMap( name: pchar; pbi: PBITMAPINFO ): pointer;</PRE>
Первым параметром передаётся имя загружаемого файла, вторым - адрес структуры BITMAPINFO, структура понадобится после вызова функции LoadBitMap().

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

<PRE>hFile := CreateFile( name, GENERIC_READ, FILE_SHARE_READ, nil,
OPEN_EXISTING, 0, 0 );
if hFile = INVALID_HANDLE_VALUE then
exit;
</PRE>
Переменная hFile - это дескриптор открытого файла. Проверить, открыт ли он в самом деле можно, сравнив дескриптор с константой INVALID_HANDLE_VALUE. Далее считывается структура BITMAPFILEHEADER:

<PRE>ReadFile( hFile, bfh, sizeof( BITMAPFILEHEADER ), dwBytesRead, nil );</PRE>
<P>Замечу, что вторым параметром функции ReadFile() передаётся сама структура, куда будут записаны данные, третьим - количество байт, которые надо прочитать. Четвёртые параметр должен присутствовать обязательно, в него функция запишет количество реально прочитанных байт. Для пущей надёжности можно сравнить это значение с размером структуры BITMAPFILEHEADER, и если значения не совпадают, объявить об ошибке.
</P>
Далее считывается структура BITMAPINFOHEADER:

<PRE>ReadFile( hFile, bi, sizeof( BITMAPINFOHEADER ), dwBytesRead, nil );</PRE>
<P>Думаю, надо объяснить, почему здесь мы читаем только данные структуры BITMAPINFOHEADER, и не считываем массив bmiColors. Дело в том, что этот массив в структуре BITMAPINFO, там, куда мы её передадим позже, всё равно не используется. Однако он входит в состав общих графических данных, поэтому мы считаем его вместе с ними, а в структуре bi массив bmiColors оставим пустым.
</P>
Далее идёт считывание графических данных. Прежде всего необходимо определить, какой размер они имеют:

<PRE><FONT COLOR="#0000FF">// Определяем размер DIB</FONT>
dwDIBSize := GetFileSize( hFile, nil ) - sizeof( BITMAPFILEHEADER ) - sizeof( BITMAPINFOHEADER );</PRE>

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

<PRE><FONT COLOR="#0000FF">// Выделяем участок памяти</FONT>
result := pbyte(GlobalAllocPtr( GMEM_MOVEABLE, dwDIBSize ));</PRE>

После этого в память считываются битовые данные, формирующие картинку, и файл закрывается:
<PRE><FONT COLOR="#0000FF">// Читаем данные DIB</FONT>
ReadFile( hFile, result^, dwDIBSize, dwBytesRead, nil );
<FONT COLOR="#0000FF">// Закрываем файл</FONT>
CloseHandle( hFile );
</PRE>
Описанная функция работает только с 24-битными несжатыми растрами. Использование 256-цветных палитровых файлов я считаю нецелесообразным, т. к. качество изображения в них не удовлетворяет требованиям современной компьтерной графики.

<P>Итак, функция LoadBitMap() загрузила в оперативную память битовые данные, формирующие изображение и вернула указатель на них как результат функции. Вернёмся теперь обратно к функции LoadData(). Первый шаг сделан - произведена максимально быстрая загрузка данных из файла (я не вижу способа, как можно ещё как-нибудь ускорить этот процесс). Теперь надо сделать второй шаг. В чём он состоит? Для ускорения загруки в играх и других программах все графические данные объединяются в один или несколько больших файлов. Такую реализацию, например, можно увидеть в игре Donuts из DirectX SDK 7-8. Такое объединение очень полезно при условии, что файл на жестком диске нефрагментирован. Данный метод, безусловно, уменьшает время загрузки, но как будет видно далее, добавляет лишних хлопот программисту.
</P>
Я подготовил простой bmp-файл, в котором хранится изображение для фона и десять "кадров", которые будут последовательно сменять друг друга. Как же загрузить эти данные на поверхности DirectDraw?
Есть два пути:
<OL>
<LI>Воспользоваться методами Lock() и Unlock() интерфейса IDirectDrawSurface7, и осуществлять прямое копирование данных функцией CopyMemory(). Это решение оптимально по скоростным характеристикам, но уж очень сложное: необходимо учитывать абсолютно все нюансы формата поверхности, на которую копируются данные - а их очень много, ведь формат меняется в зависимости от глубины цвета текущего видеорежима - 8, 16, 24, 32 бит. К тому же, выигрыш в этом случае может оказаться совсем небольшим. </LI>
<LI>Использовать функцию GDI StretchDIBits(). Она предназначена для копирования в контекст данных, расположеных не в другом контексте, а находящихся просто в указаннном участке памяти. Может сложиться впечатление, что эта функция достаточно медленна - из-за угрожающей приставки "Stretch". Однако если будут копироваться участки битов, одинаковые по высоте и ширине, то в этом случае функция, думается, должна работать быстрее.</LI>
</OL>
Я решил использовать второй способ.
Итак, первым делом создадим поверхность для фона:

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

<PRE>if g_pWallpaper.GetDC( DC ) = DD_OK then
begin
<FONT COLOR="#0000FF">// Копируем битовый массив в контекст</FONT>
StretchDIBits( DC,
0, 0, 640, 480,
0, 64, 640, 480,
pBits, bi,
0, SRCCOPY );
g_pWallpaper.ReleaseDC( DC );
end;</PRE>
<P> Заметьте, что растр в файле (и памяти) хранится в перевёрнутом виде, поэтому ось Y битовой карты направлена вверх. Это необходимо учитывать при задании области копирования. Для копирования массива битов функции StretchDIBits() необходимо передать адрес массива в памяти, а также адрес структуры BITMAPINFO - опираясь на неё, она сможет правильно произвести копирование.
</P>
Далее 10 раз осуществляется копирование в отдельные поверхности массива g_pMovie. Опять же, необходимо учитывать, что строки растра перевёрнуты. После этого необходимо освободить участок системной памяти, где хранится битовый массив:

<PRE><FONT COLOR="#0000FF">// Освободили битовый массив!</FONT>
pBits := nil;</PRE>
Вот и всё, можно приступать к отрисовке экрана.

<P>Вообще такая схема объединения всех данных в один большой или несколько больших файлов оправдана в крупных программах и играх - там, где набор различных изображений достигает сотен штук. На этапе черновой разработки целесообразно загружать растры из отдельных файлов, а в конце, перед релизом программы, в процессе доводки и оптимизации, объединить всё в один большой файл. При этом придётся немного поработать в графическом редакторе, разместив отдельные растры оптимальным способом, не оставляя "пустых" мест. Также целесообразно подготовить массив типа TRect, которыи будет описывать область каждой картинки в этом растре, и пользоваться им в функции загрузки.
</P>
<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
FASTFILE2</B></FONT></TD></TABLE></P>
Предыдущий пример продемонстрировал способ ускорить загрузку файла в память. Однако перенос данных на конкретные поверхности усложнился, да и постоянный вызов функции StretchDIBits() должен отрица-
тельно сказаться на времени копирования.

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

<P>Наконец, существует ещё один, наиболее эффективный путь. Можно не заниматься копированием растра с общей на отдельные поверхности, а переносить растр на дополнительный буфер прямо с общей поверхности.</P>
Например:
<PRE>g_pBackBuffer.BltFast( x, y, g_pMovie[ frame ], nil, DDBLTFAST_WAIT );</PRE>
Однако можно третьим параметром указать общую data-поверхность, а четвертым - не nil, а область на этой поверхности:

<PRE>g_pBackBuffer.BltFast( x, y, g_pDataSurface, arrayRect[ FRAME_01 ], DDBLTFAST_WAIT );</PRE>
Тогда можно не создавать отдельные поверхности и не заниматься копированием данных. Однако есть и недостатки. Например, память видеокарты должна быть достаточно большой - если памяти не хватит для размещения всей data-поверхности, DirectDraw разместит её в системной памяти, и процесс вывода изображения резко замедлится - вот вам и оптимизация! Также могут возникнуть проблемы с цветовыми ключами и корректным отображением спрайтов. В общем, решение половинчатое.
<P><TABLE WIDTH="100%" BGCOLOR="#FAEBD7" CELLPADDING="3" CELLSPACING="0" BORDER="0">
<TD ALIGN=CENTER><FONT COLOR="#996600"><B>
PRINTSCREEN</B></FONT></TD></TABLE></P>
Заманчиво, когда в программе имеется возможность делать "снимки" экрана и сразу записывать их в файл. Этот пример ничем не отличается от предыдущих, за исключением того, что при нажатии на клавишу "Пробел" делается запись содержимого экрана в файл screen.bmp.

<P>Функция, которая проделывает эту работу, находится в файле pscreen.pas. Рассмотрим её.
</P>
Первым делом создаётся новый файл или открывается для перезаписи старый:
<PRE><FONT COLOR="#0000FF">// создаём файл с заданным именем, в него будет производиться запись</FONT>
hFile := CreateFile( szFileName, GENERIC_WRITE, FILE_SHARE_READ, nil, CREATE_ALWAYS, 0, 0 );
if hFile = INVALID_HANDLE_VALUE then
begin
CloseHandle( hFile );
exit;
end;</PRE>
Затем нам необходимо получить данные о поверхности (здесь в функцию передан дополнительный буфер):
<PRE>
<FONT COLOR="#0000FF">// подготавливаем структуру TDDSURFACEDESC2</FONT>
ZeroMemory( @ddsd2, sizeof( TDDSURFACEDESC2 ) );
ddsd2.dwSize := sizeof( TDDSURFACEDESC2 );

<FONT COLOR="#0000FF">// получаем формат поверхности</FONT>
pSurface.GetSurfaceDesc( ddsd2 );
dwWidth := ddsd2.dwWidth;
dwHeight := ddsd2.dwHeight;
dwBPP := ddsd2.ddpfPixelFormat.dwRGBBitCount;
</PRE>
Структура ddsd2 используется дополнительно в методе Lock() поверхности. Заблокировав поверхность, можно обратится к её содержимому для чтения данных:

<PRE><FONT COLOR="#0000FF">// блокируем поверхность DirectDraw</FONT>
if( FAILED( pSurface.Lock( nil, ddsd2, DDLOCK_WAIT, 0 ) ) ) then
exit;
</PRE>
Затем необходимо выделить достаточное количество памяти под массив пикселей. Число три в конце выражения - это потому, что вывод будет осуществляться в 24-битный файл:

<PRE>pPixels := pbyte(GlobalAllocPtr( GMEM_MOVEABLE, dwWidth * dwHeight * 3 ));</PRE>
Затем начинается главное. Т. к. формат пикселя поверхности в каждом из графических режимов различается, необходимо предусмотреть все особенности размещения данных. Бессмысленно подробно описывать все операции - они запутанны и сложны. Мне понадобилось некоторое количество времени, чтобы правильно перевести все операции с указателями с языка C++ в контекст Object Pascal. Операции с указателями на этом языке получаются довольно путаными, малейшая оплошность приводит к тому, что обычно в файл записывается не тот участок памяти (получается мешанина из пикселей), или запись вообще не происходит. Обратите внимание на такую строку:
<PRE>pixel := PDWORD(DWORD(ddsd2.lpSurface) + i * 4 + j * ddsd2.lPitch)^;</PRE>
Здесь определяется цвет нового пикселя поверхности. ddsd2.lpSurface - это указатель на начало данных поверхности, а ddsd2.lPitch - шаг поверхности, учитывать его нужно обязательно.

<P>После того, как данные скопированы в массив, поверхность обязательно нужно разблокировать. Теперь можно начать запись данных в файл.
</P>
Для начала необходимо вручную подготовить структуры BITMAPFILEHEADER и BITMAPINFOHEADER. В последней надо указать ширину и высоту растра, а также разрядность пикселя. Тип сжатия должен быть BI_RGB - т. е. без сжатия.

<P>После этого с помощью API-функций Windows последовательно в файл записываются структуры BITMAPFILEHEADER, BITMAPINFOHEADER и далее - подготовленные данные из памяти. После записи файл необходимо закрыть, а память - освободить:
</P>
<PRE><FONT COLOR="#0000FF">// закрываем файл</FONT>
CloseHandle( hFile );
pPixels := nil;
</PRE>
Функция получилась громоздкой, согласен. Однако иного способа не существует - во всём виноват формат поверхности. Кстати, я не учёл режим в 256 цветов - опять же по причине анахронизма.

<P>И последнее. Данная функция работает не совсем корректно - если открыть созданный файл в графическом редакторе, то под большим увеличением можно заметить ма-аленький цветовой артефакт - один стобик пикселей имеет не тот цвет. Решение этой проблемы я так и не смог найти.
</P>
Скачать примеры: <STRONG><A HREF="http://delphi.mtu-net.ru/zip/directx3.zip">DirectX3.zip</A></STRONG> (95K)
<P ALIGN="right"><A HREF="http://www.delphikingdom.ru/asp/users.asp?ID=1146"><STRONG>Виктор Кода</STRONG></A><BR>[/doHTML]

Автор: Song 09.11.03, 11:08
Оригиналы статей были взяты из:
http://www.delphikingdom.com/helloworld/directx.htm
http://www.delphikingdom.ru/helloworld/dxpage.htm
http://www.delphikingdom.ru/helloworld/directx_04.htm

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)