On-Line Библиотека www.XServer.ru - учебники, книги, статьи, документация, нормативная литература.
       Главная         В избранное         Контакты        Карта сайта   
    Навигация XServer.ru








 

Интерфейс DirectDraw

161.jpg


В главе 11 мы познакомились с интерфейсом DirectDraw, который предоставляет программисту возможность работы с видеопамятью, а также поддержку блиттинга и переключения страниц. Эта глава посвящена непосредственному режиму Direct3D - прослойке, расположенной над DirectDraw и

предназначенной для вывода точек, линий и треугольников.

По непосредственному режиму Direct3D вполне можно написать отдельную

книгу, однако я хочу лишь в общих чертах объяснить происходящее и дать вам

программу, которая бы позволила заняться самостоятельными экспериментами.

Приложение к этой главе находится в каталоге DSDeval.

Непосредственный режим Direct3D

На рис. 13-1 компоненты DirectDraw и Direct3D объединены в одной рамке, поскольку все приложения, работающие с Direct3D, всегда так или иначе связаны и с DirectDraw (если поместить их в отдельных рамках, то на диаграмме лишь появятся дополнительные стрелки).

162.jpg

Рис. 13-1. Архитектура DirectDraw

Поскольку DirectDraw и Direct3D все же обладают разными функциями, давайте взглянем на диаграмму, на которой показаны услуги, предоставляемые каждым из этих механизмов. На рис. 13-2 перечислены некоторые из возможностей, использованных нами при разработке приложений для непосредственного режима Direct3D.

163.jpg

Видеоадаптер Рис. 13-2. Функции механизмов DirectDraw и Direct3D

Рисунок 13-2 несколько упрощенно показывает, что можно сделать с помощью Direct3D, однако он оказывается хорошей отправной точкой для нашего изучения. Как видите, прослойка DirectDraw на самом деле предназначена лишь для управления памятью видеоадаптера и палитрой. В типичной системе для работы с анимацией в видеопамяти можно разместить первичный и вторичный буфера, Z-буфер, и если останется свободная память - спрайты.

Если же мы хотим изобразить трехмерный макет, то нам понадобится определить еще и многое другое - в частности, вид макета, освещение объектов, их расположение и способность отражать свет. Все это можно сделать, воспользовавшись сервисом непосредственного режима Direct3D.

Снова обращаясь к рис. 13-2, мы видим на нем большую рамку с надписью Устройство Direct3D. Именно это устройство предоставляет многие интерфейсы для работы с прослойкой Direct3D. В той же рамке спрятан и механизм визуализации, который получает все данные и генерирует разноцветные пиксели, из которых на экране складывается трехмерная сцена. Справа от 3D-ycTpofeTBa находится ракурс. Он определяет внешний вид макета в окне - масштабные коэффициенты, перспективу и т. д. Рамка Источник света указывает на то, что в макете присутствует (по крайней мере, один) источник света, а рамка Материал напоминает о задании отражающих свойств поверхностей. Расположенная слева рамка Буфер выполнения (подробнее см. ниже) связана с наиболее сложным элементом. Возможно, я позволил себе некоторую неточность, разместив буфер выполнения в этом месте, поскольку на практике он часто помещается в видеопамять по соображениям производительности (однако сейчас об этом не стоит беспокоиться).

Конвейер визуализации

Наиболее интересной частью всего непосредственного режима является конвейер визуализации, который получает описание объектов макета, данные об источниках света, несколько матриц 4х4 и создает по ним итоговую картину, которую мы видим на экране. На рис. 13-3 изображена упрощенная схема конвейера визуализации.

164.jpg

Воспроизводимый объект (поверхность DirectDraw) Рис. 13-3. Конвейер визуализации

Модуль преобразований переводит входные координаты в систему мировых координат, руководствуясь общей матрицей преобразования. Вы можете влиять на ход преобразований, задавая матрицу мирового преобразования, а также матрицы проекции и вида. Модуль освещения пользуется текущим набором источников света и материалов, закрепленным за вершинами каждого объекта, для расчета фактических цветов вершин. Независимо от используемого модуля освещения (монохромного или RGB) его выходные данные с точки зрения растрового генератора представляют собой набор вершин с определенными цветами.

На завершающем этапе модуль растрового генератора строит изображение в буфере. Существует несколько разных растровых генераторов, выбор которых зависит от текущих режимов заполнения и закраски. Режим заполнения определяет, что должно выводиться в окне - вершины, каркас объекта или же нормальный объект. Закраска также изменяется в широком диапазоне, от ее отсутствия до закраски Гуро.

Чтобы воспользоваться конвейером визуализации, обычно мы заносим набор данных о вершинах, материалах, матрицах, источниках света и т. д. в так называемый буфер выполнения (execute buffer). Затем буфер выполнения пропускается через конвейер. Один и тот же буфер можно многократно пропустить через конвейер; вскоре мы убедимся, что это очень полезно.

Если вы захотите выполнять свои собственные преобразования мировых координат, то можете подключиться к конвейеру на этапе освещения. Если потребуется самостоятельно выполнять преобразования и обсчитывать источники света, подключайтесь к конвейеру на этапе генерации растров (должен напомнить о том, что для самостоятельного выполнения преобразований, обсчета освещения и генерации растров вам следует обратиться к главе 12).

Буфер выполнения

Конвейер визуализации получает исходные данные в буферах выполнения. Обычно такой буфер содержит набор вершин и последовательность команд, указывающих, что нужно сделать с этими вершинами. На рис. 13-4 изображен упрощенный вид буфера выполнения.

165.jpg

Рис. 13-4. Буфер выполнения

В буфере, изображенном на рис. 13-4, содержится список вершин, описывающих одну или несколько фигур в системе координат модели. За списком следует операция матричного умножения, которая может использоваться, например, для поворота макета на несколько градусов. Далее идет материал, а за ним - команда, применяемая к списку вершин (обычно сводящаяся к преобразованию и освещению вершин). Оставшаяся часть списка заполняется командами для рисования отдельных треугольников. Каждый треугольник описывается тремя индексными значениями. По индексам выбирается элемент списка вершин, с которого начинается буфер. Нетрудно догадаться, что буфер выполнения большей частью заполнен данными о вершинах и фигурах.

Поскольку в набор команд буфера выполнения также включены команды для рисования отдельных точек и линий, вы можете легко воспользоваться тем же самым набором исходных данных для рисования 'проволочной модели' объектов - для этого следует заменить команды рисования треугольников командами рисования линий. Если вы захотите самостоятельно выполнять преобразования, с освещением или без него, вид буфера выполнения почти не изменится. Отличия заключаются лишь в том, что для вершин в списке указывается другой тип, а в буфере отсутствуют матричные операции (или данные о материалах, если вы самостоятельно обсчитываете освещение). На рис. 13-5 изображена структура команды буфера выполнения.

166.jpg

Рис. 13-S. Структура команды в буфере выполнения

Команда состоит из кода операции, определяющего ее тип, поля размера, задающего общий размер блока данных, и счетчика, который указывает количество аналогичных команд, следующих за данной. Иногда счетчик оказывается чрезвычайно полезным - например, при задании набора треугольников.

Я весьма упрощенно показал, как действует буфер выполнения. Тем не менее вскоре мы увидим его в действии (стр. 315), и вы получите представление о том, как пользоваться им на практике. Если вам хочется получить более подробную информацию, обращайтесь к документации DirectX 2 SDK.

BviteD выполнения

Практическое использование непосредственного режима

Из документации DirectX 2 SDK выясняется, что для многих объектов непосредственного режима (например, источников света) определяется не только специальная структура данных, но и логический номер, по которому можно обращаться к объекту, а также СОМ-интерфейс для операций с ним. Это кажется излишеством, но если принять во внимание реализацию объектов в Direct3D, такой подход обретает смысл.

Одна из главных задач Direct3D - обеспечение аппаратного ускорения как можно большего количества операций. В этом благородном деле способны помочь логические номера. Предположим, нам потребовалось задать материал объекта. Мы заполняем структуру данных, определяющую материал, и обращаемся к механизму визуализации с требованием создать соответствующий объект. Механизм визуализации обнаруживает, что наши аппаратные средства обладают специальными возможностями по работе с материалами, и поэтому после создания объекта-материала аппаратный драйвер возвращает логический номер для нового материала. Он понадобится нам для манипуляций с созданным объектом, поскольку последний, вероятно, находится в памяти видеоадаптера и мы не сможем обращаться к нему напрямую. Поскольку все начиналось со структуры данных, расположенной в основном адресном пространстве, желательно пользоваться той же самой структурой и для работы с материалом на аппаратном уровне. СОМ-интерфейс объекта позволяет получить информацию от структуры данных и применить ее к объекту. Кроме того, именно логический номер заносится в буфер выполнения при манипуляциях с материалом, потому что работа с ним может происходить на аппаратном уровне. На рис. 13-6 показано, как связаны между собой различные компоненты.

167.jpg

Рис. 13-6. Различные компоненты материала 

Необходимость хранения структур данных, интерфейсов и логических номеров для каждого объекта приводит к чрезмерному усложнению программ, поэтому я скрыл все это в классах C++. Классы содержат операторы преобразования типов, возвращающие логический номер, и реализуют ряд полезных функций СОМ-интерфейсов.

Я разработал класс C++ для каждого объекта DirectSD, с которым собирался работать, а также для буфера выполнения. Эти классы не вошли в библиотеку SdPlus, потому что они используются только в примере DSDEval. Вы найдете их в файлах Direct.cpp и Direct.h в каталоге DSDEval. Вряд ли можно считать их законченными классами; они содержат лишь то, что было необходимо для примера.

Класс буфера выполнения

Я постарался сделать класс для работы с буфером выполнения максимально полезным. Поскольку списки вершин и команд в буфере имеют переменную длину, размер буфера трудно определить заранее. Класс выделяет под буфер выполнения область памяти и вносит в нее элементы до тех пор, пока не кончится свободное место - в этот момент он умирает. Вы подумали, что мне следовало бы заново выделить увеличенную область памяти под буфер, не так ли? Вместо этого я сделал размер буфера большим, чем того требует пример, и включил в программу оператор ASSERT, чтобы отследить переполнение буфера. Функция из файла Direct.cpp, добавляющая один байт в буфер выполнения, выглядит следующим образом:

BOOL CExecute::AddByte(BYTE b) {

// Проверить, существует ли указатель на буфер

if (m_Desc.lpData == NULL) ( LockBufferf) ;

}

BYTE* pBuf = m_dw0ffset + (BYTE*)m_Desc.IpData;

*pBuf++ = b;

m_dw0ffset++;

// Убедиться, что в буфере осталось свободное место ASSERT(m_dw0ffset ' m_Desc.dwBufferSize);

// Дописать код возврата

*pBuf = D3DOP_EXIT;

return TRUE;

)

Данная функция используется при внесении всех структур данных в буфер выполнения. Вы можете модифицировать ее, чтобы буфер автоматически увеличивался в случае его заполнения, и тогда класс станет значительно более гибким.

Несмотря на всю убогость реализации, класс значительно облегчает создание буфера выполнения и работу с ним по сравнению с предназначенными для этого макросами Direct3D.

Матрицы

Нам необходимо определить три матрицы: для мирового преобразования, проекции и вида. Для работы с тремя матрицами непосредственного режима я создал простейший матричный класс CMatrix, напоминающий класс CSdMatrix из библиотеки 3dPlus. Он облегчает создание матриц и обращение с ними, однако при этом необходимо соблюдать некоторую осторожность - я не очень тщательно отнесся к инкапсуляции данных. В сущности, вы можете по своему усмотрению изменить элементы матрицы, а затем вызвать приведенную ниже функцию Update для внесения изменений в настоящий объект-матрицу:

void CMatrix::Update() {

if(m_hMatrix && m_pID3D) {

HRESULT hr = m_pID3D-'SetMatrix(m_hMatrix, this);

ASSERT(SUCCEEDED(hr)) ;

} }

Хотя подобный подход может показаться странным, он достаточно эффективен и позволяет избежать промежуточных обновлений объекта в случае изменения сразу нескольких элементов.

О трех матрицах непосредственного режима стоит рассказать подробнее. Матрица мировых координат вначале представляет собой единичную матрицу и, по всей вероятности, таковой и останется, если ваш макет будет неподвижен. Перемещение макета может осуществляться путем изменения мировой матрицы - именно это и происходит в примере D3DEval, как мы вскоре убедимся (стр. 320). Исходная мировая матрица выглядит следующим образом:

"1000" 0100 0010 0001

Матрица вида контролирует положение камеры и в исходном состоянии обычно совпадает с матрицей переноса вдоль оси z. В своем примере я постарался воспроизвести то, что мы делали в предыдущих примерах. Камера сдвигается на небольшое расстояние вдоль оси z (в область отрицательных значений). Вот как выглядит матрица из примера, устанавливающая камеру в точке О, О, -2:

"1000 0100 0010 0021

Проекционная матрица оказывается значительно интереснее. Чтобы добавить в изображение перспективу и преобразовать пространственные координаты в двумерную систему пикселей окна, необходимо воспользоваться матрицей, которая пересчитывает координаты х и у, деля их на координату z. Проекционная матрица также задает масштабный коэффициент - он определяет, сколько экранных пикселей занимает конкретное расстояние в единицах модели при его проецировании на экран. Проекционная матрица из нашего примера выглядит следующим образом:

"20 00" 02 00 00 11 0 0 - 1 0_

Если вам это покажется совершенно очевидным, я снимаю перед вами шляпу. Чтобы посмотреть, как все происходит, возьмем вектор воображаемой точки (х, у, z, w) и умножим его на матрицу. Результат будет таким:

х' = 2х

У' = 2у

Z' = 2 -W W' = Z

Нормализуем результат, разделив его на z:

х' = 2х / z

у' = 2у / z

z' = (z-w) / z

w' = 1

Если предположить, что исходная точка была нормализована (w = 1), то эти выражения можно еще немного упростить:

х' = 2х / z у' = 2у / z z' = (z-1) / z

w' = 1

Потрясающе, Найджел! Теперь все стало гораздо понятнее. Допустим, смысл происходящего не до конца очевиден, но становится ясно, что координаты х и у делятся на z - это дает нам необходимое преобразование перспективы. Кроме того, видно, что х и у умножаются на коэффициент 2, что изначально входило в мои намерения (хотя я и не упоминал об этом).

Практическое использование непосредственного режима

Если вам захочется воспользоваться какой-нибудь другой проекцией, обращайтесь за помощью к справочным пособиям.

Приложение D3DEval

Пример D3DEval показывает, как вывести простейшую трехмерную фигуру в оконном или полноэкранном режиме с помощью монохромного или RGB-драйвера (на цветной вкладке показан внешний вид приложения D3DEval). Вы можете выбрать размер окна или (в полноэкранном режиме) разрешение экрана. Подготовительная часть этого приложения очень похожа на DDEval из главы 12, поэтому я опишу лишь те фрагменты кода, которые относятся к Direct3D.

Чтобы как можно полнее исследовать непосредственный режим Direct3D при минимальном объеме кода, я решил воспользоваться единственным объектом в макете с одним источником освещения (не считая рассеянного света), одним материалом для фона и одним материалом для объекта. С каждой новой итерацией к мировой матрице применяется преобразование поворота, чтобы макет вращался. Как и в приложении DDEval, при воспроизведении макета на экране отображается количество кадров в секунду.

В программе присутствует несколько директив #if, которые позволяют задать различные параметры работы - например, рисование проволочного каркаса вместо твердого тела.

Наш объект состоит из четырех вершин; это наименьшее количество вершин, образующих объемное тело. Задается всего один материал, так что объект имеет один цвет. Фон тоже выбран одноцветным. Довольно интересно проследить за цветовыми ограничениями, поскольку в системе с 256 цветами можно просмотреть содержимое системной палитры (с помощью утилиты Syspal из каталога Tools на диске CD-ROM) и увидеть, какие из ее элементов заняты.

Код программы состоит из двух основных фрагментов. Первый из них настраивает механизм визуализации, ракурс, освещение и фон. Второй фрагмент воспроизводит макет на экране. Поскольку оба фрагмента получились достаточно длинными, я разбил их на небольшие этапы и последовательно рассмотрел каждый из них. В программе используются классы из 4)айла Direct.cpp. Большая часть функций классов выглядит очень просто, поэтому я не стану рассматривать функции классов и сразу перейду к общей логике программы.

Подготовка

Начнем с краткого изучения фрагмента, в котором создаются окно и поверхности DirectDraw, а также выбирается оконный или полноэкранный режим работы:

BOOL CTestWnd::Create(TESTINFO* pti) {

// Создать объект DirectDraw m_pDD = new CDirectDraw;

BOOL b = m_pDD-'Create () ;

ASSERT(b) ;

// Зарегистрировать класс для тестового окна

CString strClass =

AfxRegisterWndClass(CS_HREDRAW   CS_VREDRAW,

::LoadCursor(NULL, IDC_ARROW), (HBRUSH)::GetStockObject(GRAY_BRUSH)) ;

// Определить стиль и размер окна DWORD dwStyle = WS_VISIBLE I WS_POPUP;

RECT re;

if (m_pTI-'bFullScreen) {

re.top =' 0;

re.left = 0;

re.right =' ::GetSystemMetrics(SM_CXSCREEN);

re.bottom = ::GetSystemMetrics(SM_CYSCREEN);

} else { // Оконный режим

dwStyle  = WS_CAPTION   WS_SYSMENU;

re.top = 50;

re.left = 50;

re. bottom = re. top + m_pTI-'iHeight;

re. right = re. left + m_pTI-'iWidth;

// Настроить окно, чтобы его клиентная область

// имела требуемые размеры

::AdjustWindowRect(&rc, dwStyle, FALSE);

'

// Создать окно.

// В программе нет обработчика сообщения WM CREATE,

// поэтому ничего особенного здесь не происходит. /

if (!CreateEx(0,

strClass,

"DirectSD Window",

dwStyle,

re.left, re.top,

re.right - re.left, re.bottom - re.top,

m_pTI-'pParent-'GetSafeHwnd () ,

NULL)) ( return FALSE;

}

// Обеспечить отображение окна на экране UpdateWindowf) ;

// Задать экранный режим объекта DirectDraw, // создать первичный и вторичный буфера и // при необходимости - палитру if (m_pTI-'bFullScreen) {

b = m_pDD-'SetFullScreenMode (GetSafeHwnd () , m_pTI-'iWidth,

m_pTI-'iHeight,

m pTI-'iBpp) ;

} else { b = m_pDD-'SetWindowedMode (GetSafeHwndO ,

m_pTI-'iWidth,

m_pTI-'iHeight) ;

} ASSERT(b) ;

Практически это тот же самый код, что и в примере DDEval. Он создает первичный и вторичный буфера и при необходимости - палитру. Теперь давайте рассмотрим оставшуюся часть подготовительного кода.

// Создать объект Direct3D на основе поверхностей

DirectDraw m_pD3D = new CDirect3D();

b = m_ D3D-'Create (m_pDD) ;

ASSERT(0);

/ / Задать режим освещения. При этом также назначается // аппаратный или программный драйвер и создается Z-буфер if (pti-'iLightMode ==1) (

b = m_pD3D-'SetMode(D3DCOLOR_MONO) ;

} else {

b = m_pD3D-'SetMode(D3DCOLOR_RGB) ;

} ASSERT(b) ;

// Получить указатели на интерфейсы механизма D3D

// и устройства

m_pIEngine = m pD3D-'GetD3DEngine () ;

ASSERT(m_pI Engine);

m_pIDevice = m_pD3D-'GetD3DDevice () ;

ASSERT(m_pIDevice) ;

Объект Direct3D создается на основе буферов DirectDraw. По выбранному режиму освещения определяется нужный модуль освещения. Наконец, мы получаем указатели на интерфейсы выбранного механизма Direct3D и устройства. Эти интерфейсы применяются для создания всех остальных объектов и управления

ими.

// Создать ракурс

HRESULT hr = m_pIEngine-'CreateViewport(&m_pIViewport, NULL) ;

ASSERT(SUCCEEDED(hr));

ASSERT(m_pIViewport) ;

// Присоединить ракурс к устройству 

hr = m_pIDevice-'AddViewport (m_pIViewport) ;

ASSERT(SUCCEEDED(hr));

Созданный объект ракурса закрепляется за устройством. Последнее может содержать несколько ракурсов, но в нашей программе используется только один.

// Задать конфигурацию ракурса.

// Примечание: Некоторые действия связанные с выбором

// масштабных коэффициентов и т. -д.,

// будут повторно выполнены

// позднее, при настройке проекционной матрицы.

D3DVIEWPORT vd;

memsetl&vd, 0, sizeof(vd));

vd.dwSize = sizeof(vd); // Размер структуры

// Определить область ракурса на устройстве vd.dwX =0; // Левая сторона vd.dwY = 0; // Верх

vd.dwWidth = m_pTI-'iWidth; // Ширина vd.dwHeight = m_pTI-'iHeight; // Высота

// Задать масштаб, чтобы ракурс имел размер 2х2

// единицы модели

vd.dvScaleX = D3DVAL (m_pTI-'iWidth) / D3DVAL(2.0);

vd.dvScaleY = D3DVAL (m_pTI-'iHeight) / D3DVAL(2.0);

// Установить максимальные значения координат х и у

// равными 1,

// чтобы начало координат находилось в центре,

// а координаты х и у

// принимали значения из интервала от -1 до +1,

//то есть, от -интервал/2 до + интервал/2

vd.dvMaxX = D3 DVAL(1.0);

vd.dvMaxY = D3DVAL(1.0);

// Задать интервал значений по оси z vd.dvMinZ = D3DVAL(-5.0);

vd.dvMaxZ = D3DVAL(100.О);

// Применить параметры к ракурсу hr = m_pIViewport-'SetViewport (&vd) ;

ASSERT(SUCCEEDED(hr)) ;

Мы заполняем структуру D3DVIEWPORT, чтобы задать исходное состояние ракурса - используемую им физическую область устройства и значения масштабных коэффициентов для осей х и у. Я выбрал область размером 2х2 единицы и установил начало координат в центре, чтобы значения координат х и у лежали в интервале от -1 до +1. Кроме того, следует задать интервал координат по оси z - это важно при определении видимых объектов. Объекты, находящиеся за максимальным или перед минимальным значением z, не будут воспроизводиться на экране. Подходите к делу разумно - старайтесь подобрать интервал, который как можно точнее ограничивает ваш макет, потому что при этом удается достичь максимальной точности величин из Z-буфера и избежать искажений для объектов, расположенных вблизи от камеры.

После заполнения структуры данных происходит обновление параметров ракурса.

// Задать параметры визуализации.

// Создать буфер выполнения.

CExecute exi (m_pIDevice, m_pIViewport);

// Задать режимы закраски и заполнения. По ним Direct3D // выберет драйверы, которыми он будет пользоваться. // Значения по умолчанию не определены. exi.AddRenderState(D3DRENDERSTATE_FILLMODE,

D3DFILLJ30LID) ;

exi.AddRenderState(D3DRENDERSTATE_SHADEMODE,

D3DSHADE_GOURAUD) ;

exi.AddRenderState(D3DRENDERSTATE_DITHERENABLE, 1) ;

// Разрешить использование Z-буфера exi.AddRenderState(D3DRENDERSTATE_ZENABLE, 1) ;

Сначала мы создаем буфер выполнения, в который будут заноситься последующие команды.

Далее необходимо определить, как будет воспроизводиться макет. Я выбрал сплошное заполнение с закраской Гуро и разрешил смешение цветов в RGB-режиме. Существует множество других опций, которые также можно изменять, однако именно выбранные мной параметры более всего влияют на внешний облик макета. Полный перечень установок визуализации можно найти в документации DirectX 2 SDK. Кроме того, я разрешил использование созданного ранее Z-буфера.

// Создать матрицу мирового преобразования (единичную) m_mWorld.Create(m_pIDevice) ;

exl.AddState(D3DTRANSFORMSTATE_WORLD, m_mWorld) ;

// Создать проекционную матрицу.

// Примечание: Это было сделано ранее, при настройке

// параметров ракурса, однако при выборе режимов

// заполнения

//и закраски данные ракурса были сброшены.

m_mProjection.Create(m_pIDevice);

m_mProjection._ll = D3DVAL(2);

m_mProjection._22 = D3DVAL(2);

m_mProjection._34 = D3DVAL(1);

m_mProjection._43 = D3DVAL(-1);

m_mProjection._44 = D3DVAL(0);

m_mProj ection.Update();

exI.AddState(D3DTRANSFORMSTATE_PROJECTION, m mProjection);

// Задать матрицу вида (положение камеры) m mView.Create(m_pIDevice) ;

m_mView._43 = D3DVAL(2); // Camera Z = -2;

m_mView.Update() ;

exi.AddState(D3DTRANSFORMSTATE_VIEW, m_mView) ;

Приведенный выше фрагмент определяет матрицы (мировую, проекционную и матрицу вида). В начале матрицы являются единичными, благодаря чему в дальнейшем можно заполнять лишь те элементы матриц, которые не совпадают с элементами единичной матрицы. Имена переменных класса, _43 и т. д., взяты из структуры Direct3D под названием D3DMATRIX, для которой класс CMatrix является производным.

// Добавить команду для задания уровня рассеянного освещения exI.AddAmbLight(RGB_MAKE(64, 64, 64));

Интенсивность рассеянного света задается ниже среднего. Обратите внимание на то, что здесь RGB-составляющие лежат в интервале от 0 до 255. В других случаях мы пользуемся цветовыми значениями из интервала от 0.0 до 1.0 - следите за этим.

// Выполнить команды из списка b = exi.Execute();

ASSERT(b) ;

Буфер выполнения передается на конвейер визуализации, чтобы обеспечить установку выбранных нами параметров. Данная функция работает по принципу 'пан или пропал' - если она заканчивается неудачно, то компьютер обычно 'виснет', так что постарайтесь правильно задать параметры команд перед тем, как выполнять Execute для буфера.

// Присоединить источник света к ракурсу m_Lightl.Create(m_pIEngine) ;

m_Lightl.SetColor(0.8, 0.8, 0.8);

m_Lightl.SetType(D3DLIGHT_DIRECTIONAL) ;

m Lightl.SetDirection(1, -1, 1) ;

m_Light1.Update () ;

hr = m_pIViewport-'AddLight (m_Lightl) ;

ASSERT(SUCCEEDED(hr)) ;

В модуль освещения включается направленный источник света. Обратите внимание на функцию Update, которая обновляет состояние физического объекта при помощи параметров, заданных при вызовах предыдущих функций. Источник света испускает белый цвет (точнее, серый) и направлен вниз и внутрь макета, если смотреть из левого верхнего угла.

// Создать материал для фона.

// Примечание: количество оттенков должно быть равно 1, // иначе монохромный драйвер присвоит материалу черный цвет.

// Разумеется, для фонового цвета вполне достаточно одного

// оттенка, так что это лишь позволяет нам избежать // напрасного расходования элементов палитры. m_matBkgnd.Create(m_pIEngine, -m_pIDevice);

m_matBkgnd.SetColor(0.0, 0.0, 0.5); // Темно-синий m matBkgnd.SetShades (1) ; // Только один оттенок hr = m_pIViewport-'SetBackground(m_matBkgnd) ;

ASSERT)SUCCEEDED(hr));

f

Завершающим шагом является выбор материала фона. Если пропустить этот этап, макет будет выводиться на черном фоне. Материал фона должен иметь всего один цветовой оттенок. Если указать более одного оттенка (или воспользоваться принятым в классе значением по умолчанию, равным 16), то монохромный драйвер сделает фон черным. Поскольку освещение не влияет на цвет фона, одного оттенка вполне достаточно, и выбор большего количества лишь приведет к напрасному расходованию элементов палитры.

Теперь у нас есть макет с фоном, освещением, заданным положением камеры и проекционной пирамидой. Не хватает лишь объекта.

Выполнение тестов

Пример D3DEval включает два теста. Первый из них пользуется набором неосвещенных вершин и конвейером визуализации для выполнения преобразований и расчета освещения. Второй тест берет предварительно преобразованные и освещенные вершины и использует только растровый генератор. Мы подробно рассмотрим код первого теста, поскольку такая ситуация часто встречается на практике, а затем в общих чертах посмотрим, какие же отличия возникают, если пропустить этапы обсчета преобразования и освещения в конвейере визуализации.

В каждой итерации теста мы подготавливаем буфер выполнения и многократно выполняем его. Тест запускается таймером, поэтому весь процесс выглядит непрерывным. Давайте начнем с подготовительного кода и рассмотрим его шаг за шагом.

void CTestHnd::Testl() (

// Получить указатели на первичный и вторичный буфера CDDSurface* рВВ = m_pDD-'GetBackBuffer () ;

ASSERT(pBB) ;

CDDSurface* pFB = m_pDD-'GetFrontBuffer();

ASSERT(pFB) ;

// Получить прямоугольник, описывающий первичный буфер RECT rcFront;

if (m_pTI-'bFullScreen) {

pFB-'GetRect (rcFront) ;

} else f

GetClientRect(&rcFront) ;

ClientToScreen(&rcFront) ;

)

// Получить прямоугольник, описывающий вторичный буфер RECT rcBack;

pBB-'GetRect (rcBack) ;

Мы получаем указатели на первичный и вторичный буфера и их размер. Данный фрагмент совпадает с соответствующим местом из примера DDEval.

// Создать список вершин фигуры (некое подобие пирамиды) D3DVERTEX vShape [] = {

{ // xyz вершин

D3DVAL(-0.3), D3DVAL(-0.1), D3DVAL( 0.1),

// xyz нормалей

D3DVAL(-1.0), D3DVAL(-1.0), D3DVAL(-1.0),

// uv текстуры

D3DVAL( 0.0), D3DVAL( 0.0)

}, { D3DVAL( 0.3), D3DVAL(-0.1), D3DVAL( 0.2),

D3DVAL( 1.0), D3DVAL(-1.0), D3DVAL(-1.0),

D3DVAL( 0.0), D3DVAL( 0.0) }, { D3DVAL( 0.0), D3DVAL(-0.3), D3DVAL( 0.3),

D3DVAL( 0.0), D3DVAL(-1.0), D3DVAL( 1.0),

D3DVAL( 0.0), D3DVAL( 0.0) }, { D3DVAL( O.I), D3DVAL( 0.4), D3DVAL( 0.3),

D3DVAL( 0.0), D3DVAL( 1.0), D3DVAL( 0.0),

D3DVAL( 0.0), D3DVAL( 0.0) } };

int nVerts = sizeof(vShape) / sizeof(D3DVERTEX);

Для нашего объекта создается список вершин. Объект содержит четыре вершины, для каждой из которых задается вектор нормали. Он используется в модуле освещения и при наложении покрытий (не рассматриваемых в данном примере).

// Задать материал фигуры CMaterial mShape;

mShape.Create(m_pIEngine, m_pIDevice) ;

mShape.SetColor(0.0, 1.0, 0.0); // Светло-зеленый

Приложение DSDEval '''Щ:.  

Не забывайте о том, что описание материала состоит из структуры данных в адресном пространстве приложения, а также из внешнего объекта, представленного логическим номером.

// Создать буфер выполнения CExecute ex (m_pIDevice, ni_pIViewport) ;

// Добавить в буфер данные о вершинах ex.AddVertices(vShape, nVerts)';

Моя реализация класса для буфера выполнения (CExecute) требует, чтобы заполнение буфера начиналось с вершин.

// Создать матрицу для мирового преобразования поворота CMatrix mRot(m_pIDevice);

double ry = 1; // Градусы вокруг оси у double siny = sinfry * D2R);

double cosy = cos(ry * D2R);

mRot._ll = D3DVAL(cosy);

mRot._13 = D3DVAL(-siny) ;

mRot._31 = D3DVAL(siny);

mRot._33 = D3DVAL(cosy);

mRot.Update() ;

// Команда умножения мировой матрицы

//на матрицу поворота

ex.AddMatMul(m mWorld, m mWorld, mRot);

Матрица включается в буфер выполнения как часть команды, умножающей мировую матрицу на заданную. Результат присваивается мировой матрице. После выполнения этой команды макет поворачивается на один градус вокруг оси у.

// Добавить описание материала в буфер выполнения ex.AddMaterial(mShape) ;

// Добавить команду обработки вершин, чтобы каждая

//из них

// была преобразована и освещена

ex.AddProcess(nVerts, О,

D3DPROCESSVERTICES_TRANSFORMLIGHT  D3DPROCESSVERTICES_UPDATEEXTENTS) ;

Следует отметить, что приведенная выше команда преобразования добавлена в буфер после обновления мировой матрицы, так что поворот мировой матрицы учитывается в нашем преобразовании.

// Добавляем в буфер команды рисования фигур ex.AddTriangle (0, 3, 1) ;

ex.AddTriangle(1, 3, 2);

ex.AddTriangle(2, 3, 0);

ex.AddTriangle(0, 1, 2);

Треугольники должны перечисляться с обходом вершин по часовой стрелке, иначе они будут неверно воспроизведены на экране. Параметры функции AddTriangle представляют собой индексы в списке вершин.

На этом подготовительная часть завершается. Далее мы входим в цикл, который многократно воспроизводит макет во вторичном буфере, вычисляет скорость вывода и переключает буфера, чтобы результат появился на экране:

DWORD dwStart = timeGetTime();

int nFrames = 360;

for (int iFrame = 0; iFrame ' nFrames; iFrame++) (

DWORD dwNow = timeGetTime();

double fps;

if (dwNow == dwStart) {

fps = 0;

} else {

fps = iFrame * 1000.0 /

(double)(dwNow - dwStart) ;

)

// Очистить ракурс (присвоить текущий фоновый // материал) D3DRECT г;

r.xl = 0;

r.yl = 0; // Левый верхний угол r.x2 = m_pTI-'iWidth;

r.y2 = m_pTI-'iHeight; // Правый нижний угол hr = m_pIViewport-'Clear(l, &r, D3DCLEAR_TARGET   D3DCLEAR_ZBUFFER) ;

ASSERT(SUCCEEDED(hr));

#if 1 // Заменить на 0, чтобы убрать с экрана // скорость вывода.

// Отобразить строку со скоростью вывода

// во вторичном буфере.

char buf[64] ;

sprintf(buf, "Frame %2d (%3.1f fps)", iFrame, fps) ;

CDC* pdc = pBB-'GetDC() ;

ASSERT(pdc) ;

pdc-'DrawText (buf, -1, SrcBack, DT_CENTER   DT_BOTTOM I DT_SINGLELINE) ;

pBB-'ReleaseDC(pdc) ;

#endif

// Выполнить буфер BOOL b = ex.Execute();

ASSERT(b) ;

// Переключить буфера

if (m_pTI-'bFullScreen) {

pFB-'Flip () ;

} else {

pFB-'Blt (SrcFront, pBB, SrcBack);

}

\

Если не считать работы с буфером выполнения, данный фрагмент совпадает с примером DDEval.

Расчет преобразований и освещения

Если вы будете самостоятельно выполнять все вычисления для преобразований и освещения, программа значительно упрощается. Разумеется, главное - общий объем кода, который вам придется написать, так что вы вовсе не обязательно окажетесь в выигрыше!

Для растровых генераторов монохромного и RGB-режимов вам придется написать несколько отличающийся код. Растровый генератор RGB-режима осуществляет интерполяцию отдельных RGB-составляющих; генератор монохромного режима интерполирует только оттенки одного цвета, интенсивность которого изменяется от черного до белого с несколькими промежуточными значениями. Если вы пользуетесь растровым генератором RGB-режима, цвета вершин указываются непосредственно в виде RGB-троек, при этом допускаются любые цветовые величины. Для нашего теста я сделал одну вершину красной, другую - зеленой, а третью - синей. Тест показывает, каким образом растровый генератор RGB-режима производит плавные переходы между цветами. Определение вершин для растрового генератора RGB-режима выглядит следующим образом:

void CTestWnd::Test2RGB() {

D3DTLVERTEX vShape [] = { { // x, у, z, 1/w

D3DVAL(10), D3DVAL(10), D3DVAL(2), D3DVAL(1),

// Цвет объекта и зеркальный цвет

RGBA_MAKE(255, О, О, 255),

RGBA_MAKE(255, 255, 255, 255),

// u, v текстуры

D3DVAL(0), D3DVAL(0) }, { D3DVAL(m_pTI-'iWidth - 10), D3DVAL ( 10),

D3DVAL(2), D3DVAL(1),

RGBA_MAKE(0, 0, 255, 255),

RGBA_MAKE(255, 255, 255, 255),

D3DVAL(0), D3DVAL(0)

},

{ D3DVAL( 10), D3DVAL(m_pTI-'iHeight - 10), D3DVAL(2), D3DVAL(1), RGBA_MAKE(0, 255, 0, 255), RGBA_MAKE(255, 255, 255, 255), D3DVAL(0), D3DVAL(0) } };

int nVerts = sizeof(vShape) / si'2eof (D3DTLVERTEX) ;

Описание каждой вершины состоит их экранных координат, выраженных в виде х, у, z и 1/w, за которыми следует простой и зеркальный цвета вершины, а затем - параметры и и v для текстуры, которая в данном примере не используется.

Подготовка буфера выполнения выглядит чрезвычайно просто:

// Создать буфер выполнения CExecute ex (m_pIDevice, m_pIViewport) ;

// Добавить данные о вершинах в буфер ex.AddVertices(vShape, nVerts);

// Добавить команды для вершин, чтобы каждая //из них была преобразована и освещена ex.AddProcess(nVerts, О, D3DPROCESSVERTICES_COPY I D3DPROCESSVERTICES_UPDATEEXTENTS);

// Добавить команды для рисования фигуры ex.AddTriangle(О, 1, 2);

Я не привожу копию экрана, потому что оттенков серого недостаточно для передачи хорошей картинки. Вам придется самостоятельно запустить D3DEval и посмотреть, как выглядит окно приложения. Оставшаяся часть кода совпадает с примером Test1, начинающимся на стр. 318.

При использовании растрового генератора монохромного режима цвет грани задается с помощью материала:

void CTestWnd::Test2MONO() {

CMaterial mShape;

mShape.Create(m_pIEngine, m_pIDevice) ;

mShape.SetColorf0.0, 1.0, 0.0); // Светло-зеленый

Теперь можно описать вершины. Их цвета задаются с использованием только синей составляющей цветовой структуры, ее значение определяет интенсивность оттенка цвета материала. Я постарался подобрать вершины так, чтобы добиться наибольшего разнообразия:

D3DTLVERTEX vShape [] = { { // x, у, z, 1/w

D3DVAL(10), D3DVAL(10), D3DVAL(2), D3DVAL(1), // Цвет объекта и зеркальный цвет

RGBA_MAKE(0, 0, 64, 0), RGBA_MAKE(0, 0, 64, 0), // u, v текстуры D3DVAL(0), D3DVAL(0)

),

( D3DVAL(m_pTI-'iWidth - 10) , D3DVAL( 10), D3DVAL(2), D3DVAL(1),

RGBA_MAKE(0, 0, 255, 0), RGBA_MAKE(0, 0, 255, 0), D3DVAL(0), D3DVAL(0)

},

( D3DVAL( 10), D3DVAL(m_pTI-'iHeight - 10),

D3DVAL(2), D3DVAL(1),

RGBA_MAKE(0, 0, 0, 0), RGBA_MAKE(0, 0, 0, 0),

D3DVAL(0), D3DVAL(0) } i ;

Перед обработкой вершин необходимо внести в буфер выполнения описание материала:

// Создать буфер выполнения CExecute ex (m_pIDevice, m_pIViewport) ;

// Добавить в буфер данные о вершинах ex.AddVertices(vShape, nVerts);

// Добавить описание материала в буфер выполнения ex.AddMaterial(mShape) ;

// Добавить команду обработки вершин, чтобы каждая // из них

// была преобразована и освещена

ex.AddProcess(nVerts, О, D3DPROCESSVERTICES_COPY  D3DPROCESSVERTICES_UPDATEEXTENTS) ;

Вся оставшаяся часть кода совпадает с тестом для RGB-режима. Как видите, с растровым генератором RGB-режима работать очень просто. Для монохромного режима требуется несколько больше усилии, потому что в нем считается, что каждая грань имеет всего один цвет. Для получения нормальной скорости работы необходимо сгруппировать в бу4)ере выполнения все грани одного цвета.

Чего не хватает?

Краткий обзор непосредственного режима Direct3D лишь в общих чертах показывает, как он работает и как пользоваться буферами выполнения. Я пропустил множество интересных возможностей и не затронул важные вещи (например, работу с текстурами). Как было сказано в начале главы, DirectSD - обширная тема. Тем не менее если вы заинтересовались ей, то моя программа окажется хорошим подспорьем для собственных экспериментов.

Наверняка вы успели заметить, что страниц справа почти не осталось - перед вами последняя глава книги. Возможно, вы думаете, что она просто не имеет права быть последней - мы ничего не узнали о том, как создаются многопользовательские виртуальные миры в Internet, как вывести три проекции вашего дома в одном окне, как нарисовать руку робота, состоящую из нескольких частей, и многое другое. Если вы прочитали всю книгу и просмотрели документацию DirectX 2 SDK, то вы вполне готовы к самостоятельному изучению. Я уже не должен вам помогать, потому что вы знаете ничуть не меньше меня.

Дамы и господа, вечер продолжается!

Назад       Содержание       Вперёд