ПРОГРАМИРУЕМ 3D ГРАФИКУ ИСПОЛЬЗУЯ DirectX

Цвет и текстуры

Цвет

Цвет и текстуры

Целый раздел, посвященный цвету, — не слишком ли расточительно? Впрочем, надо же с чего-то начинать, а работа с цветом относится к числу необходимейших навыков. Как было сказано в главе 1, цвет состоит из трех компонентов (красный, зеленый и синий). Значение каждого компонента может изменяться от О до 1. Для определения цвета используется набор RGB-значений, заданных в виде вещественных величин. Например, красный цвет задается как 1.0, 0.0, 0.0, синий цвет — 0.0, 0.0, 1.0 и т. д. Поскольку компилятор легко преобразовывает целые константы в double, иногда в тексте встречаются цвета, заданные в следующем виде: 1,0,0 (красный). Нужно лишь помнить, что эти значения всегда интерпретируются как вещественные.
ПРИМЕЧАНИЕ
Возможно, программисты для Windows станут жаловаться, что их заставляют учиться какому-то новому способу задания цвета. Я даже подумывал о создании класса C++ с несколькими конструкторами, облегчающего работу с цветами. Однако в итоге я все же решил, что это лишь вызовет дополнительные сложности, а особой пользы не принесет. Если вы привыкли к тому, что красный цвет задается тройкой 255, О, О, то вам придется все цветовые компоненты разделить на 255.
Самое простое, что можно сделать с цветом, — присвоить один цвет всему объекту. Давайте сразу посмотрим, как это делается. Выполните команду Edit ¦ Color в приложении Sample. На экране появляется стандартное окно диалога Color, в котором можно выбрать цвет для текущего выделенного объекта:
void CMainFrame::OnEditColor() {
ASSERT(m_pCurShape) ;
CColorDialog dig;
if (dIg.DoModal() i= IDOK) return;
m_pCurShape-»SetColor (GetRValue (dlg.m_cc. rgbResult) /
255.0,
GetGValue(dig.m cc.rgbResult) /
255.0,
GetBValue(dig.m_cc.rgbResult) /
255.0);
}
Окно диалога Color возвращает выбранный цвет в виде структуры COLORREF (rgbResult). Макросы с именами GetRValue, GetGValue и GetBValue извлекают из нее отдельные компоненты красного, зеленого и синего цветов, которые затем преобразуются в вещественные значения, лежащие в диапазоне от 0.0 до 1.0. Процедура завершается вызовом функции C3dShape::SetColor:

BOOL C3dShape::SetColor( double r, double g, double b)

{

ASSERT(m_pIMeshBld) ;

m_hr = m_pIMeshBld-»SetColorRGB(r, g, b) ;

return SUCCEEDED(m hr) ;

Как видите, ничего особенного здесь не происходит. Интерфейс построения сеток содержит функцию SetColorRGB, которая и выполняет всю основную работу. Просмотрев документацию по DirectX 2 SDK, вы увидите, что интерфейс построения сеток содержит также функцию SetColor, которая получает в качестве аргумента структуру D3DCOLOR. Я не стал включать поддержку этой структуры в класс C3dShape, но если она вам понадобится, то реализация будет вполне тривиальной. Если вам приходится много работать с цветами, можно создать специальный класс C++, производный от D3DCOLOR, по аналогии с классом C3dVector, производным от D3DVECTOR. В такой класс разумно включить преобразование значений COLORREF, операцию сложения и т. д.

Давайте проделаем что-нибудь поинтереснее и раскрасим случайными цветами все грани объекта (Рисунок 8-1 и раскрашенный самолет на цветной вставке). Зачем? Мне показалось, что вам будет интересно загрузить объект, первоначально нарисованный в 3D Studio, и проследить за созданием его граней. Случайная раскраска превосходно демонстрирует общую идею. Класс C3dShape содержит функцию SetFaceColor, которая применяет цвет к отдельной грани и упрощает процесс случайной раскраски всего объекта:

void CMainFrame::OnEditRandcolor() (

ASSERT(m_pCurShape) ;

int iFaces = m_pCurShape-»GetFaceCount () ;

for (int i = 0; i « iFaces; i++) { m_pCurShape-»SetFaceColor (i,

rand() % 100) / 100.0, rand() % 100) / 100.0, randf) % 100) / 100.0) ;

}

I

/b> ед¦:" Глава 8. Цвет и текстуры

Рисунок. 8-1. Самолет со случайной раскраской граней

Цвет и текстуры

Цвет фрейма

Возможно, вам показалось, что мы перебрали все варианты применения цвета. Однако на самом деле осталась еще одна методика, простая, но довольно важная. Когда мы занимались созданием фигур в главе 4, я уже говорил о том, что один визуальный элемент может быть присоединен сразу к нескольким фреймам, что фактически позволяет создавать копии фигуры с минимальными накладными расходами. Такая методика хорошо работает при создании 97 объектов одинакового цвета, но что делать, если нам понадобятся объекты одинаковой формы, но разных цветов?


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

Цвет фрейма задается функцией C3dShape::SetFrameColor. Кроме того, вы должны разрешить использование цвета фрейма, вызывая функцию C3dFrame::SetMaterialMode(D3DRMMATERIAL_FROMFRAME). В приложении Color работа с ней продемонстрирована на примере функции, которая создает красную сферу и затем копирует ее, варьируя цвет (команда Edit Color from Frame). Ниже приведен фрагмент функции, в котором создается красная сфера и первая копия:

Цвет

/b>

void CMainFrame::OnEditClrframe()

t

// Создать первую сферу C3dShape* pShape = new C3dShape;

pShape-»Create3phere (1) ;

pShape-»SetColor (1, 0, 0); // Красный цвет m_pScene-»AddChild(pShape) ;

m p3cene-»m_ShapeList. Append (pShape) ;

MakeCurrent(pShape) ;

pShape-»SetName ("Red master") ;

// Создать копию C3dShape* pClonel = m_pCurShape-»Clone () ;

m_pScene-»m_ShapeList. Append (pClonel) ;

m_pScene-»AddChild(pClonel) ;

pClonel-»SetPosition(-2, 0, 0) ;

// Задать цвет, связанный с фреймом копии pClonel-»SetMaterialMode(D3DRMMATERIAL_FROMFRAME) ;

pClonel-»SetFrameColor(0, 1, 0); // Green pClonel-»SetName ( "Green clone") ;

}

Чтобы задать цвет объекта-копии, достаточно связать этот цвет с фреймом копии (вместо визуального элемента) и вызвать функцию SetMaterialMode с аргументом D3DRMMATERIAL FROMFRAME.

Свойства материала

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


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

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

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

/b> lly Глава 8. Цвет и текстуры

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

Чтобы объект выглядел блестящим, можно сузить угол зеркального отражения; для имитации пластиковой поверхности следует расширить этот угол. На практике нам не приходится задавать конкретное значение угла. Вместо этого мы указываем степень, в которую должен возводиться косинус этого угла при вычислении интенсивности отраженного луча. Проще говоря, маленькое значение (скажем, 5) дает пластиковую поверхность, а высокое (например, 300) — металлическую.

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

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


Задавая параметры материала, можно до некоторой степени управлять тем, насколько блестящим будет выглядеть материал и будет ли он самостоятельно излучать свет. Интерфейс IDirect3DRMMaterial содержит три функции, определяющие основные свойства материала: цвет испускаемого им света (если он имеется), цвет зеркального отражения и показатель степени для уравнения зеркального отражения.

Вполне возможно, что от всего сказанного вы чувствуете себя слегка не в себе. В таком случае давайте рассмотрим пример. На Рисунок 8-2 изображены сферы с различными параметрами (показателями степени и свойствами излучаемого света). Однако на печати уловить отличия между ними довольно трудно, так что я советую присмотреться к сферам на экране вашего компьютера (кроме того, посмотрите на полноценный вариант этого рисунка на цветной вкладке). Запустите приложение Color и выполните команду Edit ¦ Materials.

Рисунок. 8-2. Сферы с различными свойствами материала

Цвет и текстуры

Свойства материала '^il 189

Посмотрите на верхний ряд. Левый шар был создан со свойствами материала, принятыми по умолчанию. Центральный шар излучает красный свет, так что он выглядит как бы светящимся. Правый шар тоже излучает красный свет, но он обладает более высоким показателем степени зеркального отражения (400), отчего его поверхность становится более похожей на металл. Все шары в среднем ряду имеют белый цвет, но их показатели степеней равны 3, 10 и 50. В нижнем ряду все шары красные, а показатели степени равны 100, 500 и 2000.

Чтобы немного облегчить работу со свойствами материала, я создал класс C3dMaterial:

class C3dMaterial : public C3d0bject

{

public:

DECLARE_DYNAMIC(C3dMaterial) ;

C3dMaterial() ;

virtual --C3dMaterial () ;

void SetEmissiveColor(double r, double g, double b) ;

void SetSpecularPower(double p);

void SetSpecularColor(double r, double g, double b) ;

IDirect3DRMMaterial* Getlnterface() {return m_pIMat;}

protected:

IDirect3DRMMaterial* m_pIMat;

};

Ниже приведен фрагмент кода, в котором создается красный шар в середине нижнего ряда на Рисунок 8-2. Это является типичным примером использования класса C3dMaterial:


void CMainFrame::OnEditMaterials() f

pShape = new C3dShape;

p3hape-»CreateSphere (1) ;

pShape-»SetName ( "Specular power 500") m_pScene-»AddChild(pShape) ;

m_p3cene-»m_ShapeList .Append (pShape) ;

pShape-»SetPosition(0, -2, 0);

pShape-»SetColor (1, 0, 0) ;

C3dMaterial m7;

m7.SetSpecularPower(500) ;

pShape-»SetMaterial (&m7) ;

Растровые изображения

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

/b> 1У Глава 8. Цвет и текстуры

качестве фона для макета, а несколько позже научимся пользоваться ими для создания текстур. На момент написания книги, функции Direct3D могли работать только с изображениями в формате Public Pixel Map (PPM), поэтому я создал класс C3dlmage, который загружает растры Windows (BMP) из дискового файла или из ресурсов приложения. Наличие такого класса заметно облегчает эксперименты с изображениями, поскольку в любой Windows-системе найдется хотя бы одна программа для создания растров.

Растровые файлы Windows имеют различный формат. Во всех растровых изображениях, находящихся на прилагаемом диске CD-ROM, на один пиксель отводится 8 бит — это значит, что такие растры могут иметь не более 256 цветов. На практике количество цветов на 256-цветном экране оказывается еще меньше. Чтобы понять причину, давайте посмотрим, что происходит, когда изображение используется в качестве текстуры.

Допустим, некоторый пиксель текстуры имеет зеленый цвет. При воспроизведении поверхности на экране зеленый пиксель может быть окрашен в один из многих оттенков зеленого, в зависимости от освещения поверхности. Другими словами, для каждого цвета, входящего в изображение, механизму визуализации приходится создавать несколько оттенков. Если ваша аппаратура не имеет реальных ограничений по цветам (видеосистема отображает до 24 бит/пиксель), беспокоиться не о чем. Тем не менее, если ваше приложение должно работать в 256-цветном режиме, следует продумать распределение цветов палитры. Если изображения включают много цветов, то при их воспроизведении окажутся занятыми многие элементы палитры. Чтобы добиться наилучшего эффекта, следует поэкспериментировать с цветами перед тем, как приказать художникам нарисовать тысячу растровых изображений.


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

Самое простое, что можно сделать с растровым изображением, — превратить его в фон для макета. Приложение Color содержит команду меню Edit [ Background Image, которая позволяет загрузить любой растр и сделать его фоном для текущего макета. Функция выглядит предельно просто:

void CMainFrame::OnEditBkgndImg() {

C3dlmage* pimg = new C3dlmage;

if ( !pImg-»Load() ) { delete pimg;

return;

}

ASSERT(m_p3cene) ;

m_pScene-»m_ImgList .Append (pimg) ;

m_pScene-»SetBackground (pimg) ;

t

Мы создаем новый объект C3dlmage и вызываем его функцию Load без аргументов. Функция Load выводит окно диалога File Open с фильтром, настроенным на отображение только BMP-файлов. После того как пользователь выберет

•!;ЙЙ^,

Растровые изображения ''vis 191

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

После того как растр загружен, он присоединяется к списку изображении текущего макета, а затем включается в макет в качестве текущего фона.

ПРИМЕЧАНИЕ

Очень важно, чтобы вы случайно не удалили изображение во время его использования. Структура D3DRMIMAGE не является СОМ-объектом и не имеет счетчика обращений, так что вам придется самостоятельно следить за всеми загруженными изображениями. Если вы будете пользоваться классом СЗШтаде, то сможете предохранить свои объекты СЗсПтаде, включая их в список изображений макета. При этом ваши растры останутся в живых до удаления макета. Предупреждение относится и к текстурам, которые аналогичны в этом отношении растровым изображениям.

Запустите приложение Color и задайте в нем фоновое изображение. Обратите внимание — фоновый растр растягивается, чтобы заполнить весь макет, поэтому его пропорции изменяются в зависимости от формы окна приложения.


Должен признаться, что при реализации функции C3dScene::SetBackground я немного смошенничал. На самом деле механизм визуализации требует, чтобы в качестве фона была задана текстура, но мне показалось, что логичнее будет ограничиться растровым изображением. Функция реализована так, что ей достаточно передать растр, а текстура создается без вашего участия. На Рисунок 8-3 показан пример простого макета с одним объектом (танком) и фоновым изображением — лужайкой перед моим домом. Наличие фона заметно украшает макет.

Рисунок. 8-3. Танк с фоновым изображением

Цвет и текстуры

/b> lip" Глава 8. Цвет и текстуры

Текстуры

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

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

Растровые изображения, на основе которых строятся текстуры, должны обладать определенными атрибутами. Самый важный из них — размер изображения. Каждая сторона должны состоять из пикселей, количество которых равно целой степени двойки. Следовательно, изображения 32х32, 128><256 или 4х4 могут использоваться для создания текстур, а изображение размером 320х240 — нет. Данное ограничение призвано повысить производительность при воспроизведении текстур. Разумеется, механизм визуализации может взять любое изображение и растянуть его так, чтобы стороны приняли требуемые размеры, однако разработчики решили, что вам лучше сделать это самостоятельно, чтобы максимально сохранить степень контроля за качеством изображения.


Раз уж разговор зашел о качестве, я бы хотел напомнить вам, что если ваше приложение должно работать в системе с 256 цветами (которые на сегодняшний день являются самыми распространенными), необходимо но возможности ограничить количество цветов в текстуре. Как было сказано в разделе «Растровые изображения» на стр. 190, я постарался обойтись восемью цветами (принятым по умолчанию количеством цветов в текстуре). При этом механизм визуализации может более гибко пользоваться системной палитрой для представления всех оттенков, необходимых для воспроизведения всего макета. Разумеется, некоторые объекты обладают похожими цветами, и это также помогает снизить общие требования. Поскольку не существует «железного» правила относительно того, сколько цветов нужно для той или иной текстуры или изображения, я бы посоветовал немного поэкспериментировать с числом цветов перед тем, как приступать к созданию окончательного варианта графики.

Механизм визуализации содержит функции, которые ограничивают количество цветов, используемых на устройстве воспроизведения (по умолчанию — 32) и количество оттенков в текстуре (по умолчанию — 16). Разумеется, если все ваши пользователи работают с 24-битным цветом, вам не нужно беспокоиться об этих проблемах, поскольку механизм визуализации сгенерирует все необходимые цвета. При желании можно изменить стандартное количество цветов в текстуре и количество создаваемых оттенков одного цвета, функциями C3dTexture::SetColors и C3dTexture::SetShades соответственно.

Текстуры 'W 193

Наложение текстур

В компьютерном мире нет ничего простого (возьмите хотя бы OLE — людей, которые разбираются в нем, постоянно не хватает). К счастью, наложить текстуру гораздо легче, чем внедрить в приложение поддержку OLE, и все же придется немало потрудиться.

Текстура может накладываться на поверхность объекта четырьмя различными способами. Каждый из них связан с отдельным математическим алгоритмом, определяющим, каким образом текстура покрывает поверхность объекта. В простейшем случае (плоское покрытие) текстура в большей или меньшей степени растягивается, чтобы заполнить всю поверхность. При более сложных алгоритмах — цилиндрическом, сферическом и хромовом покрытии — объект фактически «заворачивается» в текстуру. Давайте рассмотрим каждый из четырех способов наложения текстуры на объект.


Плоское покрытие

Плоское покрытие является самым простым способом наложения текстуры на поверхность. Вероятно, его даже не следовало бы называть «покрытием», поскольку на самом деле оно ничего не покрывает, а скорее напоминает раскрашенную декорацию, повешенную перед поверхностью. Начнем с самого тривиального примера — наложения текстуры на объект с одной гранью, с применением плоского покрытия. Приложение Color позволяет вывести на экран грань с текстурой, изображенную на Рисунок 8-4 (команда Edit ¦ Insert Тех Map Face).

Рисунок. 8-4. Текстура, наложенная на повернутую грань

Цвет и текстуры

Грань на Рисунок 8-4 слегка повернута вокруг оси у, и текстура напоминает фотографию, на которую смотрят сбоку. Для получения такого результата к текстуре была дополнительно применена коррекция перспективы. По умолчанию механизм

/h2>

Глава 8. Цвет и текстуры

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

void CMainFrame::OnEditInstxface ()

{

// Создать фигуру с одной гранью //и наложить на нее текстуру C3dShape* pShape = new C3dShape() ;

D3DVECTOR vlist[] = (

(-1.0, -1.0, 0.0},

{ 1.0, -1.0, 0.0},

{ 1.0, 1.0, 0.0},

(-1.0, 1.0, 0.0} };

int iVectors = sizeof(vlist) / sizeof(D3DVECTOR);

int iFaces[] = {4, 0, 3, 2, 1, // Передняя грань 4, 0, 1, 2, 3, // Задняя грань 0);

pShape-»Create (vlist, iVectors, iFaces);

// Раскрасить заднюю грань, чтобы видеть ее pShape-»SetFaceColor(l, О, О, 1); // Синий цвет

// Загрузить текстуру C3dTexture* pTex = new C3dTexture;

if ( !pTex-»Load(IDB_Gl) ) ( return;

} m_pScene-»m_ImgList. Append (pTex) ;

// Присоединить текстуру к передней грани p3hape-»SetFaceTexture(0, pTex) ;

// Создать покрытие.

// Грань имеет размеры 2х2 единицы, поэтому


// мы масштабируем

// текстуру для того, чтобы она поместилась на грани

// ровно один раз. Кроме того, текстура также

// инвертируется,

// чтобы изображение не получилось перевернутым.

C3dWrap wrap;

wrap.Create(D3DRMWRAP_FLAT, NULL,

Текстуры '•$11 195

-1, -1, 0, // Базовая точка О, 0, 1, // Направление О, 1, 0, // Верх О, pTex-»GetHeight() -1,

// Базовая точка текстуры 0.5, -0.5);// Масштаб текстуры

//(с инверсией)

// Наложить покрытие на передний грань wrap.Apply(pShape, 0) ;

pShape-»SetName ("Face") ;

m_pScene-»AddChild(p3hape) ;

m_pScene-»m_ShapeLi St. Append (pShape) ;

MakeCurrent(pShape) ;

}

Вам может показаться, что функция получилась слишком длинной для одной грани, но меньшего кода вряд ли можно ожидать, если последовательно рассмотреть все действия. Первое, что необходимо сделать, — создать сам объект. Мы строим списки вершин и граней, а затем конструируем объект функцией C3dShape::Create. Я намеренно создал объект с двумя гранями, чтобы вы могли развернуть объект и при этом видеть его. Задняя грань окрашена в синий цвет.

Текстура передней грани загружается в новый объект C3dTexture из ресурсов приложения. Растр текстуры включается в приложение точно так же, как и любой другой ресурс — с помощью AppStudio из Visual C++. Поскольку текстура должна находиться в памяти во время работы с ней, инкапсулирующий ее объект C++ включается в список изображений макета, чтобы предотвратить случайное удаление текстуры до уничтожения макета. Затем текстура присоединяется к нужной грани объекта функцией C3dShape::SetFaceTexture.

Тем не менее присоединения текстуры к грани объекта еще недостаточно. Необходимо также определить объект-покрытие, который управляет процессом наложения текстуры на грань. В нашем случае объект C3dWrap создается с аргументом D3DRMWRAP_FLAT, определяющим плоское покрытие. Затем покрытие накладывается на переднюю грань объекта функцией C3dWrap::Apply, аргументами которой является указатель на фигуру и номер грани.

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


Параметры покрытия

На Рисунок 8- 5 изображена текстура, наложенная на грань с применением плоского покрытия.

Держа перед глазами Рисунок 8-5, рассмотрим назначение параметров функции создания покрытия. Первый набор (параметры с третьего по пятый) задает базовую точку текстуры на грани. На Рисунок 8-5 — это -1, -1, 0. Два следующих набора параметров задают вектор направления и верхний вектор; эта пара векто-

/b> ЯЕ Глава 8. Цвет и текстуры

Рисунок* 8-5* Наложение текстуры на грань

Цвет и текстуры

ров определяет ориентацию покрытия по отношению к грани. Вектор направления показывает, как «движется» текстура для того, чтобы закрыть объект, а верхний вектор поворачивает текстуру на определенный угол. Далее необходимо задать положение базовой точки на текстуре. На Рисунок 8-5 базовая точка текстуры находится в левом нижнем углу (базовая точка растрового изображения расположена наверху слева и совпадает с началом координат). Два последних параметра задают масштабные коэффициенты по осям х и у. Чтобы их определить, следует предположить, что размер текстуры равен 1х1. Поскольку грань имеет размер 2х2 единицы, необходимо вдвое растянуть текстуру по каждому направлению, и правильный коэффициент будет равен 0.5. Обратите внимание — поскольку мы выбрали базовую точку текстуры внизу, масштабный коэффициент для оси у становится отрицательным. Я взял базовую точку и масштабный коэффициент с таким расчетом, чтобы изображение правильно накладывалось на грань.

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

Вероятно, труднее всего понять смысл вектора направления. Данный параметр можно рассматривать как направление, в котором необходимо двигать текстуру для того, чтобы «набросить» ее на грань. Лично мне на первых порах пришлось немало повозиться с параметрами покрытия. Я много экспериментировал, пока не убедился, что в полной мере осознал все происходящее — мои усилия окупились при освоении более сложных покрытий, которыми мы вскоре займемся.


Параметры покрытия

/h2>

Коррекция перспективы

Давайте вернемся к коррекции перспективы, о которой уже упоминалось выше на стр.194. Рассмотрим текстуру, которая представляет собой черный крест на фоне белого квадрата. Посмотрим, как эта текстура накладывается на квадратную грань, расположенную под некоторым углом к камере. Исходная ситуация изображена на Рисунок 8-6.

Рисунок. 8-6. Квадратная текстура, которая накладывается на квадратную грань, расположенную под углом к камере

Цвет и текстуры

Чтобы воспроизвести текстуру на грани, обе поверхности (грань и текстура) делятся на треугольники (триангулируются), как показано на Рисунок 8-7.

Грань и текстура, разделенные на треугольники

Цвет и текстуры

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

Как видите, наша грань выглядит так, словно ее сложили вдоль общей стороны двух треугольников. Чтобы справиться с этой проблемой, алгоритм наложения текстуры на поверхность должен выполнять операцию деления вместо простой линейной интерполяции. Разумеется, дополнительное деление замедляет работу, но это — цена, которую приходится платить за реализм.

/h2>

Глава 8. Цвет и текстуры

Рисунок. 8-8. Текстура, наложенная без коррекции перспективы

Цвет и текстуры

Коррекция перспективы осуществляется функцией SetPerspective, входящей в интерфейс построения сеток. Коррекция происходит при каждом вызове функции C3dShape::Create или любой другой функции для создания фигуры, которая обращается к C3dShape::Create. Следовательно, если вы работаете с классом CSdShape, то вам не придется беспокоиться о коррекции перспективы — она включается автоматически.

Наложение разных текстур на смежные грани

Благополучно разобравшись с наложением текстур на одну грань, я решил сделать «кубик с картинками» — куб, на каждую грань которого наложена собственная текстура. Я создал куб функцией C3dShape::CreateCuboid, загрузил шесть различных текстур и наложил их все с использованием одного плоского покрытия. На двух гранях получились вполне нормальные картинки, зато остальные четыре грани содержали хаотическое нагромождение линий.


— Я знаю, сэр, пожалуйста, вызовите меня!

— Да?

— Сэр, нужно использовать разные покрытия для разных направлений! Я подумал точно так же, создал отдельное покрытие для каждого вектора направления и попробовал снова. Опять ничего не вышло. Я оказался в полном замешательстве. Пришлось вступать в переписку с разработчиками механизма визуализации и выяснять, что же было сделано неверно. Оказалось, что сведения о наложенных текстурах сохраняются для вершин, а не для граней. Таким образом, каждый раз, когда я полагал, будто накладываю текстуру на грань, на самом деле механизм визуализации накладывал ее на вершины этой грани. Каждая вершина получала информацию о текстуре, и когда в дальнейшем я пытался наложить другую текстуру на ту же самую вершину (хотя и принадлежащую смежной грани!), она перекрывала старую текстуру и обезображивала предыдущую грань. В результате все правильно происходило лишь для грани с последней наложенной текстурой, а все остальные грани, смежные с ней, оказывались испорченными.

Выход заключается в том, чтобы создать куб из шести граней, не имеющих общих вершин. Затем мы накладываем на эти грани текстуры и получаем нужный результат, как нетрудно убедиться, выполнив в приложении Color команду Edit ¦ Insert Picture Cube. Ниже приведен исходный текст функции, создающей куб с текстурами:

Параметры покрытия

/b>

void CMainFrame::OnEditPiccube()

{

// Создать куб с ребром в 2 единицы и раздельными // гранями double s = 2;

C3dShape* pShape = new C3dShape;

D3DVECTOR vlist[] = (

(-1.0, -1.0, -1.0},

( 1.0, -1.0, -1.0},

{ 1.0, -1.0, 1.0},

{-1.0, -1.0, 1.0), // Нижняя грань

(-1.0, 1.0, -1.0),

{ 1.0, 1.0, -1.0),

{ 1.0, 1.0, 1.0),

(-1.0, 1.0, 1.0), // Верхняя грань

(-1.0, -1.0, -1.0),

(-1.0, 1.0, -1.0),

(-1.0, 1.0, 1.0),

(-1.0, -1.0, 1.0), // Левая грань

{ 1.0, -1.0, -1.0),

{ 1.0, 1.0, -1.0),

( 1.0, 1.0, 1.0),

{ 1.0, -1.0, 1.0), // Правая грань

(-1.0, -1.0, -1.0),

(-1.0, 1.0, -1.0),

{ 1.0, 1.0, -1.0),

{ 1.0, -1.0, -1.0), // Ближняя грань


(-1.0, -1.0, 1.0),

(-1.0, 1.0, 1.0),

{ 1.0, 1.0, 1.0),

( 1.0, -1.0, 1.0) // Дальняя грань

int iVectors = sizeof(vlist) / sizeof(D3DVECTOR);

int iFaces[] = (4, 0, 1, 2, 3,

4, 4, 7, 6, 5, 4, 8, 11, 10, 9, 4, 12, 13, 14, 15, 4, 16, 17, 18, 19, 4, 20, 23, 22, 21, 0);

/b> IJI'*' Глава 8. Цвет и текстуры

p3hape-»Create (vlist, iVectors, iFaces);

for (int i = 0; i « 6; i++) {

// Загрузить текстуру char buf[64] ;

sprintf(buf, "g%d.bmp", i+1) ;

C3dTexture* pTex = new C3dTexture;

m pScene-»m_ImgList .Append (pTex) ;

if (pTex-»Load(IDB_Gl+i) ) (

// Присоединить текстуру к грани p3hape-»SetFaceTexture(i, pTex) ;

// Получить нормаль к грани C3dVector vn = pShape-»GetFaceNormal (i) ;

// Изменить направление вектора нормали, чтобы он // показывал направление покрытия vn = -vn;

// Вычислить произвольный верхний вектор C3dVector vu •= vn.GenerateUp ();

// Создать покрытие, ориентированное по данной

// грани

C3dWrap wrap;

wrap.Create(D3DRMWRAP_FLAT, NULL,

-s/2, -s/2, -s/2, // Базовая точка vn.x, vn.y, vn.z, // Направление vu.x, vu.y, vu.z, // Верх // Базовая точка текстуры О, pTex-»GetHeight () -1, // Масштаб текстуры (с инверсией) 1.0/s, -1.0/s);

// Наложить покрытие на переднюю грань wrap.Apply(pShape, i);

} }

pShape-»SetName ("Picture cube") ;

m_pScene-»AddChild(pShape) ;

m pScene-»m_ShapeList .Append (pShape) ;

MakeCurrent(pShape) ;

)

Параметры покрытия "^l 201

Цилиндрическое покрытие

Давайте рассмотрим следующий вид покрытия, при котором текстура оборачивается вокруг объекта по цилиндрической поверхности. На Рисунок 8-9 изображен пример наложения цилиндрического покрытия на объект.

Рисунок. 8-9. Цилиндрическое покрытие

Цвет и текстуры

Текстура сворачивается в цилиндр, который затем проектируется на поверхность объекта. Я обнаружил, что эта методика придает кроне и стволам моих деревьев более реалистичный вид. На Рисунок 8-10 изображен результат наложения текстур с цилиндрическим покрытием. Вы можете увидеть его на экране, запустив приложение Color и выполнив команду Edit ¦ Insert Тех Map Face.


. Пример наложения текстуры с цилиндрическим покрытием

Цвет и текстуры

/b> ¦Д1^'' Глава 8. Цвет и текстуры

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

void CMainFrame::OnEditTree()

(

// Загрузить текстуры C3dTexture* pTexl = new C3dTexture;

pTexl-»Load(IDB_LEAVES) ;

m_pScene-»m_ImgList .Append (pTexl) ;

C3dTexture* pTex2 = new C3dTexture;

pTex2-»Load(IDB_BARK) ;

m__pScene-»m_ImgList. Append (pTex2) ;

// Создать цилиндрическое покрытие C3dWrap wrap;

wrap.Create(D3DRMWRAP_CYLINDER,

NULL,

0, 0, 0, // Базовая точка

О, 0, 1, // Направление

О, 1, 0, // Верх

О, 0, // Базовая точка текстуры

1, 1); // Масштаб текстуры

// Создать крону и ствол double h = (rand() % 100) / 50.0 + 1.0;

double x = ((rand() % 100) - 50) / 10.0;

double z = ((randf) % 100) - 50) / 10.0;

double у = -2;

C3dShape* pTree = new C3dShape;

pTree-»CreateCone (x, y+h/4, z, h/4, TRUE,

x, y+h, z, 0, FALSE);

m_pScene-»m_ShapeList. Append (pTree) ;

C3dShape* pTrunk = new C3dShape;

pTrunk-»CreateRod(x, y, z,

x, y+h/4, z,

h/20);

m_pScene-»m_ShapeList. Append (pTrunk) ;

pTree-»AddChild(pTrunk) ;

// Наложить текстуры pTree-»SetTexture (pTexl) ;

wrap.Apply(pTree) ;

pTrunk-»SetTexture (pTex2) ;

wrap.Apply(pTrunk) ;

Параметры покрытия '''^

203

pTree-»SetName ("Tree") ;

m_pScene-»AddChild(pTree)

MakeCurrent(pTree) ;

Обратите внимание — для кроны и ствола используется всего один объект C3dWrap. Ориентация и масштаб в обоих случаях совпадают, и нам не пришлось создавать разные объекты для покрытии. В качестве упражнения запустите приложение Color и вставьте в макет дерево. Затем разверните его так, чтобы видеть основание конуса. Можете ли вы объяснить, почему текстура выглядит так странно? Как справиться с этой проблемой?

Сферическое покрытие

Не стоит долго гадать, для чего нам нужно сферическое покрытие — разумеется, мы займемся операцией «Генезис»*. Прежде чем углубляться в волнующие подробности, давайте сразу взглянем на конечный результат. На Рисунок 8-11 показано наложение текстуры на сферу с использованием сферического покрытия (на цветной вкладке имеется более качественный вариант рисунка). Вы можете увидеть его па экране, запустив приложение Color и выполнив команду Edit ¦ Insert A World.


Рисунок. 8-11. Планета, созданная с помощью сферического покрытия

Цвет и текстуры

* Автор имеет ii пилу ;{ наменитый американский телесериал Star Trek, n одном и.ч .')ии:юдо¦1 которого операция «Гснсдис» превращает пустынную планету п райский уголок. — Примеч. пкрев.

/b> Ян1 Глава 8. Цвет и текстуры

На Рисунок 8-12 изображен растр, на основе которого была создана текстура.

. Текстура планеты

Цвет и текстуры

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

void CMainFrame::OnEditInsworld() (

// Создать сферу (планету)

C3dShape* pPlanet = new C3dShape;

pPlanet-»CreateSphere (2) ;

// Загрузить текстуру C3dTexture* pTexl = new C3dTexture;

pTexl-»Load(IDB_WORLD) ;

m_pScene-»m_ImgList. Append (pTexl) ;

// Присоединить текстуру к сфере pPlanet-»SetTexture (pTexl) ;

// Создать сферическое покрытие C3dWrap wrap;

wrap.Create(D3DRMWRAPJ3PHERE,

NULL,

0, 0, 0, // Базовая точка

О, 0, 1, // Направление

О, 1, 0, // Верх

О, 0, // Базовая точка текстуры

1, 1); // Масштаб текстуры

// Наложить покрытие на сферу wrap.Apply(pPlanet) ;

pPlanet-»SetDirection(0.5, 0.8, 0);

pPlanet-»SetName ( "World" ) ;

m_pScene-»AddChild (pPlanet) ;

Параметры покрытия "тЩ: 205

m_pScene-»m_ShapeList.Append (pPlanet) ;

MakeCurrent(pPlanet) ;

}

Хромовые покрытия

Покрытия, с которыми нам приходилось работать до настоящего момента, просто накладывали текстуру на объект в фиксированном положении. В общем случае вид поверхности не зависит от положения объекта (не считая эффектов освещения). Но вдруг объект окажется блестящим? Например, на поверхности хромированного объекта отражается все, что находится вокруг него. При перемещении такого объекта отражения на нем будут незначительно меняться, но вращаться вместе с объектом они не будут.

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


Рисунок. 8-13. Хромовое покрытие

Цвет и текстуры

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

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

/h2>

Глава 8. Цвет и текстуры

объекте. Как бы ни изменялось положение объекта, покрытие накладывается на поверхность объекта так, чтобы сохранить прежнюю ориентацию текстуры по отношению к макету. Возможно, вам покажется, что многократные наложения текстуры будут происходить медленно, но на самом деле это не так — и к тому же у нас нет выбора!

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

Интерфейс IDirect3DRMFrame содержит функцию AddMoveCallback, которая задает функцию, вызываемую при перемещении объекта. Я решил, что для наших целей все же следует избежать возни с уведомляющими сообщениями и вместо этого работать с хромовыми покрытиями по аналогии со всеми остальными. По этой причине я создал класс C3dChromeWrap, производный от C3dWrap, в котором реализована функция косвенного вызова для обработки перемещений фрейма. Код приложения, предназначенный для работы с хромовыми покрытиями, сильно напоминает остальные примеры для работы с покрытиями:


void CMainFrame::OnEditChroroe() {

// Загрузить фигуру C3dShape* pShape = new C3dShape;

if (!pShape-»Load() ) ( delete pShape;

return;

}

NewScene() ;

ASSERT(m_pScene) ;

// Создать хромовое покрытие m__pChromeWrap = new C3dChromeWrap;

m_pChromeWrap-»Create (pShape,

m_wnd3d.GetStage () -»GetCamera () ) ;

// Загрузить фоновое изображение макета C3dlmage* pimg = new C3dlmage;

p!mg-»Load(IDB_CHROME) ;

m_pScene-»m_InigList.Append (pimg) ;

m_pScene-»SetBackground (pimg) ;

// Загрузить текстуру C3dTexture* pTex = new C3dTexture;

pTex-»Load(IDB CHROME);

Параметры покрытия '"^Ц; 207

m_pScene-»m_ImgList .Append (pTex) ;

// Наложить текстуру pShape-»SetTexture (pTex) ;

// Сделать ее очень блестящей C3dMaterial matt-mat.SetSpecularPower (2000) ;

pShape-»SetMaterial (&mat) ;

// Присоединить новую фигуру m_pScene-»AddChild(pShape) ;

m pScene-»m_ShapeList .Append (pShape) ;

MakeCurrent(pShape) ;

// Слегка повернуть фигуру p3hape-»SetRotation (1, 1, 1, 0.03);

(

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

Для фона макета и текстуры выбирается одно и то же растровое изображение, IDB_CHROME. Текстура присоединяется к объекту функцией C3dShape::SetTexture. Обратите внимание — мы не пользуемся функцией C3dWrsp::Apply, как это делалось для других типов покрытий; данная функция вызывается позже, во время движения фрейма. С помощью объекта C3dMaterial мы изменяем отражающие свойства поверхности и придаем ей металлический блеск. Наконец, остается лишь включить фигуру в макет и привести ее во вращение, чтобы продемонстрировать эффект хромирования.


Код класса C3dChromeWrap находится в файле 3dlmage.cpp и состоит из двух основных частей: создание покрытия и наложение его при перемещении объекта. Покрытие задается следующим образом:

BOOL C3dChromeWrap::Create(C3dShape* pShape,

C3dCamera* pCamera) f

if (!C3dWrap::Create(D3DRMWRAP_CHROME,

pCamera,

О, О, О,

О, 1, О,

О, 0, -1,

О, О,

1, -1)) (

/b> Щр* Глава 8. Цвет и текстуры

return FALSE;

// Задать функцию косвенного вызова для перемещения ASSERT(pShape) ;

m_p3hape = pShape;

IDirect3DRMFrame* pIFrame = pShape-»Get!nterface () ;

ASSERT(pIFrame) ;

m hr = pIFrame-»AddMoveCallback-(C3dChromeWrapCallback, - this);

return SUCCEEDED(m hr) ;

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

После создания покрытия следующим шагом является обращение к функции косвенного вызова фрейма AddMoveCallback. Давайте посмотрим, как работает функция косвенного вызова:

static void C3dChromeWrapCallback(IDirect3DRMFrame* pIFrame,

void* pArg, D3DVALUE delta) f

C3dChromeWrap* pThis = (C3dChromeWrap*) pArg;

ASSERT(pThis) ;

ASSERT (pThis-»IsKindOf (RUNTIME_CLASS (C3dChromeWrap) ) ) ;

pThis-»ApplyRelative (pThis-»m_pShape, pThis-»m pShape);

Обратите внимание — это статическая функция, не принадлежащая классу C3dChromeWrap. Соответственно, для нее не определен указатель this. Для передачи указателя this используется второй аргумент функции косвенного вызова, предоставляющий функции доступ к данным класса.


Единственная задача функции C3dChromeWrapCallback — наложение покрытия функцией ApplyRelative. Первым аргументом ApplyRelative является фрейм, на который должно налагаться покрытие, а вторым — фигура, визуальные элементы которой будут изменены при наложении хромового покрытия. Поскольку объект C3dShape содержит и то и другое, аргументы ApplyRelative в нашем случае выглядят одинаково.

Параметры покрытия ^Щ 209

Загрузка объектов с текстурами

Предположим, вы создали в 3D Studio какой-нибудь эффектный объект с текстурой, затем сохранили его в файле 3DS, а текстуру — в растровом файле Windows (BMP-файле). Такой объект можно загрузить вместе с текстурой, необходимо лишь преобразовать файл 3DS в формат . х с помощью специальной утилиты из DirectX 2 SDK. Функция C3dShape::Load берет на себя все хлопоты по загрузке текстуры. Давайте рассмотрим этот процесс более внимательно, поскольку он несколько ненадежен и когда-нибудь у вас наверняка возникнут проблемы. Научившись загружать объекты с текстурами из файлов, мы поймем, как включить те же самые файлы, содержащие объект и текстуру, в число ресурсов приложения и загрузить их более надежным способом.

Загрузка из файлов

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


Как же справиться с этой проблемой? Существуют по крайней мере три решения:

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

• Включить файлы объекта и текстуры в ресурсы приложения и загружать их оттуда.

• Разработать свой собственный файловый формат, в котором данные объекта и текстуры хранятся в одном файле.

Давайте посмотрим, как работает функция C3dShape::Load. Ниже приведен ее полный исходный текст:

const char* C3dShape::Load(const char* pszFileName) {

static CString strFile;

if (!pszFileName ¦¦ !strlen(pszFileName)) { // Вывести окно диалога File Open

/b> ЭД!!5' Глава 8. Цвет и текстуры

CFileDialog dig(TRUE,

NULL,

NULL,

OFN_HIDEREADONLY,

_3DOBJ_LOADFILTER,

NULL) ;

if (dlg.DoModal() != IDOK) return NULL;

// Получить путь к файлу strFile = dlg.m_ofn.IpstrFile;

} else (

strFiie = pszFileName;

)

// Удалить любые существующие визуальные элементы New () ;

// Попытаться загрузить файл ASSERT(m_pIMeshBld) ;

m_hr = m_pIMeshBld-»Load( (void*) (const char*) strFile,

NULL,

D3DRMLOAD_FROMFILE ¦ D3DRMLOAD_FIRST,

C3dLoadTextureCallback,

this);

if (FAILED(m_hr)) { return NULL;

)

AttachVisual(m_pIMeshBld) ;

m strName = "File object: ";

m_strName += pszFileName;

return strFile;

В начале функции Load определяется имя файла. Если имя файла не было передано при вызове функции, оно запрашивается у пользователя в окне диалога File Open. Затем мы удаляем из объекта-фигуры все существующие визуальные элементы и вызываем функцию интерфейса построения сеток для загрузки объекта из заданного файла на диске. Четвертый параметр, C3dLoadTextureCallback, содержит указатель на функцию, которая загружает текстурное изображение. Пятый параметр представляет собой произвольную величину, которая задается пользователем и передается функции, загружающей текстуру. Мы передаем в нем this, указатель на объект CSdShape, поскольку функция загрузки текстуры является статической и не принадлежит классу C3dShape.


Загрузка объектов с текстурами ''^Цё- 211

Давайте рассмотрим функцию косвенного вызова, в которой происходит загрузка текстуры:

static HRESULT C3dLoadTextureCallback(char* pszName, void* pArg,

LPDIRECT3DRMTEXTURE* ppITex)

{

C3dShape* pThis = (C3dShape*)•pArg;

ASSERT(pThis) ;

ASSERT (pThis-»IsKindOf (RUNTIME_CLASS (C3dShape) ) ) ;

ASSERT(pszName) ;

// Загрузить текстуру ASSERT(ppITex) ;

C3dTexture* pTex = new C3dTexture;

if ( !pTex-»Load( (const char* ) pszName) ) {

delete pTex;

return E FAIL;

} *ppITex = pTex-»Get!nterface () ;

// Включить текстуру в список изображений фигуры pThis-»m_ImgList .Append (pTex) ;

return NOERROR;

1

Сначала мы преобразуем пользовательский аргумент к типу указателя на класс C3dShape и проверяем его. Далее создается новый объект C3dTexture и вызывается его функция Load для того, чтобы загрузить текстуру из файла. После того как по данным изображения будет создан объект-текстура, указатель на интерфейс текстуры возвращается построителю сеток, чтобы он мог продолжить загрузку объекта.

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

C3dShape::~C3dShape() {

if (m_pIVisual) m_pIVisual-»Release () ;

if (m_pIMeshBld) m_pIMeshBld-»Release () ;

m_ImgList.DeleteAll() ;

}

Возможно, вы только что подумали: «Эй, постойте-ка, ведь у нас уже имеется список изображений для всего макета!» Вы правы. Если среди аргументов функции C3dShape::Load передавать указатель на макет, то наши изображения вполне можно было бы просто занести в список изображений макета. Однако я решил,

/b> Щу Глава 8. Цвет и текстуры

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


Загрузка из ресурсов

Самый простой способ избежать всех проблем, связанных с местонахождением файлов, — внести объекты и их текстуры в число ресурсов приложения. В качестве примера я включил корпус танка и две текстуры, используемые в нем, в состав ресурсов приложения Color. В меню Edit присутствует команда Tank Hull (resource), реализованная следующим образом:

void CMainFrame::OnEditTank()

{

C3dShape* pShape = new C3dShape;

BOOL b = pShape-»Load(IDX_TANK) ;

ASSERT(b) ;

ASSERT(m_pScene) ;

m_pScene-»AddChild (pShape) ;

m_pScene-»m_ShapeList .Append (pShape) ;

MakeCurrent(pShape) ;

Все, что нам понадобилось, — идентификатор ресурса нашего объекта-танка. Но как происходит загрузка текстур? И как же включить в приложение те ресурсы, которые нам нужны?

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

// Загрузить фигуру из XOF-pecypca BOOL C3dShape::Load(UINT uiResid) {

// Удалить любые существующие визуальные элементы

New () ;

// Попытаться загрузить файл ASSERT(m_pIMeshBld) ;

D3DRMLOADRESOURCE info;

info.hModule = AfxGetResourceHandle() ;

info.lpName = MAKEINTRESOURCE(uiResid);

info.lpType = "XOF";

m_hr = m_pIMeshBld-»Load (sinfo,

NULL,

D3DRMLOAD_FROMRESOURCE,

C3dLoadTextureResCallback,

this) ;

ASSERT;SUCCEEDED(m_hr)) ;

if (FAILED(m_hr)) ( return FALSE;

}

AttachVisual(m_pIMeshBld) ;

m strName = "Resource object";

return TRUE;

Обратите внимание — здесь также используется функция косвенного вызова для загрузки текстур. Только на этот раз текстуры являются ресурсами приложения и загрузка их должна осуществляться по-другому:

static HRESULT CSdLoadTextureResCallback(char* pszName, void* pArg,

LPDIRECT3DRMTEXTURE* ppITex)

{

C3dShape* pThis = (C3dShape*) pArg;

ASSERT(pThis) ;

ASSERT (pThis-»IsKindOf (RUNTIME_CLASS (C3dShape) ) ) ;

ASSERT(pszName) ;


// Загрузить текстуру ASSERT(ppITex) ;

C3dTexture* pTex = new C3dTexture;

if ( !pTex-»LoadResource ( (const char* ) pszName) ) (

delete pTex;

return E_FAIL;

} *ppITex = pTex-»Get!nterface () ;

// Включить текстуру в список изображений фигуры pThis-»m_ImgList .Append (pTex) ;

return NOERROR;

}

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

Поскольку AppStudio не позволяет использовать обычное имя файла (например, Camo.bmp) как идентификатор ресурса, мы не сможем включить текстуру в ресурсы приложения с помощью AppStudio. Вместо этого придется вручную отредактировать файл RC2 по аналогии с тем, как мы добавляли к ресурсам файл объекта .X:

/b> iHl' Глава 8. Цвет и текстуоы

// Корпус танка

IDX_TANK XOF res\t_hull.x

camo.bmp BITMAP res\camo.bmp

camousa.bmp BITMAP res\camousa.bmp

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

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

Довольно о цветах

Нам довелось увидеть немало разнообразных примеров того, как цвета и текстуры комбинируются с трехмерными фигурами. Однако на этом дело не кончается — мы не рассмотрели еще один случай, связанный с текстурами, прозрачностью... и только двумя измерениями! Речь о нем пойдет в следующей главе.

DirectSD

Глава 13 DirectSD
Непосредственный режим Direct3D

DirectSD

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

DirectSD

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

DirectSD

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

DirectSD

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

DirectSD


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

/h2>

Глава 13. Direct3D

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

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

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

DirectSD

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

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


BviteD выполнения

/h2>

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

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

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

Рисунок. 13-6. Различные компоненты материала 308 Hill1 Глава 13. Direct3D

DirectSD

Необходимость хранения структур данных, интерфейсов и логических номеров для каждого объекта приводит к чрезмерному усложнению программ, поэтому я скрыл все это в классах 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;

)

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

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

Несмотря на всю убогость реализации, класс значительно облегчает создание буфера выполнения и работу с ним по сравнению с предназначенными для этого макросами 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:

/b> 1У Глава 13. Direct3D

"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, что изначально входило в мои намерения (хотя я и не упоминал об этом).

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

/b>

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

Приложение 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) ;

/b> вГ Глава 13. Direct3D

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

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,

nrMJnnwauiJQ n'mCtf-al ^vH^ ^14

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) ;

// Присоединить ракурс к устройству 314 йЦГ Глава 13. Direct3D

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 — это важно при определении видимых объектов. Объекты, находящиеся за

Приложение DSDEval '^ЦЦ 315

максимальным или перед минимальным значением 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();

/b> ЩЦ' Глава 13. Direct3D

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, которая обновляет состояние физического объекта при помощи параметров, заданных при вызовах предыдущих функций. Источник света испускает белый цвет (точнее, серый) и направлен вниз и внутрь макета, если смотреть из левого верхнего угла.

ГЬиложение DSDEval 'tH 317

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

// Примечание: количество оттенков должно быть равно 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) ;

/b> ЩУ Глава 13. DirectSD

// Получить прямоугольник, описывающий первичный буфер 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 '''Щ:. 319

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

// Создать буфер выполнения 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);

/b> irf? Глава 13. Direct3D

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();

ГЬиложение DSDEval '!№ 321

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)

/b> йЙ? Глава 13. DirectSD

},

{ 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); // Светло-зеленый

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

Приложение D3DEval ^И 323

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)ере выполнения все грани одного цвета.


/b> НУ Глава 13. DirectSD

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

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

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

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

Интерфейс DirectDraw

Глава 12 Интерфейс DirectDraw
Что такое DirectDraw?

Интерфейс DirectDraw

Библиотека DirectDraw предоставляет единый программный интерфейс для работы с различными видеоадаптерами. Но ведь подобный интерфейс, Microsoft Windows Graphics Device Interface (GDI), существовал и ранее? Вы совершенно правы. Главное отличие между DirectDraw и GDI заключается в том, что DirectDraw позволяет работать непосредственно с видеоадаптером, a GDI — наоборот, ограждает вас от этого! Возможно, сказанное не совсем справедливо по отношению к GDI — интерфейс проектировался для создания переносимых приложений, а о какой переносимости можно говорить, если кто угодно как угодно развлекается с видеоадаптером?
Конечно, переносимые приложения — вещь хорошая, но если программные прослойки, обеспечивающие переносимость, оказываются слишком «толстыми» или неэффективными, это снижает производительность приложения и делает его недопустимо медленным. Игры как раз и составляют такой класс приложений, для которых незначительная разница в производительности может обернуться разницей между доходами и потерями для фирмы-разработчика.
Даже несмотря на то, что Windows GDI совершенствовался с годами, всегда хотелось обойти его и напрямую обращаться к видеоадаптеру — например, когда приложение работает в полноэкранном режиме. Если приложение занимает весь экран, почему бы не дать ему полную свободу в работе с видеоадаптером? Интерфейс DirectDraw позволяет нам почти напрямую обращаться к видеоадаптеру в любом Windows-приложении. Остается лишь решить, стоит этим пользоваться или нет?
Чтобы вам было проще принять решение, мы кратко рассмотрим наиболее интересные возможности DirectDraw на примере несложного приложения. Это поможет вам в дальнейшем самостоятельно экспериментировать с DirectDraw.
Архитектура DirectDraw
На Рисунок 12-1 изображена слегка упрощенная архитектура GDI и DirectDraw с точки зрения приложения, работающего с трехмерными объектами.
Как видите, для рисования трехмерного объекта у приложения есть четыре возможности:

• GDI.

• OpenGL.

• Абстрактный режим Direct3D.

• DirectDraw.

Рисунок. 12-1. Архитектуры GDI и DirectDraw

Интерфейс DirectDraw

Рисование в GDI

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

Рисунокование в OpenGL

Для полноты картины я включил в наше рассмотрение механизм визуализации OpenGL. OpenGL входит в Win32 и позволяет создавать переносимые трехмерные приложения, обходясь без самостоятельных вычислений глубины и т. д. Язык OpenGL поддерживается на большом количестве платформ, что повышает переносимость вашего приложения. Качество реализации OpenGL отличается на разных платформах, так что производительность приложения также может изменяться. Поскольку реализация OpenGL в Microsoft Windows 95 использует прослойку Direct3D, ее производительность оказывается лучше, чем в ряде других систем.

Рисунокование в абстрактном режиме Direct3D

До настоящего времени мы использовали только абстрактный режим Direct3D. Механизм абстрактного режима во многих отношениях напоминает OpenGL. Главное отличие заключается в том, что абстрактный режим оптимизирован для работы с прослойкой Direct3D и обеспечивает бесспорно лучшую производительность из всех возможных.

Рисунокование в DirectDraw

Путь, который мы рассмотрим сейчас, идет от приложения прямо к прослойке DirectDraw/Direct3D. Особый интерес представляет ветка DirectDraw, которая идет вниз, к видеоадаптеру, через HAL (прослойка абстрактной аппаратуры, Hardware Abstraction Layer) и HEL (прослойка эмуляции аппаратуры, Hardware Emulation Layer). Работа только с сервисом DirectDraw в определенном отношении напоминает обращение с GDI, поскольку немалую часть вычислении вам придется выполнять самостоятельно. Тем не менее если вы уверены в своих силах, то этот путь теоретически позволит добиться наилучшей производительности, поскольку перед вами открывается почти прямой доступ к видеоадаптеру. Если вы собираетесь перенести в Windows уже существующий механизм визуализации, то вам следует идти именно этим путем.


На Рисунок 12-1 компоненты DirectDraw и Direct3D изображены в одной общей рамке. Между ними существует немало сходства, так что в условном изображении архитектуры их можно разместить вместе. Тем не менее они все же отличаются между собой, и в этой главе мы будем рассматривать только DirectDraw. О Direct3D рассказывается в главе 13.

Прослойка абстрактной аппаратуры (HAL)

Мне не нравятся термины «прослойка» или «абстрактный», поскольку они наводят на мысли о какой-то жирной и неповоротливой программе, хотя HAL с подобным представлением не имеет ничего общего. HAL существует для того, чтобы обеспечить возможность унифицированного взаимодействия с видеоадаптером. Во многих случаях ее можно просто считать таблицей, которая показывает, где хранятся те или иные данные. Например, при отображении видеопамяти на основное адресное пространство HAL содержит информацию о том, с какого адреса начинается блок видеопамяти. Кроме того, HAL знает о том, через какой порт можно работать с аппаратной палитрой и т. д. На самом деле прослойка HAL оказывается очень тонкой.

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

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

Прослойка эмуляции аппаратуры (HEL)


HEL существует для того, чтобы предоставлять услуги, не поддерживаемые конкретной моделью видеоадаптера. Многие видеокарты содержат блиттеры — аппаратные средства для пересылки битовых блоков (bitbit) из одного участка видеопамяти в другой. На некоторых картах имеются блиттеры для пересылки блоков в основной памяти, а также в видеопамяти. Пересылая спрайт в своей программе, вы не задумываетесь о том, где он хранится в данный момент. Это означает, что некоторая внешняя программа должна следить за аппаратными и программными средствами. Такая задача возложена на HEL, которая осуществляет программную эмуляцию той или иной операции в случае, если ваши аппаратные средства не могут ее выполнить.

Компоненты DirectDraw

Архитектуру DirectDraw можно рассмотреть и с несколько иной точки зрения, как показано на Рисунок 12-2.

В этом случае DirectDraw предоставляет интерфейс для работы с видеопамятью и палитрой. Я изобразил на рисунке палитру, поскольку 256-цветные дисплеи с палитрой продолжают оставаться самыми распространенными. Если ваша видеосистема работает с 16-, 24- или 32-битной кодировкой пикселей, то палитра не понадобится, и схема несколько упрощается. Если временно не обращать внимания на палитру, мы увидим, что приложение должно располагать средствами для доступа к видеопамяти и перемещения данных в ней.

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

Если учесть, что HEL обеспечивает выполнение блитовых операций даже в том случае, если они не поддерживаются на аппаратном уровне, то становится понятно, почему на Рисунок 12-2 нет аппаратного блиттера. Приложение должно работать лишь с различными областями видеопамяти и, возможно, с палитрой.


Для приложения с трехмерными объектами схема выглядит более сложно, однако мы рассмотрим соответствующие отличия в следующей главе. А сейчас давайте обсудим различные компоненты DirectDraw и способы управления ими.

Поверхности в видеопамяти

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

Рисунок. 12-2. Компоненты DirectDrow

Интерфейс DirectDraw

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

К одним поверхностям могут присоединяться другие. Взгляните на Рисунок 12-2. Вторичный буфер присоединен к первичному, в этом случае ими легче управлять, поскольку вам нужно хранить указатель лишь на первичный буфер. Указатель на вторичный буфер всегда можно получить из списка присоединенных поверхностей первичного буфера. При работе с трехмерными объектами иногда бывает необходимо отводить поверхность под Z-буфер, ее тоже удобно присоединить к первичному буферу. Кроме того, поверхность можно выделить под альфа-буфер и также присоединить ее к другой поверхности.

Поверхность описывается структурой DDSURFACEDESC, в которой содержатся поля для высоты поверхности, ее ширины и т. д. Кроме того, в нее входит структура DDPIXELFORMAT, описывающая формат отдельных пикселей. Пиксели могут быть заданы в виде RGB-тройки, индекса палитры, YUV-значения или в каком-нибудь другом формате, который поддерживается вашим видеоадаптером. Количество бит на пиксель изменяется от 1 до 32. Для поверхностей, где число бит на пиксель составляет 16 и менее, цветовые составляющие (например, красная, зеленая и синяя) задаются в виде масок, для которых следует выполнить операцию поразрядного AND со значением конкретного пикселя.


Чтобы получить доступ к поверхности, необходимо заблокировать ее и получить указатель. Завершив работу с поверхностью, вы разблокируете ее. Блоки-

ровка нужна для доступа к поверхности в режиме чтение/запись; она также помогает организовать взаимодействие с аппаратным блиттером и т. д. Следовательно, перед тем как пытаться получить доступ к поверхности, необходимо знать о том, кто ее использует. Пример будет приведен немного ниже, в разделе «Тестирование прямого доступа к пикселям» на стр. 296.

Рабочая поверхность может быть потеряна, если в системе присутствует другое приложение, также использующее DirectDraw. При попытке выполнить какую-нибудь операцию с потерянной поверхностью обычно возвращается код ошибки DDERR_SURFACELOST. Потерянная поверхность восстанавливается функцией IDirectDrawSurface::Restore (ресурсы Windows используются совместно — со временем к этому привыкаешь).

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

Палитры

Для описания цветов в поверхностях с 1-, 4- и 8-битной кодировкой пикселей применяются палитры. В DirectDraw поддерживаются 4- и 8-битные палитры, которые содержат 16 и 256 цветов соответственно. Палитра также может представлять собой набор индексов в другой палитре. Если вам приходилось работать с палитрами в Windows, то и палитры DirectDraw покажутся хорошо знакомыми. Если же вы привыкли «играть» с цветовыми таблицами видеоадаптера, то вам, вероятно, понравится гибкость работы с палитрами DirectDraw. Создание палитр рассматривается ниже, на стр. 298.

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


При создании палитры вы указываете, какую ее часть можно отдать в распоряжение DirectDraw. При работе в оконном режиме обычно приходится резервировать 20 системных цветов Windows (по 10 с каждого конца палитры) и разрешать DirectDraw пользоваться оставшимися 236 элементами. В полноэкранном режиме другие приложения все равно не видны, поэтому резервировать для них системные цвета незачем, и вы можете предоставить в распоряжение DirectDraw всю палитру. DirectDraw определяет несколько новых флагов, показывающих, как используется тот или иной элемент палитры (в дополнение к существующим флагам Windows). Флаг D3DRMPALETTE_FREE позволяет DirectDraw использовать данный элемент палитры. Флаг D3DRMPALETTE_READONLY позволяет DirectDraw читать элемент палитры и работать с ним, не изменяя его значение (используется для системных цветов в оконном режиме), а флаг D3DRMPALETTE_RESERVED резервирует элемент палитры для ваших собственных целей. DirectDraw не изменяет и не использует элементы палитры, помеченные флагом D3DRMPALETTE_RESERVED.

Ограничители

DirectDraw работает в одном из двух режимов. Первый режим, который больше всего привлекает бывших DOS-программистов, — полноэкранный. В этом случае вы можете полностью распоряжаться изображением на экране. Обычно в полноэкранном режиме создаются два буфера, как показано на Рисунок 12-3.

Рисунок. 12-3. Работа с буферами в полноэкранном режиме

Интерфейс DirectDraw

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

Второй режим больше соответствует облику стандартных приложений Windows. В оконном режиме приходится работать с привычным окном приложения, расположенным на рабочем столе, как показано на Рисунок 12-4.

Оконный режим связан с определенными сложностями, поскольку поверхность вторичного буфера используется совместно с GDI. При непосредственной записи в видеопамять необходимо соблюдать осторожность и не выйти за пределы области вашего окна. Чтобы помочь вам в этом, DirectDraw предоставляет объект-ограничитель DirectDrawClipper, который присоединяется к первичному буферу. Ограничитель следит за окном, созданным на рабочем столе, и определяет границы памяти первичного буфера. В оконном режиме вы уже не можете переключать страницы, как это делается в полноэкранном режиме, и размер вторичного буфера обычно равен лишь размеру окна. Некоторые видеоадаптеры выполняют сложные перекрытия, которые позволяют в оконном режиме переключать страницы так, как это делается в полноэкранном; при соответствующей аппаратной поддержке DirectDraw позволяет вызвать функцию переключения Flip для поверхности в оконном режиме.


Компоненты DirectDraw

/h2>

Рисунок. 12-4. Оконный режим

Интерфейс DirectDraw

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

Поверхности и GDI

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

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

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

/b>

Глава 12. Интерфейс DirectDraw

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


Интересный побочный эффект от использования видеопамяти в GDI прослеживается при попытке отладить программу, непосредственно работающую с пикселями, в графическом отладчике (например, отладчике Microsoft Visual C++). Перед тем, как работать с поверхностью, ее необходимо заблокировать. При этом DirectDraw блокирует подсистему Win 16, через которую осуществляется доступ к таким компонентам Windows, как USER и GDI. Когда DirectDraw заблокирует Winl6, GDI перестанет работать, поэтому ваш отладчик не сможет ничего вывести на экран и «повиснет».

Выход заключается в том, чтобы работать с отладчиком, не использующим GDI (например, WDEB386), или запустить Visual C++ в сеансе удаленной отладки, подключаясь к целевому компьютеру через кабель или сетевое соединение TCP/IP. Инструкции по удаленной отладке приведены в справочном разделе Books Online среды Visual C++.

Работа с DirectDraw

После краткого знакомства с «машинным отделением» DirectDraw, давайте посмотрим, как же пользоваться интерфейсом DirectDraw в наших приложениях. Код, которым мы будем заниматься, взят из примера, находящегося в каталоге DDeval. Это приложение написано мной специально для экспериментов с DirectDraw. Его даже трудно назвать приложением — скорее, это инструмент для тестирования и оценки, поэтому средства его пользовательского интерфейса ограничены. DDEval не делает ничего сверхъестественного, но показывает вам, как использовать GDI для работы с поверхностью, как запустить программу в полноэкранном или оконном режиме, как напрямую работать с пикселями поверхности. Тесты позволяют измерить скорость работы приложения, выраженную в количестве кадров в секунду (fps), чтобы вы могли наглядно представить себе производительность.

ПРИМЕЧАНИЕ

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

Структура программы DDEval

Я создал программу DDEval с помощью Visual C++ AppWizard. Я решил оформить ее в виде окна диалога, а не в виде приложения с интерфейсом SDI или MDI. Окно диалога изображено на Рисунок 12-5.


DDEval проводит четыре теста, каждый из которых может выполняться либо в оконном, либо в полноэкранном режиме. В оконном режиме размер окна выбирается из списка (от 320х200 до 1024х768). Количество цветов всегда совпадает с

Работа с DirectDraw 'тЩ! 283

Рисунок. 12-5. Приложение DDEval

Интерфейс DirectDraw

числом цветов в системе, на которой выполняется программа. Чтобы изменить цветовой режим, необходимо обратиться к свойствам экрана в Панели управления'Windows.

При выборе полноэкранного режима программа составляет перечень всех возможных режимов. На моем компьютере Dell он состоит из 17 режимов, так что на однообразие жаловаться не приходится. Полноэкранные режимы не ограничиваются текущим количеством цветов. Даже если вы работаете с 8-битными цветами, то при желании можете провести тестирование в полноэкранном режиме с 16--битными цветами.

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

Программа DDEval

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

void CDDEvalDIg::SetInitialState () (

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

BOOL b = m_pDD-»Create () ;

}

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

После того как создан объект CDirectDraw, можно составлять перечень доступных полноэкранных режимов. Приведенная ниже функция заполняет список в окне диалога DirectDraw Evaluation:

/b>

Глава 12. Интерфейс DirectDraw

void CDDEvalDIg::ShowModes(

int iModes = m_pDD-»GetNumModes () ;

DDSORFACEDESC dds;

for (int i = 0; i « iModes; i++) ( m_pDD-»GetMode!nfo (i, &dds);

sprintf(buf,

"%41u x %41u x %21u", dds.dwWidth, dds.dwHeight,


dds.ddpfPixelFormat.dwRGBBitCount) m_cbModes.AddString(buf) ;

}

Как видно из листинга, мы определяем режимы и получаем описание поверхности для каждого из них. Затем для каждого режима конструируется строка, которая заносится в список. Код объекта CDirectDraw выглядит несколько сложнее, поскольку для нумерации режимов в DirectDraw используются функции косвенного вызова. Я реализовал функции CDirectDraw::GetNumModes и CDirectDraw::GetModelnfo с помощью одной функции косвенного вызова, расположенной в файле 3dDirDrw.cpp:

// Информационная структура для нумерации режимов typedef struct _EnumModeInfo {

int iModeCount;

int iMode;

DDSURFACEDESC ddSurf;

} EnumModeInfo;

// Функция косвенного вызова для нумерации // режимов устройства

static HRESULT FAR PASCAL EnumModesFn(LPDDSURFACEDESC psd, LPVOID pArg)

1

EnumModeInfo* pinfo = (EnumModeInfo*) pArg;

ASSERT(pinfo) ;

// Проверить правильность режима if (p!nfo-»iMode == p!nfo-»iModeCount) {

p!nfo-»ddSurf = *psd;

return DDENuMRET_CANCEL; // Прекратить нумерацию

}

p!nfo-»iModeCount++;

return DDENUMRET_OK;

}

Работа с DirectDraw ТЙВ 285

// Определить количество поддерживаемых экранных режимов int CDirectDraw::GetNumModes()

{

ASSERT(m_pIDD) ;

EnumModeInfo mi;

mi.iModeCount = 0;

mi.iMode = -1;

m_hr = m_pIDD-»EnumDisplayModes (0, NULL, Smi, EnumModesFn) ;

ASSERT(SUCCEEDED(m_hr)) ;

return mi.iModeCount;

}

// Получить данные для заданного режима BOOL CDirectDraw::GetModeInfo(int iMode,

DDSURFACEDESC* pDesc)

{

int iModes = GetNumModes();

if (iMode »= iModes) return FALSE;

ASSERT(m_pIDD) ;

EnumModeInfo mi;

mi. iModeCount =0;

mi.iMode = iMode;

m hr = m pIDD-»EnumDisplayModes (0, NULL, &mi, EnumModesFn);

ASSERT(SUCCEEDED(m_hr)) ;

*pDesc = mi.ddSurf;

return TRUE;

}

Структура EnumModeInfo управляет функцией нумерации и служит для возвращения результата. При подсчете количества режимов возвращаемые данные не используются. При получении информации по конкретному режиму функция нумерации вызывается до тех пор, пока номер режима, возвращаемый интерфейсом DirectDraw, не совпадет с заданным.


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

Подготовка тестового окна

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

/b> в^ Глава 12. Интерфейс DirectDraw

BOOL CTestWnd::Create(TESTINFO* pti)

{

// Сохранить информацию о тесте m_pTI = pti;

ASSERT(m_pTI) ;

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

BOOL b = m_pDD-»Create () ;

ASSERT(b);~

II Зарегистрировать класс окна

CString strClass =

AfxRegisterWndClass(CS_HREDRAW I CS_VREDRAW, ::LoadCursor(NULL, IDC_ARROW), (HBRUSH)::GetStockObject(GRAY_BRUSH)

// Задать стиль и размеры окна DWORD dwStyle = WS_VISIBLE ¦ 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_pT I-»i Height;

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

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

\

if (!CreateEx(0,

strClass,

"DirectDraw Window",

dwStyle,

re.left, re.top,

re.right — re.left, re.bottom — re.top,

m pTI-»pParent-»GetSafeHwnd() ,

NULL)) {

return FALSE;

i

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

// Установить режим для окна

Работа с DirectDraw 'Ч^' 287

ASSERT(m_pTI) ;

if (m_pTI-»bFullScreen) f

b = m_pDD-»SetFull3creenMode (GetSafeHwndO ,

m_pTI-»iWidth,

m_pTI-»iHeight,

m_pTI-»iBpp) ;

} else (

b = m_pDD-»SetWindowedMode (GetSafeHwnd () ,

m_pTI-»iWidth,

m_pTI-»iHeight) ;


} ASSERT(b) ;

// Выполнить тестирование SetTimerfl, 100, NULL);

return TRUE;

»

В первой половине функции мы создаем объект CDirectDraw и тестовое окно, которое появляется на экране. Большей частью она состоит из стандартного кода Windows. После того как окно создано, объект CDirectDraw переводится в оконный или полноэкранный режим. Наиболее сложной оказывается завершающая часть. Функции SetFullScreenMode и SetWindowedMode равносильны вызову функции CDirectDraw::_SetMode, выполняющей всю работу по созданию первичного и вторичного буферов, а также связанной с ними палитры. Установка режима состоит из трех этапов:

1. Задание уровня кооперации (cooperative level), который определяет, какие действия разрешается выполнять с DirectDraw. Если приложение должно работать в окне, выбирается нормальный режим. Для полного экрана следует затребовать монопольный (exclusive) режим. Уровень кооперации помогает распределять ресурсы между системой и приложениями DirectDraw.

2. Создание первичного и вторичного буферов. Буфера являются поверхностями DirectDraw. Если мы собираемся работать в полноэкранном режиме, то создаем так называемую сложную переключаемую поверхность, состоящую из двух одинаковых буферов. Для работы в окне создается два отдельных буфера: первичный, используемый совместно с GDI, и вторичный, принадлежащий только приложению.

3. Определить, нужен ли ограничитель, и если да, то создать его.

Ниже приводится функция для установки режима:

BOOL CDirectDraw::_SetMode(HWND hWnd, int ex, int cy,

int bpp, BOOL bFullScreen) t

ASSERT(m_pIDD) ;

// Освободить все существующие буфера ReleaseAllO ;

/b> вг Глава 12. Интерфейс DirectDraw

// Задать уровень кооперации if (bFullScreen) {

if (!SetCooperativeLevel(hWnd, DDSCL_EXCLOSIVE ¦

DDSCL_FULLSCREEN)) ( return FALSE;

}

m_hr = m_pIDD-»SetDisplayMode (ex, cy, bpp) ;

if (FAILED(m_hr)) { return FALSE;

} m_bRestore = TRUE;

} else {

if (!SetCooperativeLevel(hWnd, DDSCL_NORMAL)) ( return FALSE;

} )

// Создать первичную и вторичную поверхности


m_iWidth = ex;

m_iHeight = су;

DDSURFACEDESC sd;

inemset(&sd, 0, sizeof(sd));

sd.dwSize == sizeof(sd);

if (bFullScreen) {

// Создать сложную переключаемую поверхность // с первичным и вторичным буферами sd.dwFlags = DDSD_CAPS

I DDSD_BACKBUFFERCOUNT;

sd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE

I DDSCAPS_FLIP

I DDSCAPS_COMPLEX

I DDSCAPS_3DDEVICE;

sd.dwBackBufferCount = 1;

// Создать поверхности для первичного и вторичного буфера

m_pFront Buffer = new CDDSurface/if ( !m_pFrontBuffer-»Create (this, Ssd) ) { return FALSE;

} ASSERT(m_pFrontBuffer) ;

// Получить указатель на присоединенный вторичный буфер DDSCAPS caps;

:::::^^ Работа с DirectDraw ч!И 289

memset(Scaps, 0, sizeof(caps));

caps.dwCaps = DDSCAPS_BACKBUFFER;

m_pBackBuffer = m_pFrontBuffer-»GetAttachedSurface(&caps) ;

if (!m_pBackBuffer) {

delete m_pFrontBuffer;

m_pFrontBuffer = NULL;

return FALSE;

}

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

// Создать две поверхности для оконного режима — // первичную, используемую совместно с GDI, //и вторичный буфер для вывода изображения.

// Создать поверхность первичного буфера. // Примечание: поскольку первичный буфер является // основной (существующей) поверхностью, // мы не указываем его ширину и высоту. sd.dwFlags = DDSD_CAPS;

sd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

m_pFrontBuffer = new CDDSurface;

if ( !m_pFrontBuffer-»Create (this, &sd) ) { return FALSE;

}

// Создать поверхность вторичного буфера sd.dwFlags = DDSD_WIDTH

I DDSD_HEIGHT

I DDSD_CAPS;

sd.dwWidth = ex;

sd.dwHeight = cy;

sd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN

I DDSCAPS_3DDEVICE;

m_pBackBuffer = new CDDSurface;

if ( !m_pBackBuffer-»Create(this, &sd)) (

delete m_pFrontBuffer;

m_pFrontBuffer = NULL;

return FALSE;

}

// Создать объект-ограничитель для первичного буфера, // чтобы вывод ограничивался пределами окна m_pClipper = new CDDClipper;

if ( !m_pClipper-»Create(this, hWnd) ) { return FALSE;

} }

/b> ^^' Глава 12. Интерфейс DirectDraw

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


Приведенный фрагмент содержит большое количество аспектов, которые я не стану подробно объяснять. Интерфейсы DirectDraw описаны в документации по DirectX SDK. Вы можете самостоятельно провести ряд экспериментов, взяв за основу данный код.

Тестирование

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

void CTestWnd::OnTimer(UINT nIDEvent) {

// Выполнить следующий тест switch (m_pTI-»iTest) { case 1:

TestGDIText() ;

breaks-case 2:

TestGDIGfxf) ;

break;

case 3:

TestDDSprite() ;

break;

case 4:

TestDirectPixels() ;

break;

default:

ASSERT(0) ;

break;

}

Тестирование GDI при работе с текстом

В этом тесте я решил прибегнуть к услугам GDI для того, чтобы вывести в окне небольшой текст. При этом мне хотелось оценить, насколько медленно GDI будет справляться с данной задачей и будет ли вывод текста влиять на другие тесты. Результаты, полученные на моем компьютере Dell, меня вполне устроили — при выполнении теста в окне 320х200 скорость превышала 200 кадров в секунду. Все остальные тесты построены на основе этого кода, поэтому мы подробно рассмотрим его, шаг за шагом:

void CTestWnd::TestGDIText() (

ASSERT(m_pTI) ;

// Получить указатели на первичный и вторичный буфера

Работа с DirectDraw ''TO 291

CDDSurface* pBB = m_pDD-»GetBackBuffer () ;

ASSERT(pBB) ;

CDDSurface* pFB = m_pDD-»GetFrontBuffer();

ASSERT(pFB) ;

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

if (m_pTI-»bFullScreen) {

pFB-»GetRect (rcFront) ;

} else (

GetClientRect(SrcFront) ;

ClientToScreen(&rcFront) ;

}

RECT rcBack;

pBB-»GetRect (rcBack) ;

DWORD dwStart = timeGetTime() ;

int nFrames = 100;

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);

}

// Подготовить выводимый текст char buf[64];

sprintffbuf, "Frame td (%3.1f fps)", iFrame, fps);


// Получить DC для вторичного буфера CDC* pdc = pBB-»GetDC() ;

ASSERT(pdc) ;

// Заполнить буфер белым цветом pdc-»PatBlt (rcBack.left,

rcBack.top,

rcBack.right — rcBack.left,

rcBack.bottom — rcBack.top,

WHITENESS) ;

// Вывести текст pdc-»DrawText (buf,

-1,

srcBack,

DT_CENTER ¦ DT_BOTTOM I DT_SINGLELINE) ;

/b> в¦1' Глава 12. Интеофейс DirectDraw

// Освободить DC pBB-»ReleaseDC(pdc) ;

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

if (m_pTI-»bFullScreen) { pFB-»Flip() ;

} else (

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

} } )

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

Каждый цикл тестирования в функции TestGDIText состоит из следующих этапов:

1. Определить текущее время и вычислить текущую скорость вывода.

2. Создать выводимый текст.

3. Получить контекст устройства для вторичного буфера.

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

5. Вызвать другую функцию GDI для вывода текста во вторичный буфер.

6. Освободить контекст устройства.

7. Переключить буфера и вывести результат на экран.

После получения DC в программе встречаются привычные вызовы функций GDI, так что нетрудно забыть, что мы работаем с поверхностью DirectDraw, а не с оконным DC.

Разумеется, суть последнего этапа не всегда заключается в том, что первичный буфер меняется местами со вторичным. При работе в полноэкранном режиме функция CDDSurface::Flip действительно меняет буфера местами, однако в оконном режиме содержимое вторичного буфера копируется в первичный функцией Bit. В случае смены буферов не нужно беспокоиться о новой роли каждого из них, так как за всем следит DirectDraw, и когда мы требуем указатель на вторичный буфер, то всегда получаем именно то, что нужно.


Производительность GDI при выводе текста оказалась хорошей, и потому в следующем тесте я решил определить, насколько быстро GDI может рисовать на поверхности.

Тестирование GDI при работе с графикой

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

Работа с DirectDraw ^Щ 293

шлось добавить в функцию CTestWnd::TestGDIGfx приведенную ниже строку и в каждом цикле отслеживать положение прямоугольника в окне:

// Нарисовать прямоугольник pdc-»Rectangle (х, у, х+сх, у+су);

Вы можете самостоятельно протестировать интерфейс GDI и посмотреть, как он справляется с рисованием прямоугольников.

Тестирование DirectDraw при работе со спрайтами

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

void CTestWnd::TestDDSprite() {

DDSURFACEDESC sd;

memset(&sd, 0, sizeof(sd));

sd.dwSize = sizeof(sd);

sd.dwFlags = DDSD_WIDTH

I DDSD_HEIGHT

I DDSD_CAPS;

sd.dwWidth = ex;

sd.dwHeight = cy;

sd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;

CDDSurface sprite;

BOOL b = sprite.Create(m_pDD, &sd) ;

ASSERT(b) ;

// Получить DC для поверхности спрайта.

// Нарисовать спрайт в виде красного круга на черном

фоне

CDC* pdc = sprite.GetDC();

ASSERT(pdc) ;

pdc-»PatBlt(0, 0, ex, cy, BLACKNESS);

CBrush br;

br.CreateSolidBrush(RGB(255, 0, 0) ) ;

CBrush* pbrOld = pdc-»SelectObject (Sbr) ;

pdc-»Ellipse (0, 0, ex, cy) ;


pdc-»SelectObject (pbrOld) ;

sprite.ReleaseDC(pdc) ;

// Задать черньй цвет в качестве цветового ключа

/b> Глава 12. Интерфейс DirectDraw

DDCOLORKEY ck;

ck.dwColorSpaceLowValue =0; // Черный ck.dwColorSpaceHighValue = 0;

sprite.SetColorKey(DDCKEY_SRCBLT, &ck) ;

Я создал поверхность, размер которой равен размеру спрайта (сх х су) и затем воспользовался функциями GDI, чтобы заполнить поверхность черным цветом и нарисовать красный круг. Поскольку цветовым-ключом поверхности задан черный цвет, при выводе спрайта рисуется только красный круг. Признаюсь, здесь я немного смошенничал и выбрал черный цвет в качестве цветового ключа лишь потому, что 0 соответствует черному цвету независимо от того, как его рассматривать — как RGB-значение или индекс палитры.

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

// Закрасить буфер белым цветом CDC* pdc = pBB-»GetDC() ;

ASSERT(pdc) ;

pdc-»PatBlt (rcBack. left,

rcBack.top,

rcBack.right — rcBack.left,

rcBack.bottom — rcBack.top, i

WHITENESS) ;

// Вывести текст pdc-»DrawText (buf,

-1,

SrcBack,

DT_CENTER ¦ DT_BOTTOM ¦ DTJ3INGLELINE) ;

pBB-»Relea5eDC (pdc) ;

// Вывести спрайт RECT rcDst;

rcDst.left = x;

rcDst.top = y;

rcDst.right = rcDst.left + ex;

rcDst.bottom = rcDst.top + cy;

RECT rcSrc;

rcSrc.left = 0;

rcSrc.top = 0;

rcSrc.right = rcSrc.left + ex;

rcSrc.bottom = rcSrc.top + cy;

// Вызвать Bit с цветовым ключом pBB-»Blt (SrcDst, Ssprite, SrcSrc, DDBLT WAITIDDBLT KEYSRC) ;

Работа с DirectDraw

/b>

Перед тем как копировать спрайт во вторичный буфер, мы с помощью функции GDI закрашиваем буфер белым цветом и выводим строку с количеством кадров в секунду. Обратите внимание на то, что среди аргументов функции Bit имеется флаг DDBLT_KEYSRC, который указывает ей на необходимость использования цветового ключа поверхности-источника. Для того чтобы прозрачные области обрабатывались правильно, необходимо задать цветовой ключ в поверхности-источнике и указать этот флаг. Я провел пять или шесть часов в недоумении, пока не догадался включить флаг DDBLT_KEYSRC в вызов функции Bit. Что тут можно сказать? Видимо, я соображаю недостаточно быстро.


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

Тестирование прямого доступа к пикселям

Я едва не отказался от этого теста. Когда в течение целого дня мой компьютер «зависал» через каждые пять минут, я возненавидел DirectDraw и решил, что в дальнейшем буду работать с графикой только через GDI. Все это время я пытался заблокировать буфер, чтобы получить возможность писать в него. Вместе с буфером почему-то блокировался и компьютер, и мне приходилось перегружаться. Намучившись, я лег спать, и в лучших нердовских традициях решение пришло во сне. Чтобы моя программа заработала, из нее нужно было убрать всего один символ. Привожу старую и новую версию функции CDDSurface::Unlock из файла SdDirDraw.cpp, которая причинила мне столько огорчений:

void CDDSurface::Unlock() (

if (!m_SurfDesc.IpSurface) return; // Поверхность

// не заблокирована

m_hr = m_pISurf-»Unlock(&m_Surf Desc. IpSurface) ;

ASSERT(SUCCEEDED(m_hr)) ;

m_SurfDesc.IpSurface = NULL;

}

void CDDSurface::Unlock () (

if (!m_SurfDesc.IpSurface) return; // Поверхность

// не заблокирована

m_hr != m_pISurf-»Unlock(m SurfDesc.IpSurface);

ASSERT(SUCCEEDED(m_hr)) ;

m_SurfDesc.IpSurface == NULL;

}

Удалось ли вам найти отличие? Эти указатели так похожи друг на друга — до тех пор, пока вы не попытаетесь их использовать!

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

29А SSSi' Глава 17 Hu-rorvheui- Diror-tnraiiu

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

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


После того как мы будем знать количество бит на пиксель и маски для красной, зеленой и синей составляющих (или индексы палитры), можно приступать к рисованию. Чтобы определить смещение в буфере, по которому нужно записать пиксель, умножьте ширину буфера на номер строки. Ширина буфера измеряется в байтах. Она может отличаться от количества пикселей, умноженного на количество байт на пиксель, потому что строки часто дополняются по границе ближайших 4 байтов (32-разрядное выравнивание).

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

void CTestWnd::TestDirectPixels() (

// Получить информацию о вторичном буфере

int iBpp = pBB-»GetBitsPerPixel () ;

ASSERT(iBpp »= 8);

int iWidth - pBB-»GetWidth() ;

int iHeight = pBB-»GetHeight () ;

int iPitch = pBB-»GetPitch() ;

// Получить RGB-маски DWORD dwRMask, dwGMask, dwBMask;

pBB-»GetRGBMasks (dwRMask, dwGMask, dwBMask) ;

// Для каждой маски определить ширину в битах // и количество бит, на которые маска // смещена от конца младшего байта DWORD dwRShift, dwGShift, dwBShift;

DWORD dwRBits, dwGBits dwBBits;

dwRShift = dwRBits = 0 dwGShift = dwGBits = 0 dwBShift = dwBBits = 0 if (iBpp » 8) {

DWORD d = dwRMask;

while ((d & 0х1) == 0) ( d = d »» 1;

dwRShift++;

)

while (d & 0х01) { d = d »» 1;

dwRBits++;

}

Работа с DirectDraw ж! 297

d = dwGMask;

while ( (d & 0х1) ==0) {

d = d »» 1;

dwGShift++;

}

while (d & 0х01) { d = d »» 1;

dwGBits++;

} d = dwBMask;

while ((d & 0х1) == 0) (

d = d »» 1;

dwBShift++;

}

while (d & 0х01) { d = d »» 1;

dwBBits++;

}

i

Обратите внимание на то, что цветовые маски нужны лишь в том случае, когда пиксели поверхности кодируются более чем 8 битами. Кроме того, предполагается, что поверхность имеет формат RGB, а не YUV или какой-нибудь другой. В случае 8-битной кодировки пикселей необходимо создать палитру:

// Если буфер имеет 8-битную кодировку пикселей, // получить элементы палитры и присвоить им // нужные цветовые значения PALETTEENTRY ре [256];


BYTE r, g, b;

CDDPalette* pPal = pBB-»GetPalette () ;

if (pPal) {

pPal-»GetEntries (0, 256, ре) ;

// Задать нужные цветовые значения. // Мы воспользуемся моделью с 2-битной кодировкой // R, Си В-составляющих for (r = 0; r « 4; г++) { for (g = 0; g « 4; g++) ( for (b = 0; b « 4; b++) {

int index =10+r*16+g*4+b;

pe[index].peRed = r * 85;

ре[index].peGreen = g * 85;

pe[index].peBlue = b * 85;

}

/b> ЦУ Глава 12. Интерфейс DirectDraw

// Заполнить оставшиеся элементы палитры серым цветом, // чтобы их можно было увидеть при отладке for (int i = 10 + 4*4*4; i « 246; i++) {

ре[i].peRed = 192;

pe[i].peGreen = 192;

pe[i].peBlue = 192;

}

// Обновить палитру pPal-»SetEntries (0, 256, ре) ;

// Удалить объект палитры delete pPal;

}

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

// Заблокировать буфер и получить указатель.

// ВНИМАНИЕ: Не пытайтесь включать пошаговое выполнение

// до вызова Unlock.

BYTE* pBuf = (BYTE*) pBB-»Lock () ;

if (pBuf) (

for (int у = 0; у « iHeight; y++) (

// Определить смещение начала строки int n = iwidth;

DWORD dwOffset = у * iPitch; // В байтах

// Получить цвет int ir = GetRValue(cIrLine);

int ig = GetGValue(clrLine);

int ib = GetBValue(cIrLine);

// Вывести пиксели непосредственно в буфер

switch (iBpp) (

case 8: {

// Найти индекс для цвета

// в соответствии с принятой нами моделью int index = 10 + (ir / 85) * 16 + (ig /85) * 4 + (ib / 85) ;

BYTE* p = pBuf + dwOffset;

while (n-) {

*p++ = (BYTE) index;

Работа с DirectDraw ^Щ 299

) breaks-case 16: (

// Построить цветовое значение

DWORD dw = (ir »» (8 - dwRBits)) ««

dwRShift

I (ig »» (8 - dwGBits)) «« dwGShift

I (ib »» (8 - dwBBits)) «« dwBShift;

WORD w = (WORD)dw;

WORD* p = (WORD*)(pBuf + dwOffset);

while (n—) *p++ = w;

) breaks-case 24:

// Упражнение для самостоятельной работы:

breaks-case 32: {

DWORD dw = (ir »» (8 - dwRBits)) «« dwRShift


I (ig »» (8 - dwGBits)) «« dwGShift

I (ib »» (8 - dwBBits)) «« dwBShift;

DWORD* p = (DWORD*)(pBuf + dwOffset);

while (n—) *p++ = dw;

) breaks-default:

break;

i

// Перейти к следующему цвету NextColor(clrLine) ;

} pBB-»Unlock() ;

// Снова можно работать с отладчиком

> NextColor(cIrStart) ;

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

/b> ЩЦ' Глава 12. Интерфейс DirectDraw

телями на DWORD и получить неверный результат, поскольку компилятор прибавит 2 или 4 вместо 1 или наоборот.

Чтобы тест генерировал другой набор линий, следует внести изменения в функцию NextColor, которая определяет цвет для вывода следующей линии.

Наверняка вы обратили внимание, что я пропустил код для 24-битной кодировки. Мой видеоадаптер работает только с 8-, 16-и 32-битными цветами, поэтому я не мог протестировать 24-битный код. Результаты данного теста приведены на цветной вкладке.

Веселье продолжается

Разработка приложений для DirectDraw в моем представлении не является утомительным и занудньм кодированием. Скорее это сплошное развлечение. Хотелось бы видеть, как вы примените на практике продемонстрированные мной методы.

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

Интерфейсы и классы

Глава 3 Интерфейсы и классы
Работа с интерфейсами СОМ-объектов

Интерфейсы и классы

Давайте в общих чертах познакомимся с составной объектной моделью (СОМ — Component Object Model) и работой СОМ-интерфейсов.
Интерфейс представляет собой набор функций, объединенных общим назначением. Интерфейсные функции напоминают функции классов C++, за тем исключением, что функции интерфейса только определяются, но не реализуются. Можно считать их чем-то вроде плана для класса C++, который вы только собираетесь написать.
СОМ-объектом называют фрагмент кода, реализующий один или несколько интерфейсов. СОМ-объекты могут быть чрезвычайно простыми (например, объекты классов C++ со статической компоновкой) или чрезвычайно сложными (программа, работающая на сервере на другом краю Земли). Если вы представляете себе работу библиотек динамической компоновки (DLL), то по словам моего коллеги Дейла Роджерсона «СОМ-объекты при программировании на C++ играют ту же роль, что и DLL при программировании на С».
Любой СОМ-объект в обязательном порядке должен поддерживать интерфейс с именем lUnknown, обеспечивающий два базовых свойства СОМ-объектов: подсчет обращений и способность запрашивать другие интерфейсы. При помощи интерфейса lUnknown можно определить, какие еще интересующие вас интерфейсы поддерживаются объектом. Поясню сказанное на примере. Предположим, мы только что создали трехмерный объект средствами механизма визуализации и теперь хотим изменить его положение в макете. Поскольку нужная функция для изменения положения присутствует в интерфейсе IDirect3DRMFrame, желательно выяснить, поддерживается ли этот интерфейс созданным объектом, и, если результат проверки окажется положительным, — вызвать соответствующую функцию IDirect3DRMFrame для изменения положения объекта. Для определения того, поддерживается ли тот или иной интерфейс данным объектом, следует вызвать функцию IUnknown::Querylnterface:
HRESULT hr;
IDirect3DRMFrame* pIFrame = NULL;
hr = pI[Jnknown-»QueryInterface (IID IDirect3DRMFrame, (void**)&plFrame) ;

Работа с интерфейсами СОМ-объектов ^Щ 61

Если вызов функции был успешным, следовательно, объект поддерживает интерфейс IDirect3DRMFrame и вы можете им пользоваться:

pIFrame-»SetPosition(2, 4, 5);

Когда функция Querylnterface возвращает указатель на IDirect3DRMFrame, она также увеличивает на единицу счетчик обращений объекта. Следовательно, при завершении работы с указателем необходимо снова уменьшить значение счетчика обращении:

pIFrame-»Release () ;

pi Frame = NULL;

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

#define RELEASE (p) ( (р)-»Release () ; (р) = NULL;)

ПРИМЕЧАНИЕ

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

Интерфейс lUnknown является базовым для всех остальных СОМ-интерфейсов, так что при наличии указателя на любой интерфейс можно вызвать Querylnterface для любого интерфейса, которым вы хотите пользоваться. Например, если у нас уже имеется указатель на интерфейс IDirect3DRMFrame (pIFrame) и необходимо выяснить, поддерживается ли интерфейс IDirect3DRMMesh объектом, на который ссылается указатель, проверка может выглядеть следующим образом:

HRESULT hr;

IDirect3DRMMesh* pIMesh = NULL;

hr = pIFrame-»Query!nterface(IID_IDirect3DRMMesh, (void**)SpIMesh) ;

if (SUCCEEDED(hr)) {

// Использовать интерфейс для работы с сетками

int i = pIMesh-»GetGroupCount;

pIMesh-»Release;

pIMesh = NULL;

>

Это исключительно мощное средство, поскольку при наличии любого интерфейсного указателя на любой СОМ-объект можно определить, поддерживает ли данный объект тот интерфейс, которым вы хотите пользоваться. Единственное, чего нельзя сделать — получить список всех интерфейсов, поддерживаемых объектом.


62 Illll8 Глава 3. Интерфейсы и классы

Все имена СОМ-интерфейсов начинаются с префикса I, по которому их можно отличить от классов C++ или других объектов. Я не стал пояснять этот факт в тексте, поскольку счел его достаточно очевидным, но потом решил, что, возможно, кто-то из читателей недоумевает по этому поводу. Префикс 1 также напомнит о том, что после завершения работы с интерфейсом необходимо вызвать Release.

Кроме того, любой интерфейс или СОМ-объект может наследовать функции и свойства от другого интерфейса или целой группы интерфейсов. Однако выяснить это программными средствами невозможно; приходится смотреть на определение интерфейса. Например, если заглянуть в заголовочный файл d3drmobj.h в DirectX 2 SDK, вы увидите, что интерфейс IDirect3DRMFrame является производным от IDirect3DRMVisual. Следовательно, IDirect3DRMFrame заведомо поддерживает все функции интерфейса IDirect3DRMVisual. IDirect3DRMVisual является производным от IDirect3DRMObject, который в свою очередь порожден от IDnknown. Следовательно, интерфейс IDirect3DRMFrame поддерживает все функции IDirect3DRMFrame, а также все функции интерфейсов IDirect3DRMVisual, IDirect3DRMObject и lUnknown.

ПРИМЕЧАНИЕ

Все интерфейсы механизма визуализации имеют префикс IDirect3D. Интерфейсы с префиксом IDirect3DRM относятся к более высокому уровню и предназначаются для работы с фреймами, фигурами, источниками света и т. д. Буквы RM являются сокращением от Retained Mode (то есть «абстрактный режим», в отличие от расположенного на более низком уровне непосредственного режима, Immediate Mode).

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

Позвольте мне завершить этот краткий обзор СОМ-объектов на обсуждении того, как функции AddRef и Release интерфейса lUnknown применяются для совместного использования объектов. Предположим, мы хотим создать макет с несколькими деревьями. Описание дерева состоит из набора описаний вершин и граней, объединенных в сетку. Сетка является визуальным объектом, который можно присоединить к фрейму для того, чтобы задать его положение в макете. На самом деле одна и та же сетка может присоединяться к нескольким фреймам. Для создания нашего маленького леса понадобится одна сетка, определяющая форму дерева, и несколько фреймов для указания положений отдельных деревьев. Затем сетка присоединяется к каждому фрейму в качестве визуального элемента, для этого используется следующий вызов:


pIFrame-»AddVisual (pIMesh) ;

Если взглянуть на код функции AddVisual в IDirect3DRMFrame, вы увидите что-нибудь в таком роде:

Работа с интерфейсами СОМ-объектов Tflil 63

HRESULT IDirect3DRMFrame::AddVisual(IDirect3DRMVisual * pIVisual)

f

pIVisual-»AddRef () ;

AddVisualToList(pIVisual) ;

}

Функция AddRef, входящая в интерфейс Визуального элемента (визуальный интерфейс), увеличивает счетчик обращений к объекту-сетке. Зачем? Затем, что во время существования объекта-фрейма нельзя допустить уничтожения объекта, предоставляющего ему визуальный интерфейс. После уничтожения фрейма или удаления из него конкретного визуального интерфейса код фрейма освобождает объект-сетку:

pIVisual-»Release () ;

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

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

Позвольте дать пару последних рекомендаций по работе с СОМ-объектами. Любая функция, которая возвращает указатель на интерфейс, перед тем как вернуть управление, вызывает AddRef для увеличения счетчика обращений; после завершения работы с указателем необходимо вызвать Release, чтобы избежать ненужного хранения объектов в памяти. Если вы копируете указатель на интерфейс, вызовите AddRef для копии и освободите оба указателя функцией Release, когда надобность в них отпадет. Помните о том, что возврат указателя на интерфейс одной из ваших функций фактически равносилен его копированию. Перед тем, как возвращать указатель, не забудьте вызвать для него AddRef.

А теперь я собираюсь нарушить только что установленное правило. Если вы стопроцентно уверены в том, что делаете, то при копировании указателя можно обойтись и без вызова AddRef, однако при этом следует неуклонно следить за тем, чтобы функция Release была вызвана нужное количество раз. Лишние вызовы Release приведут к уничтожению используемого объекта, их нехватка — к непроизводительным расходам памяти. Просмотрев исходные тексты библиотеки 3dPlus, вы убедитесь, что во многих объектах C++ присутствует функция Getlnterface. Она возвращает указатель на тот интерфейс, для которого данный класс C++ выступает в роли оболочки. Я сделал это из соображений удобства и производительности. Функция Getlnterface не увеличивает счетчик обращений, так что при вызове какой-либо из функций Getlnterface не следует вызывать Release для возвращаемого указателя.


Книги, указанные в разделе «Библиография», содержат более подробную информацию о СОМ-объектах.

64 ВДГ Глава 3. Интерфейсы и классы

Интерфейсы трехмерной графики

Речь пойдет лишь о самых распространенных интерфейсах, включенных мной в библиотеку SdPlus. Все интерфейсы механизма визуализации документированы в справочных файлах, входящих в комплект DirectX 2 SDK, так что я не собираюсь подробно рассматривать работу всех функций каждого интерфейса. На Рисунок 3-1 изображена иерархия интерфейсов, входящих в библиотеку 3dPlus. Диаграмма была создана на основании определений интерфейсов из файла d3drmobj.h, входящего в DirectX 2 SDK.

Рисунок. 3-1. Иерархия интерфейсов в механизме визуализации

Интерфейсы и классы

Интерфейсы трехмерной графики тЦЦ 65

Как видите, в иерархии существуют две основные группы: интерфейсы, производные от IDirect3DRMObject, и интерфейсы, производные от IDirect3DRMVisual. Интерфейс IDirect3DRMObject является общим предком для всех интерфейсов библиотеки и включает функцию SetAppData, которая позволяет включить в любой интерфейс закрытую 32-разрядную переменную. Например, такая возможность оказывается очень полезной при инкапсуляции интерфейса в классе C++. Закрытая переменная будет содержать указатель на объект класса C++, и при наличии указателя на интерфейс можно быстро добраться до объекта-оболочки C++.

Важно учесть, что интерфейсы, производные от IDirect3DRMVisual, могут использоваться в качестве аргумента любой функции, для которой требуется указатель на интерфейс IDirect3DRMVisual (см. пример IDirect3DRMFrame::AddVisual на следующей странице). Что касается аргументов функций, следует упомянуть о том, что в реальных прототипах функций, определенных в DirectX 2 SDK, типы интерфейсов не указываются прямо. Например, функция, аргументом которой является указатель на интерфейс IDirect3DRMVisual, может быть определена следующим образом:

HRESULT IDirect3DRMFrame::AddVisual(LPDIRECT3DRMVISUAL pVisual) ;

Как видите, указатель на интерфейс IDirect3DRMVisual имеет тип LPDIRECT3DRMVISUAL.


ПРИМЕЧАНИЕ

Использование специальных типов данных в качестве указателей общепринято в Microsoft Windows. Мне кажется, что в новом 32-разрядном мире эта практика устарела, поскольку нам уже не нужно различать near и far-указатели. Как можно видеть в приведенном выше примере, использование специальных типов данных также затрудняет ответ на вопрос, что же собой представляет аргумент функции на самом деле. Имена типов, состоящие из прописных букв, также являются общепринятыми — регистр помогает отличить тип указателя от типа объекта, на который ссылается данный указатель. Как бы то ни было, я все равно не считаю такую практику полезной. И все же определения в SDK записаны именно так, к тому же они соответствуют стандартам Windows, поэтому мы должны учесть это обстоятельство и следовать ему в своих программах.

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

void AttachVisual(IDirect3DRMVisual* pIVisual);

Почти все интерфейсные функции возвращают значение типа HRESULT, которое проверяется в моих программах следующим образом:

ASSERT;SUCCEEDED(m_hr)) ;

А иногда проверка выглядит так:

return SUCCEEDED(m_hr);

66 illy Глава 3. Интерфейсы и классы

Обратите внимание — СОМ-интерфейсы могут возвращать значение S_FALSE, которое свидетельствует об ошибке, но успешно проходит проверку макроса SUCCEEDED. Ни один из интерфейсов Direct3D не возвращает S_FALSE, поэтому использование макросов SUCCEEDED и FAILED всегда будет давать правильный результат.

Давайте пройдемся по интерфейсам, производным от IDirect3DRMObject и изображенным на Рисунок 3-1, и кратко рассмотрим назначение каждого из них.

IDirect3DRMDevice

Интерфейс содержит управляющие функции, которые влияют на отображение макета в вашем окне. Функции работают со вспомогательным слоем Direct3D, и, в сущности, со многими аспектами физического устройства вывода. Вероятнее всего, вы будете пользоваться этим интерфейсом для изменения качества визуализации с помощью функции SetQuality. Кроме того, функция SetShades интерфейса IDirect3DRMDevice служит для ограничения количества цветовых оттенков при работе с палитрами. В качестве примера давайте посмотрим, как устанавливается качество визуализации. Ниже приведена реализация функции SetQuality в классе C3dDevice (он находится в файле 3dStage.cpp):


void C3dDevice::SetQuality(D3DRMRENDERQUALITY quality), {

if (!m_pIDevice) return;

m hr = m pIDevice-»SetQuality (quality) ;

AiSERT(SUCCEEDED(m_hr)) ;

)

А вот как функция C3dDevice: :SetQuality используется при первом создании объекта C3dStage и инициализации переменной m_Quality значением D3DRMRENDERJ30URARD:

BOOL C3dStage::Create(CDirect3D* pD3D) (

// Установить текущее качество визуализации m_Device.SetQuality(m_Quality);

}

Качество визуализации может соответствовать нескольким уровням — от простейшего «проволочного каркаса» до закраски методом Гуро, как показано в табл. 3-1. Я выбрал в качестве стандартной закраску Гуро (одна из технологий для получения плавной закраски), поскольку, на мой взгляд, она дает самый реалистичный результат.

Интерфейсы трехмерной графики '^Ц^ 67

Таблица 3-1. Возможные значения параметров функции SetQuality

Качество визуализации Закраска Освещение Заполнение
D3DRMRENDER WIREFRAME Однородная Нет Нет
(«проволочный
каркас»)
D3DRMRENDER UNLITFLAT Однородная Нет Сплошное
D3DRMRENDER FLAT Однородная Да Сплошное
D3DRMRENDER GOURARD Метод Гуро Да Сплошное
D3DRMRENDERPHONG Метод Фонга Да Сплошное"
* He поддерживается и Direct3D версии 2 (DircctX 2).
ПРИМЕЧАНИЕ

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

IDirect3DRMViewport

Интерфейс IDirect3DRMViewport управляет работой проекционной системы, изображенной на Рисунок 3-2 и преобразующей пространственные координаты в двумерные координаты на экране вашего компьютера. Функция SetBack используется для задания положения задней отсекающей плоскости на оси z. Функция SetField изменяет фокальное расстояние камеры, воспроизводящей макет.


Рисунок. 3-2. Проекционная система

Интерфейсы и классы

68

Глава 3. Интерфейсы и классы

Функция SetProjection определяет, следует ли применять к изображению корректировку перспективы, или же объекты должны воспроизводиться в простой ортогональной проекции. В большинстве случаев следует пользоваться перспективной проекцией для повышения реализма. Коррекция перспективы рассматривается в главе 8, где мы будем изучать наложение текстур.

Помимо определения исходных условий, основное назначение этого интерфейса связано с выбором объектов в макете. Функция Pick определяет, какой объект (если он имеется) лежит под заданной точкой экрана. Мы подробнее рассмотрим эту функцию в главе 7.

IDirect3DRMFace

Интерфейс IDirect3DRMFace позволяет определить или задать атрибуты одной грани трехмерного объекта. Например, вы можете задать цвет грани функцией SetColor, или же получить вектор, направленный по нормали к ней, функцией GetNormal. Для получения указателя на интерфейс IDirect3DRMFace обычно следует запросить у интерфейса IDirectSDRMMeshBuiIder список граней, после чего выбрать из возвращаемого массива одну конкретную грань. Присвоение цвета грани в функции C3dShape::SetFaceColor происходит следующим образом:

BOOL C3dShape::SetFaceColor(int nFace, double r, double g,

double b) {

if (nFace »= GetFaceCount()) return FALSE;

// Получить список граней IDirect3DRMFaceArray* pIFaces = NULL;

ASSERT
m_hr = m_pIMeshBld-»GetFaces (&pl Faces);

ASSERT(SUCCEEDED(m_hr)) ;

// Выбрать из списка нужную грань IDirect3DRMFace* pIFace = NULL;

m_hr = pIFaces-»GetElement (nFace, SpIFace);

ASSERT(SUCCEEDED(m_hr)) ;

// Задать цвет грани m_hr = pIFace-»SetColorRGB(r, g, b) ;

ASSERT(SUCCEEDED(m_hr)) ;

// Освободить интерфейсы грани и списка граней pIFace-»Release () ;

pIFaces-»Release () ;

return TRUE;

Интерфейсы трехмерной графики ^fit 69

IDirect3DRMLight

Интерфейс IDirect3DRMLight предназначен для управления различными источниками света, поддерживаемыми механизмом визуализации (источники света более подробно рассматриваются в главе 10). Источник света может обладать различными характеристиками, от цвета до закона изменения интенсивности с расстоянием. Приведу простой пример установки цвета источника в функции C3dLight::SetColor:


BOOL C3dLight::SetColor( double r, double g, double b) {

ASSERT(m_pILight) ;

m_hr = m_pILight-»SetColorRGB(D3DVAL(r) , D3DVAL(g), D3DVAL(b));

return SUCCEEDED(m hr) ;

}

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

IDirect3DRMWrap

Покрытие (wrap) определяет способ наложения текстуры на объект. Покрытия могут быть плоскими, цилиндрическими, сферическими и хромовыми. Для наложения покрытий (за исключением хромовых) на сетку используется функция Apply. Хромовое покрытие, предназначенное для имитации отражающих поверхностей, накладывается функцией ApplyRelative; при этом текстура ориентируется по отношению к фрейму, а не к объекту, благодаря чему достигается правильное поведение «отражений» даже при вращении объекта.

Покрытие также можно наложить на одну грань объекта. Ниже приводится функция (из файла 3dlmage.cpp), которая накладывает объект-покрытие C3dWrap на заданную грань объекта C3dShape:

BOOL C3dWrap::Apply(C3dShape* pShape, int nFace) {

ASSERT(pShape) ;

ASSERT(m_pIWrap) ;

if (nFace »= pShape-»GetFaceCount () ) return FALSE;

// Получить список граней IDirect3DRMMeshBuiider* pIBId = pShape-»GetMeshBuilder () ;

ASSERT(pIBId) ;

IDirect3DRMFaceArray* pIFaces = NULL;

m_hr = pIBld-»GetFaces (&pIFaces) ;

ASSERT;SUCCEEDED(m_hr)) ;

// Выбрать из списка нужную грань 70 ЩЩУ Глава 3. Интерфейсы и классы

IDirect3DRMFace* pIFace = NULL;

m_hr = pIFaces-»GetElement (nFace, SpIFace) ;

ASSERT(SUCCEEDED(m_hr)) ;

// Наложить покрытие на грань m_hr = m_pIWrap-»Apply(pIFace);

ASSERT(SUCCEEDED(m_hr)) ;

// Освободить интерфейсы pIFace-»Release () ;

pIFaces-»Release () ;

return SUCCEEDED(m_hr) ;

\

IDirect3DRMMaterial

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

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


IDirect3DRMVisual

Интерфейс IDirect3DRMVisual не содержит собственных функций. Он лишь является базой, от которой порождаются все интерфейсы, которые могут использоваться в качестве визуальных элементов. Хотя в документации по SDK интерфейс IDirect3DRMVisual упоминается довольно часто, обычно он используется лишь как тип аргументов различных функций, как показано в объявлении AddVisual на стр. 64.

IDirect3DRMFrame

IDirect3DRMFrame используется чаще других интерфейсов и служит для изменения свойств фрейма. Например, можно задать положение фрейма функцией SetPosition или определить его ориентацию функцией SetOrientation. Приведу другой пример — функция SetTexture закрепляет за фреймом текстуру, которая используется сетками, прикрепленными к фрейму в качестве визуальных элементов. Таким образом, одна сетка, определяющая форму объекта, может использоваться с различными текстурами. Ниже приводится функция C3dFraiDe::SetPosition, которая пользуется интерфейсом для установки положения фрейма (при наличии объявления IDirect3DRMFrame* m_plFrame):

Интерфейсы трехмерной графики '^^i 71

void C3dFrame::SetPosition(double x, double y, double z, C3dFrame* pRef)

[

ASSERT(m_pIFrame) ;

m_hr = m_pIFrame-»SetPosition(_GetRef (pRef) , D3DVAL(x), D3DVAL(y) , D3DVAL(z)) ;

ASSERT (SUCCEEDED (m_,hr) ) ;

}

Функция SetRotation задает вращение фрейма вокруг заданного вектора, а функция SetVeiocity — скорость вращения. Такая возможность оказывается полезной, если в вашем макете происходит непрерывное движение и вы не хотите постоянно пересчитывать положение объектов.

Если фрейм является корневым (то есть не имеет родительского фрейма), можно задать для него фоновое изображение функцией SceneSetBackground или просто выбрать цвет фона функцией SceneSetBackGroundRGB.

IDirect3DRMMesh

Интерфейс сеток IDirect3DRMMesh в основном используется для задания атрибутов групп внутри сетки. Группой называется набор вершин с общими атрибутами (например, цветом). Группировка сходных элементов повышает производительность визуализации и часто используется абстрактным режимом DirectSD.


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

IDirect3DRMShadow

Интерфейс IDirect3DRMShadow не содержит собственных функций и служит в качестве типа данных для объектов-теней, которые являются разновидностью визуальных элементов. Работа с тенями рассмотрена в главе 10.

IDirect3DRMMeshBuilder

Комплексный интерфейс, используемый для создания трехмерных объектов. Большая часть функций класса C3dShape реализована именно с помощью интерфейса IDirect3DRMMeshBuilder. Интерфейс содержит много функций, от очень простых (например, Load, загружающей сетку из файла на диске) до более сложных, типа функции AddFaces, которая по списку вершин, нормалей (векторов, обозначающих направление) и описаниям граней создает новый набор граней сетки. О применении сеток для создания трехмерных объектов рассказано в главе 4.

Ниже приводится функция C3dShape::Create, которая использует интерфейс построения сеток для создания нового объекта по описаниям вершин и граней (при условии, что переменная m_plMeshBld объявлена как указатель на IDirect3DRMMeshBuilder):

72 аЦ^' Глава 3. Интерфейсы и классы

BOOL CSdShape::Create(D3DVECTOR* pVectors, int iVectors, D3DVECTOR* pNormals, int iNormals, int* pFaceData, BOOL bAutoGen)

(

ASSERT(m_pIMeshBld) ;

// Построить сетку по списку векторов

ASSERT(sizeof(ULONG) == sizeof(int));

m_hr = m_pIMeshBld-»AddFaces (iVe'ctors, pVectors, iNormals, pNormals, (ULONG*)pFaceData, NULL);

ASSERT(SUCCEEDED(m_hr)) ;

if ((iNormals == 0) && bAutoGen) (

m pIMeshBld-»GenerateNormals () ;

}

AttachVisual(m_pIMeshBld) ;

// Разрешить коррекцию перспективы m_pIMeshBld-»SetPerspective (TRUE) ;

return TRUE;

}

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


int C3dShape::GetFaceCount() (

ASSERT(m_pIMeshBld) ;

int i = (int) m_pIMeshBld-»GetFaceCount () ;

return i;

}

IDirect3DRMTexture

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

Функция SetDecalTransparencyColor задает прозрачные области текстуры. Де-калом (decal) называется текстура, которая воспроизводится непосредственно как визуальный элемент и обычно представляет собой что-то вроде плоского спрайта, всегда обращенного лицевой стороной к камере. Тем не менее прозрач-

Интерфейсы трехмерной графики '^¦р1 73

ные текстуры вовсе не обязаны использоваться в качестве декалов. Текстуры подробнее рассмотрены в главе 8, а спрайты — в главе 9.

Библиотека классов 3dPlus

В предыдущих главах уже говорилось, что библиотека классов 3dPlus не претендует на роль ведущего средства для работы с функциями Direct3D. Я спроектировал ее лишь для того, чтобы исследовать концепции трехмерной графики более удобным и привычным способом, чем с использованием СОМ-интерфейсов. Для разработки трехмерных приложений эта библиотека не нужна, однако ее использование в качестве учебного средства или основы для настоящей библиотеки может ускорить вашу работу.

Для проверки указателей и различных условий в библиотеке применяются директивы ASSERT. Во многих случаях ошибки в ваших программах приведут к тому, что отладчик Visual C++ остановится на директиве ASSERT вместо того, чтобы заглохнуть где-нибудь в ядре библиотеки трехмерной графики.

Многие классы 3dPlus представляют собой простейшие оболочки для интерфейсов Direct3D. Отдельные классы предоставляют более высокий уровень функциональности, чем интерфейс. В любом случае я старался сделать так, чтобы вы могли максимально просто обойти класс и напрямую обратиться к базовому интерфейсу. Для этого в большинстве классов библиотеки присутствует функция Getlnterface, которая возвращает указатель на базовый интерфейс. Обратите внимание на то, что перед возвращением указателя она не вызывает функцию AddRef, так что в этом случае вам не следует вызывать функцию Release для указателя — относитесь к нему, как к обычному указателю на объект класса C++.


На Рисунок 3- 3 изображена иерархия классов библиотеки 3dPlus. Я не стал включать в нее классы, относящиеся непосредственно к программному слою DirectDraw. Все классы на Рисунок 3-3 относятся к абстрактному режиму Direct3D.

Классы библиотеки делятся на три группы: производные непосредственно от C3d0bject, производные от C3dVisual и производные от CSdFrame. Если вы посмотрите на иерархию Direct3D, изображенную на Рисунок 3-1 на стр. 65, то увидите, что эти две иерархии во многом схожи. Основное отличие между ними заключается в том, что я сделал некоторые классы производными от C3dFi-ame, чтобы объекты этих классов могли иметь собственное положение и направление и при этом выступать в роли визуальных элементов. Следовательно, по отношению к интерфейсам это означает, что классы, производные от CSdFrame, используют оба интерфейса — IDirect3DRMFrame и IDirecGDRMVisual. Давайте кратко познакомимся с классами 3dPlus, узнаем их назначение и в отдельных случаях посмотрим, как ими пользоваться.

C3dEngine

Класс C3dEngine объединяет несколько глобальных функций механизма визуализации. Библиотека классов 3dPlus содержит всего один глобальный объект этого класса с именем the3dEngine. Функции данного класса чаще всего используются для создания других объектов, относящихся к механизму визуализации, и возвращают указатель на интерфейс. Обычно вам не придется непосредственно пользоваться этим классом в своих приложениях, однако при создании объектов других классов нередко применяется код C3dEngine. В приведенном ниже примере показано, как работают с объектом the3dEngine:

74 ¦¦у Глава 3. Интерфейсы и классы

Рисунок. 3-3. Иерархия классов библиотеки 3dPlus

Интерфейсы и классы

Библиотека классов 3dPlus

BOOL C3dFrame::Create(C3dFrame* pParent)

{

if (m_plFrame) {

m pIFrame-»Release () ;

m_plFrame = NULL;

}

if (!the3dEngine.CreateFrame(_GetRef(pParent), &m_plFrame)) (

TRACE ("Frame create failedW);

m_plFrame = NULL;

return FALSE;

} ASSERT(m_plFrame) ;

m_pIFrame-»SetAppData ( (ULONG) this) ;


return TRUE;

}

Именно функция CreateFrame объекта the3dEngine фактически создает интерфейс фрейма и присваивает указатель на него переменной фрейма m_plFrame.

C3dMatrix

В механизме визуализации предусмотрена собственная матрица 4х4 для преобразований координат, однако я предпочитаю пользоваться классами C++, поскольку они сокращают объем программного кода. Например, найдите в описании класса C3dFrame на стр. 81 функцию C3dPosCtrl::OnUpdate, и вы увидите, как матрица используется для вращения двух векторов. Программа выглядит до смешного простой, невзирая на сложный математический базис вычислений. Классы C++ позволяют чрезвычайно гибко работать с матрицами, не загромождая программу.

Разумеется, вы не обязаны пользоваться классами C3dMatrix и C3d Vector. Однако при работе с другими классами библиотеки SdPlus вы увидите, что наличие классов для матриц и векторов упрощает вашу работу. Матрицы подробнее рассмотрены в главе 5.

C3dDevice

Класс C3dDevice представляет собой простую оболочку для интерфейса IDirect3DRMDevice. Класс C3dStage пользуется C3dDevice для создания окружения, в котором отображаются трехмерные объекты. Вероятно, вам не придется обращаться к объектам этого класса, если только вы не надумаете полностью пересмотреть концепцию сцены. Тем не менее вам может пригодиться функция SetQuality, устанавливающая качество визуализации в вашем приложении. Работу с классом устройства рассмотрим на примере функции для создания сцены:

BOOL C3dStage::Create(CDirect3D* pD3D) {

// Создать новое устройство по поверхностям Direct3D

76 ШУ Глава 3. Интерфейсы и классы

if (!m_Device.Create(pD3D)) return FALSE;

// Установить текущее качество визуализации m_Device.SetQuality(m_Quality) ;

}

C3dViewport

Класс C3dViewport представляет собой простую оболочку для интерфейса IDirectSDRMViewport. Маловероятно, что вам придется непосредственно работать с этим классом, поскольку объект C3dStage берет управление ракурсом на себя. Ниже приводится функция класса сцены, которая воспроизводит на экране текущее состояние макета:


void C3dStage::Render()

{

ASSERT(m_plFrame) ;

// Очистить ракурс m_Viewport.Clear ();

if (m_pScene) {

// Воспроизвести макет m_Viewport.Render(m_pScene) ;

}

// Обновить изображение m Device.Update() ;

)

Как видите, пользоваться классами- оболочками очень просто. Различные функции класса скрывают функции базового СОМ-интерфеиса и упрощают программу.

CSdWrap

Класс C3dWrap (определяемый в Sdlmage.cpp) также в основном используется как оболочка интерфейса IDirectSDRMWrap, однако он обладает и самостоятельной ценностью. Функция Apply реализована в двух версиях. Ниже приведен первый, более простой вариант, при котором покрытие накладывается на весь объект:

BOOL C3dWrap::Apply(CSdShape* pShape) (

ASSERT(pShape) ;

ASSERT(m_pIWrap) ;

HRESULT hr;

Библиотека классов 3dPlus ^Ш 77

hr = m_pIWrap-»Apply(pShape-»GetVisual () ) ;

return SUCCEEDED(hr) ;

)

Второй вариант, приведенный на стр. 70, накладывает покрытие лишь на одну грань.

Наличие двух разных вариантов функции Apply упрощает код и одновременно сохраняет гибкость в реализации. Покрытия, в том числе и хромовые, подробно рассмотрены в главе 8, посвященной текстурам.

C3dVisual

Базовый класс для всех классов, объекты которых используются в качестве визуальных элементов макета. C3dVisual содержит переменную, в которой хранится имя объекта. Для задания и получения этого имени служат функции SetName и GetName. Имена помогают при выделении объектов с помощью мыши — отображение имени объекта позволяет проверить выбор.

C3dlmage

Единственный класс, который не пользуется никакими интерфейсами, входящими в механизм визуализации. Он предназначен для загрузки изображений из файлов на диске или ресурсов приложения и их последующего использования в качестве текстур или декалов. Механизм визуализации определяет для таких изображений специальную структуру с именем D3DRMIMAGE. Класс C3dlmage пользуется ей для хранения данных загруженного изображения. В приведенном ниже примере класс C3dlmage применяется для загрузки из файла на диске изображения, которое будет использовано в качестве фонового изображения макета:


C3dlmage* pimg = new C3dlmage;

if ( !pImg-»Load() ) {

delete pimg;

return;

}

ASSERT(m_pScene) ;

m pScene-»m_ImgList .Append (pimg) ;

m_pScene~»SetBackground (pimg) ;

Функция C3dlmage:: Load вызвана без аргументов, поэтому на экране появляется окно диалога. Здесь пользователь может выбрать растровый (bitmap) файл Windows, который будет служить фоновым изображением. Кроме того, загружаемый растр можно выбрать и другим способом — передавая функции Load имя файла или идентификатор растрового ресурса. В приведенном ниже примере мы загружаем растровый ресурс и затем используем его для создания текстуры:

// Загрузить изображение земного шара C3dlmage* pimgl = new C3dlmage;

if ( !pImgl-»Load(IDB_WORLD) ) {

78 Щу Глава 3. Интерфейсы и классы

AfxMessageBox("Failed to load worldl.bmp");

delete pimgl;

return;

} m_pScene-»m_ImgList. Append (pimgl) ;

// Создать текстуру по изображению C3dTexture texl;

texl.Create(pimgl);

C3dTexture

Класс C3dTexture (определяемый в 3dlmage.cpp) является оболочкой интерфейса IDirect3DRMTexture. Как видно из приведенного выше примера, текстуры создаются на основе графических изображений. Размеры сторон у таких изображений должны быть равны степеням двойки. Так, параметры 32х64, 128х128 и 4х8 подходят для создания текстур; а величины 32х45 и 11х16 являются недопустимыми. Если изображение имеет неправильный размер, функция C3dTexture::Create завершится неудачей:

BOOL C3dTexture::Create() {

if (m_pITexture) (

m_pITexture-»Release () ;

m_pITexture = NULL;

}

// Убедиться, что размеры изображения равны степеням 2 for (int i = 0; (1 «« i) « GetWidth(); i++);

for (int j = 0; (1 «« j) « GetHeight(); j++);

if (GetWidthf) != (1 «« i) ¦¦ GetHeightO != (1 «« j)) ( TRACE("This image can't be used as a texture."\ " Its sides are not exact powers of 2\n");

}

if (!the3dEngine.CreateTexture(GetObject(), &m_pITexture)) {

TRACE("Texture create failed\n");

m pITexture = NULL;

return FALSE;

}

ASSERT(m_pITexture) ;


return TRUE;

}

Текстуры воспроизводятся на экране с учетом покрытий. Покрытие определяет алгоритм, в соответствии с которым текстура накладывается на объект.

^^teb Библиотека классов SdPlus ж! 79

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

C3dlmage* pimgl = new C3dlmage;

pImgl-»Load(IDB_LEAVES) ;

C3dTexture texl;

texl.Create(pimgl) ;

C3dWrap wrap;

wrap.Create(D3DRMWRAP_CYLINDER, NULL,

0, 0, 0, // Начало координат

О, 0, 1, // Направление

О, 1, 0 // Вверх

О, 0, // Начало текстуры

1, 1); // Масштаб текстуры

pTree-»SetTexture (Stexl) ;

wrap.Apply(pTree);

C3dFrame

Класс C3dFrame является оболочкой интерфейса IDirect3DRMFrame и включает несколько дополнительных функций, облегчающих работу с ним. Фреймы содержат ряд атрибутов, в число которых входит положение фрейма и его ориентация в трехмерном пространстве. Положение фрейма устанавливается функцией SetPosition, а ориентация (то есть направление, в котором обращен фрейм) — функцией SetDirection. Для точного определения ориентации необходимо указать два вектора. Первый вектор описывает переднее направление, а второй — верхнее. Рассмотрим ситуацию на примере летящего самолета. Передний вектор (или вектор направления) — это курс, по которому летит самолет, то есть направление, в котором ориентирован его нос. Верхний вектор показывает, куда обращено хвостовое перо самолета — вверх, вниз, влево и т. д. Для некоторых объектов верхний вектор оказывается несущественным. Например, в вашем макете может присутствовать конус, указывающий на некоторый объект. Ориентация конуса совпадает с направлением, куда смотрит его вершина. Верхний вектор не имеет никакого значения, поскольку при вращении конуса вокруг продольной оси его внешний вид не меняется. Чтобы упростить вашу работу, функция SetDirection позволяет задать только передний вектор и определяет верхний вектор за вас. Вот как это делается:

void C3dFrame::SetDirection(double dx, double dy, double dz,


C3dFrame* pRef) f

ASSERT(m_pIFrame) ;

// Создать передний вектор C3dVector d(dx, dy, dz);

80

Глава З. Интерфейсы и классы

// Сгенерировать верхний вектор C3dVector u = d.GenerateUpO;

SetDirection(d.x, d.y, d.z, u.x, u.y, u.z, pRef);

}

Класс C3dVector содержит ряд функции для генерации верхних векторов, благодаря которым работа с классом упрощается до предела. Мне это нравится.

Во всех функциях для определения положения и ориентации присутствует обязательный аргумент — эталонный фрейм (pRef в приведенном выше примере). Он чрезвычайно важен, поскольку ваш фрейм может находиться в произвольном месте иерархии фреймов, а его положение определяется его собственным преобразованием вместе с преобразованиями всех родительских фреймов. Это напоминает бег по кухне; если перенести ваш дом из Вашингтона в Колорадо, вы все равно сможете бегать по кухне, но ваше положение на планете при этом изменится. Другими словами, любые перемещения происходят по отношению к некоторому эталонному фрейму. Для удобства можно передать вместо эталонного фрейма NULL, и тогда за эталон будет принят фрейм-родитель. Примером использования эталонного фрейма служит функция из файла SdlnCtlr.cpp, которая позиционирует объекты в макете по мере того, как пользователь перемещает их с помощью клавиатуры, мыши или джойстика:

void C3dPosCtlr::OnUpdate(_3DINPUTSTATE& st,

C3dFrame* pFrame) {

// Получить указатель на сцену, которая будет

// использоваться

// в качестве эталона при определении положений

// фреймов и т. д.

ASSERT(m_pWnd) ;

C3dStage* pStage = m_pWnd-»GetStage () ;

ASSERT(pStage) ;

double x, y, z;

pFrame-»GetPosition (x, y, z, pStage);

x += st.dX * 0.1;

y += st.dY * 0.1;

z += st.dZ * 0.1;

pFrame-»SetPosition (x, y, z, pStage);

C3dVector d, u;

pFrame-»GetDirection (d, u, pStage);

// Повернуть вектор направления и верхний вектор double a = 3.0;

C3dMatrix r;

r.Rotatef-st.dR * a, -st.dU * a, -st.dV * a) ;

d = r * d;

u = r * u;

pFrame-»SetDirection (d, u, pStage);

}

Библиотека классов SdPlus '^Ш 81


Нетрудно убедиться, что все положения объектов задаются относительно сцены; именно на такое поведение рассчитывает пользователь, когда он перемещает объекты по сцене.

C3dScene

Класс C3dScene содержит всю информацию, необходимую для описания макета:

источники света, список фигур, текущее фоновое изображение и настройку камеры. В любой момент времени к сцене может быть присоединен только один объект-макет C3dScene. Макет содержит встроенный источник рассеянного света, параметры которого задаются функцией SetAmbientLight. Вы можете включить в макет и другие источники света, вызывая функцию AddLight. Макет возглавляет всю иерархию фреймов, поэтому трехмерные фигуры (которые также являются фреймами) присоединяются к ней функцией AddChild. Вы можете задать цвет фона макета функцией SetBackgroiind(r, g, Ь) или же вместо этого указать фоновое изображение, для чего используется функция SetBackground(plmage). Функция Move обновляет положение всех движущихся объектов макета и воспроизводит текущий макет в окне приложения. Вызов функции Move приводит к каким-то результатам лишь в том случае, если макет присоединен к сцене. В объекте C3dScene также хранятся векторы, определяющие положение и направление камеры. Значения этих векторов задаются функциями SetCameraPosition и SetCameraDirection. Приведенный ниже фрагмент программы создает новый макет и задает исходные параметры источников света:

// Создать исходный макет m_pScene = new C3dScene;

if ( !m_pScene-»Create () ) return FALSE;

// Установить источники света C3dDirLight dl;

dl.Create(0.8, 0.8, 0.8);

m_pScene-»AddChild(&dl) ;

dl.SetPosition(-2, 2, -5);

dl.SetDirection(1, -1, 1);

m_pScene-»SetAmbientLight (0 . 4, 0.4, 0.4);

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

Объект C3dScene содержит два объекта-списка, которые облегчают работу с макетами. Список m_ShapeList помогает определить перечень объектов C3dShape, а список mJmageUst — объектов C3dlmage, удаляемых при уничтожении макета. Новью объекты заносятся в эти списки только при вызове функции Append объекта-списка. Вы не можете свободно манипулировать содержимым этих списков.


C3dSprite

Класс C3dSprite поддерживает работу с плоскими объектами в объемном мире. В документации по DirectX 2 SDK такие спрайты именуются декадами. В главе 9

82 ЩЦ^ Глава 3. Интерфейсы и классы

показано, как можно пользоваться спрайтами в играх, где производительность важнее подлинной объемности изображения. Класс CSdSprite является производным от CSdFrame, поэтому спрайты обладают теми же возможностями позиционирования, что и трехмерные объекты.

CSdCamera

Класс СЗсЮатега не содержит собственных функций и в сущности вообще ничего не делает. Он является производным от C3dFrame и позволяет установить положение и ориентацию камеры — все, что нужно, чтобы камера пронеслась над макетом или просто была обращена в одном направлении.

C3dShape

Класс C3dShape сочетает функциональность интерфейсов фрейма и визуального элемента, что позволяет создавать трехмерные фигуры, для которых можно задать непосредственное расположение в макете. Одна фигура легко присоединяется к другой в качестве потомка, так что вы можете строить сложные фигуры. В класс входит несколько функций, предназначенных для создания простых геометрических фигур (сфер, цилиндров и конусов), а также функция Load, которая позволяет создать объект по данным из файла .х. Вы можете задать цвет и текстуру всей фигуры или ее отдельных граней. При наложении текстур на отдельные грани имеются ограничения, о которых рассказано в главе 8. В приведенном ниже примере мы создаем простую геометрическую фигуру и присоединяем ее к текущему макету:

C3dShape shi;

shi.CreateCube(2) ;

m_pScene-»AddChild(&shl) ;

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


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

C3dStage

Класс C3dStage используется классом C3dWnd и обеспечивает устройство и ракурс, необходимые для отображения трехмерного макета в окне. Данный класс

Библиотека классов 3dPlus ^iS. 83

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

BOOL C3dWnd::SetScene(C3dScene* pScene)

{

if (!m_pStage) return FALSE;

m_pScene = pScene;

m_pStage-»SetScene (m_p3cene) ;

if (m_pScene) (

// Разрешить воспроизведение

// во время пассивной работы приложения m_bEnableUpdates = TRUE;

( else (

m_bEnableUpdates = FALSE;

}

return TRUE;

}

C3dLighf

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

BOOL C3dLight::Create(D3DRMLIGHTTYPE type,

double r, double g, double b)

{

// Создать фрейм для источника света if (!C3dFrame::Create(NULL)) return FALSE;

// Создать объект-источник света ASSERT(m_pILight == NULL);

if (!the3dEngine.CreateLight(type, r, g, b, &m_pILight)) ( return FALSE;

} ASSERT(m_pILight) ;

// Присоединить источник света к его фрейму ASSERT(m_plFrame) ;

m_hr = m_pIFrame-»AddLight (m pILight);

if (FAILED(m_hr)) return FALSE;

return TRUE;

}

84 liy Глава 3. Интерфейсы и классы

CSdAmbLight


Класс C3dAmbLight реализует рассеянный источник света, встроенный в объект C3dScene. Вместо того чтобы вызывать функции этого класса, можно работать с источником рассеянного света в макете посредством C3dScene::SetAmbientLight.

C3dDirLight

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

C3dDirLight dl;

dl.Create(0.8, 0.8, 0.8);

m_pScene-»AddChild(&dl) ;

dl.SetPosition(-2, 2, -5);

dl.SetDirection(l, -1, 1);

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

Класс C3dLight имеет еще несколько производных классов. Все различные типы источников света рассмотрены в главе 10.

C3dShapeList, C3dlmageList, C3dFrameList

Эти классы предназначены для хранения списка объектов C3dShape (C3dShapeList), C3dlmage (C3dlmageList), C3dFrame (C3dFrameList). Каждый из этих классов является производным от CObList, входящего в библиотеку MFC. Подробное описание работы CObList можно найти в документации по MFC. Все эти списки содержатся в объекте C3dScene и помогают найти фигуры, изображения и фреймы, которые необходимо удалить при уничтожении макета.

C3dWnd

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

// Создать трехмерное окно if (!m_wnd3d.Create(this, IDC_3DWND)) { return -1;

}

Библиотека классов SdPlus ^Щ 85

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


m_pScene = new C3dScene;

if ( !m_pScene-»Create () ) return FALSE;

// Установить источники света C3dDirLight dl;

dl.Create(0.8, 0.8, 0.8);

m_pScene-»AddChild(&dl) ;

dl.SetPosition(-2, 2, -5);

dl.SetDirection(1, -1, 1);

m_pScene-»SetAmbientLight (0.4, 0.4, 0.4);

m wnd3d.SetScene(m pScene);

C3dVector

В механизме визуализации определен трехмерный вектор D3DVECTOR, который представляет собой обычную структуру, однако я предпочитаю работать с векторными объектами C++. При таком подходе я могу иметь несколько конструкторов, упрощающих создание вектора, и, конечно же, реализовать операторы (например, сложения и умножения) для упрощения кода. Класс C3dVector является производным от структуры D3DVECTOR, так что во всех случаях, где допускается присутствие структуры D3DVECTOR, вы можете пользоваться объектом C3dVector. В качестве примера использования класса C3dVector приведу фрагмент программы, который обновляет положение трехмерного объекта, перемещаемого пользователем по экрану:

C3dVector d, u;

pFrame-»GetDirection (d, и, pStage);

// Повернуть вектор направления и верхний вектор double a = 3.0;

C3dMatrix r;

r.Rotate(-st.dR * a, -st.dU * a, -st.dv * a) ;

d = r * d;

u = r * u;

pFrame-»SetDirection (d, u, pStage);

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

86 в¦?' Глава 3. Интерфейсы и классы

Классы DirectDraw в библиотеке 3d Plus

В библиотеку 3dPlus входит несколько классов для поддержки интерфейсов DirectDraw. Эти классы изображены на Рисунок 3-4.

Рисунок. 3-4. Вспомогательные классы DirectDraw из библиотеки 3dPlus

Интерфейсы и классы

Такие классы представляют собой простейшие оболочки над базовыми интерфейсами DirectDraw (более подробное описание интерфейсов DirectDraw можно найти в главе 12). Код, реализующий эти классы, находится в файле 3dDirDraw.cpp в каталоге с исходными текстами библиотеки 3dPlus. При его написании я взял за основу код приложения VIEWER, входящего в комплект DirectX 2 SDK. Я реализовал лишь минимальный набор функций, обеспечивающий работу моего класса CSdDevice. Разумеется, вы обладаете полным правом просмотреть исходный текст и сделать с ним все, что сочтете нужным.


В главе 13 подробно рассмотрены интерфейсы DirectDraw, для которых в библиотеке 3dPlus вообще нет классов-оболочек. Однако в коде для главы 13 все же есть достаточно «тонкие» классы-оболочки для этих интерфейсов.

Спасибо за внимание

Мы очень кратко рассмотрели все интерфейсы и классы C++, которыми будем пользоваться в оставшейся части книги для создания приложений-примеров. Несомненно, изложенный материал вызвал у вас множество вопросов; все, что упоминалось в этой главе, более подробно рассматривается в последующих главах. Если вы не можете отличить функции библиотеки 3dPlus от функций интерфейсов Direct3D, помните: все интерфейсные указатели имеют префикс pi, a указатели на объекты библиотеки 3dPlus — префикс р. Кроме того, не забывайте, что классы C++ нередко являются лишь удобными оболочками для интерфейсов Direct3D. Только немногочисленные классы (например, C3dShape) реально обладают собственным содержанием. В последующих главах мы изучим их более подробно.

Клипы

Глава 11 Клипы
Покадровая съемка

Клипы

На память мне приходит анекдот времен моего детства, которым я поделюсь с вами, хочется вам того или нет. Известный режиссер снимает в пустыне фильм о ковбоях. Тысячи статистов, томящихся под палящим солнцем, готовы к участию в грандиозной массовой сцене. На вершинах трех холмов стоят камеры, которые должны запечатлеть происходящее в различных ракурсах. Режиссер дает сигнал, статисты бросаются в бой. После завершения сцены режиссер спрашивает операторов, нормально ли прошла съемка. Первый оператор отвечает, что у него в камере заело пленку и он пропустил всю сцену. Второй говорит, что его камеру окутало облако пыли и ему удалось отснять всего несколько секунд. Третий оператор молчит. Режиссер кричит: «Эй, там, третья камера! Как дела?» Оператор отвечает: «Все готово, можно снимать!»
Концепция покадровой съемки достаточно проста: при каждом перемещении и обсчете макета мы захватываем изображение на экране и сохраняем его в списке. Затем клип воспроизводится путем последовательного копирования на экран изображений из списка. Если результат нас устраивает, список сохраняется в файле в формате, который может быть использован другой программой. Полная последовательность действий такова:
1. Подготовить пустой список изображений.
2. Сохранить текущую палитру (для 256-цветной видеосистемы).
3. Сохранить текущий размер трехмерного окна.
4. Переместить объекты макета в новое положение.
5. Воспроизвести макет во вторичном буфере.
6. Сохранить содержимое вторичного буфера.
7. Включить полученное изображение в список.
8. Повторить нужное количество раз, начиная с шага 4.
Перед тем как приступать к изучению программы, реализующей этот алгоритм, я должен предупредить вас, что он быстро поглощает системную память. Например, давайте представим, что у нас имеется окно размером 320х200 пикселей на 256-цветном дисплее. На каждое изображение потребуется 76,800 байт.
Покадровая съемка '''^Ц' 251
Если съемка будет производиться с частотой 10 кадров в секунду (fps), то за 10 секунд будет израсходовано 7,680,000 байт памяти (не считая накладных расходов по ведению списка, заголовков изображений и т. д.). При запуске приложения следует приготовиться к тому, что после исчерпания всей свободной памяти ваш диск начнет быстро заполняться. Разумеется, существует несколько способов выйти из положения — например, сжимать графические данные или непосредственно записывать изображения в файл на диске, однако мы рассмотрим их позднее. А пока давайте посмотрим, как же выполняется простейшая покадровая съемка.

Классы для работы с клипами

Я включил в библиотеку 3dPlus три класса, предназначенных для записи клипов. Объекты CMovieFrame используются для хранения отдельных кадров, объекты класса CMovieTake — для хранения палитры и списка кадров (одного эпизода), а объекты CMovie — для хранения целого клипа (который теоретически может состоять из нескольких эпизодов). Тем не менее, в отличие от режиссера из анекдота, мы снимаем все с первого раза, так что объект CMovie будет содержать только один объект CMovieTake.

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

Сохранение палитры

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

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

На проблеме только одной палитры хлопоты не кончаются. Нам понадобится особая разновидность палитры, которая называется идентичной палитрой (identity palette) и точно совпадает с текущей системной палитрой. Идентичная палитра позволяет напрямую копировать растровое изображение в видеопамять, обходясь без преобразования цветов. Преобразование цветового индекса каждого пикселя растра в цветовой индекс физической палитры происходит чрезвычайно медленно. Более подробно этот вопрос рассмотрен в книге «Animation Techniques in Win32» (Thomson, Microsoft Press, 1995).


Раз нам приходится работать с одной палитрой, будет вполне логично сохранить ее в начале съемки клипа. Давайте посмотрим, как это делается. Начнем с

/b> ИЦГ Глава 11. Клипы

delete pip;

return pPal;

}

Для получения цветов палитры от интерфейса используется структура LOGPALETTE; они передаются приложению в объекте CPalette. Класс CPalette, входящий в библиотеку MFC, хорошо подходит для хранения набора цветов; мы убедимся в этом, когда будем рассматривать воспроизведение клипа в окне приложения.

Функция класса CMovie, запрашивающая палитру у объекта DirectDraw в начале съемки, выглядит следующим образом:

BOOL CMovie::Record() {

Stop () ;

// Удалить все, что было записано ранее m Take.DeleteAll();

m iCurFrame =0;

// Сохранить текущую палитру ASSERT(m_p3dWnd) ;

CDirectDraw* pDD = m_p3dWnd-»GetDD () ;

ASSERT(pDD) ;

m_Take. SetPalette (pDD-»GrabPalette () ) ;

// Сохранить размер кадра CRect re;

m_p3dWnd-»GetClientRect (&rc) ;

m_Take.SetSize(re.Width(), re.Height()) ;

// Начать запись m_b!sRecording = TRUE;

return TRUE;

}

Как нетрудно убедиться, палитра сохраняется в текущем (и только в текущем!) объекте класса CMovieTakenpH помощи функции CMovieTake::SetPalette. Кроме того, мы сохраняем размер клиентной области текущего окна. Запись кадров происходит во время пассивной работы приложения.

Запись изображений из буфера

Захват изображения из буфера визуализации — задача достаточно сложная, поскольку при этом необходимо учесть множество разных тонкостей. Ниже приведен фрагмент функции CMovie::Update, вызываемой в цикле пассивной работы приложения для записи кадров по воспроизводимым изображениям.

/b> ШУ Глава 11. Клипы

// Получить указатель на объект DirectDraw в трехмерном

окне

ASSERT(m_p3dWnd) ;

CDirectDraw* pDD = m_p3dWnd-»GetDD () ;

ASSERT(pDD) ;

// Получить текущее изображение BITMAPINFO* pBMI = NULL;

BYTE* pBits = NULL;

HBITMAP hBmp = pDD-»GrabImage (&pBMI, (void**)&pBits);

ASSERT(hBmp) ;

ASSERT(pBMI) ;

ASSERT(pBits) ;


// Создать кадр клипа

CMovieFrame* pFrame = new CMovieFrame(hBmp, pBMI,

pBits) ;

// Включить кадр в эпизод m_Take.AddTail(pFrame) ;

m iCurFrame++;

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

Каждый кадр представляет собой область памяти, которая называется секцией аппаратно-независимого растра (device-independent bitmap section, или сокращенно DIB-секция). DIB-секцию можно использовать в качестве растра Windows (при помощи связанного с ней логического номера HBITMAP) и напрямую записывать в нее данные через указатель. Вскоре мы увидим, как создается DIB-секция, а пока следует обратить внимание на то, что переменные hBmp, pBMI и pBits, содержащиеся в приведенном выше фрагменте, связаны с той или иной частью полученного изображения. Данное обстоятельство может привести к определенным затруднениям при попытке освободить память, выделенную под DIB-секцию. Для этого необходимо вызвать функцию ::DeleteObject для логического номера HBITMAP (hBmp), не пытаясь удалить данные по указателю pBits. Указатель pBMI ссылается на заголовочный блок, который должен удаляться отдельно от графических данных растра. Кому-то все это покажется неоправданно усложненным, однако к DIB-секции можно обращаться несколькими способами, и все выше указанные переменные необходимы для эффективной работы с ней.

Функция CDirectDraw::Grablmage выглядит значительно сложнее, поскольку ей приходится поддерживать различные форматы буфера. В общих чертах процесс выглядит примерно так: мы получаем описание типа буфера, создаем структуру типа BITMAPINFO для описания DIB-секции, создаем DIB-секцию, затем задаем маски сдвига и наконец копируем графические данные. При этом возникают определенные сложности, поскольку формат пикселей поверхностей DirectDraw не всегда совпадает с форматом пикселей DIB-секции; а следовательно, нам при-

Покадровая съемка ''Д1 255


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

Полагаю, я достаточно подготовил вас. Ниже приведена функция, которая получает изображение из буфера:

// Получить текущее изображение из буфера.

// Если функция возвращает информационный заголовок

// или графические данные, вызывающая функция должна

// освобождать соответствующую память вызовами delete pBMI

// и ::DeleteObject(hBmp). Не пытайтесь применять delete

// к графическим данным, поскольку они принадлежат

// объекту-растру.

HBITMAP CDirectDraw::GrabImage(BITMAPINFO** ppBMI,

void** ppBits) {

// Задать исходные значения, возвращаемые функцией

if (ppBMI) *ppBMI= NULL;

if (ppBits) *ppBits = NULL;

if (!m_pBackBuffer) return NULL;

// Заблокировать вторичный буфер, чтобы получить

// его описание.

// ВНИМАНИЕ: вы не сможете осуществить пошаговое

// выполнение

// этого фрагмента в отладчике — поверхность GDI

// блокируется.

LPDIRECTDRAWSURFACE iS = m_pBackBuffer-»GetInterface () ;

ASSERT;iS) ;

DDSURFACEDESC ds;

ds.dwSize = sizeof(ds);

m_hr = i3-»Lock(NULL,

&ds,

DDLOCK_WAIT,

NULL) ;

if (m_hr != DD_OK) {

TRACE("Failed to lock surface\n");

return NULL; // Failed to lock surface }

// Разблокировать поверхность, чтобы можно было // воспользоваться отладчиком //и при необходимости выйти из приложения m_hr = m_pBackBuffer-»GetInterface()-»Unlock(ds.lpSurface) ;

// Убедиться, что поверхность относится к одному

// из типов,

// с которыми мы можем работать.

// Примечание: программа обрабатывает только

/b> ЯИЦ1 Глава 11. Клипы

// поверхности с 8-,

// 16- и 24-битной кодировкой пикселей.

if (!(ds.ddpfPixelFormat.dwFlags & DDPF_PALETTEINDEXED8) && '(ds.ddpfPixelFormat.dwFlags & DDPF_RGB)) { return NULL; // Формат не поддерживается программой

} int iBitCount;

if (ds.ddpfPixelFormat.dwFiags & DDPF_PALETTEINDEXED8) {

iBitCount = 8;

} else if (ds.ddpfPixelFormat.dwFlags & DDPF_RGB) {

// Проверить поверхность на допустимость типа


// цветовой кодировки пикселей

iBitCount = ds.ddpfPixel Format.dwRGBBitCount;

if ((iBitCount != 16) && (iBitCount != 24)) ( return NULL; // He поддерживается программой

}

}

ASSERT(ds.dwFlags & DDSD_WIDTH);

int iWidth = ds.dwWidth;

ASSERT(ds.dwFlags & DDSD_HEIGHT) ;

int iHeight = ds.dwHeight;

// Проверить, нужно ли создавать цветовую таблицу int iCIrTabEntries = 0;

if (ds.ddpfPixelFormat.dwFlags & DDPF_PALETTEINDEXED8

// Построить цветовую таблицу iCIrTabEntries = 256;

iBitCount = 8;

// Создать структуру BITMAPINFO, описывающую растр int iSize = sizeof(BITMAPINFO) + iCIrTabEntries * sizeof(RGBQUAD) ;

BITMAPINFO* pBMI = (BITMAPINFO*) new BYTE[iSize];

memsetfpBMI, 0, iSize);

pBMI-»bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

pBMI-»bmiHeader.biWidth = iWidth;

pBMI-»bmiHeader.biHeight = iHeight;

pBMI-»bmiHeader. biPlanes = 1;

pBMI-»bmiHeader.biBitCount = iBitCount;

pBMI-»bmiHeader.biClrUsed = iCIrTabEntries;

HOC hdcScreen = ::GetDC(NULL) ;

// Создать цветовую таблицу, если необходимо if (iCIrTabEntries » 0) (

ASSERT(iClrTabEntries«= 256);

PALETTEENTRY ре[256];

Покадровая съемка ''illi' 257

ASSERT(m_pPalette) ;

m_pPalette-»GetInterface () -»GetEntries (0, 0,

iCIrTabEntries, pe) ;

for (int i = 0; i « iCIrTabEntries; i++) ( pBMI-»bmiColors [i] .rgbRed = pe[i].peRed;

pBMI-»bmiColors [i] .rgbGreen = pe[i].peGreen;

pBMI-»bmiColors [i] .rgbBlue = pe[i].peBlue;

} >

// Создать DIB-секцию, размер которой

// совпадает с размером вторичного буфера

BYTE* pBits = NULL;

HBITMAP hBmp = ::CreateDIBSection(hdcScreen, pBMI,

DIB_RGB_COLORS, (VOID**)&pBits, NULL, 0);

::ReleaseDC(NULL, hdcScreen);

if (!hBmp) { delete pBMI;

return NULL;

}

ASSERT(pBits) ;

// Скопировать графические данные на поверхность DIB

int iDIBScan =

( ( (pBMI-»bmiHeader.biWidth

* pBMI-»bmiHeader.biBitCount) + 31) & -31) »» 3;

int iSurfScan = ds.lPitch;

BYTE* pDIBLine = pBits + (iHeight - 1) * iDIBScan;

BYTE* pSurfLine = (BYTE*)ds.IpSurface;


// Сдвигать вниз до тех пор, пока младший байт источника //не совпадет с младшим байтом приемника. // Сдвигать снова, пока не будет достигнута точность // в 5 бит. DWORD dwRShift = O.DWORD dwGShift = 0;

DWORD dwBShift = O.DWORD dwNotMask;

if ((ds.ddpfPixelFormat.dwFlags & DDPF RGB) &&

(iBitCount »= 16)) {

if (iBitCount == 16) { dwNotMask = OxFFFFFFEO;

/b> Глава 11. Клипы

} else {

dwNotMask = OxFFFFFFOO;

} DWORD dwMask = ds.ddpfPixelFormat.dwRBitMask;

ASSERT(dwMask) ;

while ((dwMask & 0х01) == 0) {

dwRShift++;

dwMask = dwMask »» 1;

} while ((dwMask & dwNotMask) != 0) {

dwRShift++;

dwMask = dwMask »» 1;

} dwMask = ds.ddpfPixelFormat.dwGBitMask;

ASSERT(dwMask) ;

while ((dwMask & 0х01) == 0) (

dwGShift++;

dwMask = dwMask »» 1;

) while ((dwMask & dwNotMask) != 0) {

dwGShift++;

dwMask = dwMask »» 1;

}

dwMask = ds.ddpfPixelFormat.dwBBitMask;

ASSERT(dwMask) ;

while ((dwMask & 0х01) == 0) {

dwBShift++;

dwMask = dwMask »» 1;

) while ((dwMask & dwNotMask) != 0) {

dwBShift++;

dwMask = dwMask »» 1;

} i

// Снова заблокировать поверхность // для получения графических данных m_hr = iS-»Lock(NULL,

Sds,

DDLOCK_SURFACEMEMORYPTR I DDLOCK_WAIT,

NULL) ;

ASSERT(m_hr == DD_OK) ;

for (int у = 0; у « iHeight; y++) { switch (iBitCount) { case 8: {

BYTE* pDIBPix = pDIBLine;

BYTE* pSurfPix = pSurfLine;

for (int x = 0; x « iWidth; x++) {*pDIBPix++ =

Покадровая съемка 'Ч^Щ 259

// *pSurfPix++;

} } break;

case 16: (

WORD* pDIBPix = (WORD*)pDIBLine;

WORD* pSurfPix = (WORD*)pSurfLine;

WORD r, g, b;

// (int x = 0; x « iWidth; x++) ( r = (*pSurfPix & (WORD)

ds.ddpfPixel Format.dwRBitMask) »» dwRShift;

g = (*pSurfPix & (WORD)

ds.ddpfPixelFormat.dwGBitMask) »» dwGShift;

b = (*pSurfPix & (WORD)

ds.ddpfPixeiFormat.dwBBitMask) »» dwBShift;

*pDIBPix++ = ((r & OxIF) «« 10) ¦ ( (g & OxIF) «« 5) I (b S OxIF) ;

p3urfPix++;

} } break;

case 24: {

BYTE* pDIBPix = pDIBLine;

BYTE* pSurfPix = pSurfLine;


for (int x = 0; x « iWidth; x++) (

// ВНИМАНИЕ: Предполагается, что RGB-маски // поверхности и DIB-секции совпадают, что // на самом деле не всегда справедливо. // Нам следовало бы рассматривать маску // ds.ddpfPixelFormat.dwRGBBitMask.

*pDIBPix++ = *pSurfPix++;

*pDIBPix++ = *pSurfPix++;

*pDIBPix++ = *pSurfPix++;

} } break;

default:

// Мы не должны сюда попасть

break;

} pDIBLine -= iDIBScan;

pSurfLine += iSurfScan;

}

// Разблокировать буфер

m hr = m pBackBuffer-»Get!nterface()

»Unlock(ds.lpSurface) ;

/b> ill5' Глава 11. Клипы

// Проверить, возвращен ли информационный // заголовок растра if (ppBMI) {

*ppBMI = pBMI;

} else {

delete pBMI;

}

// Проверить, возвращен ли указатель //на графические данные растра if (ppBits) *ppBit3- = pBits;

// Вернуть логический номер растра return hBmp;

}

Если объем кода начисто отбил у вас охоту снимать клипы, вспомните о том, что функция уже написана! Вам остается только вызвать ее. Подробное описание ее работы выходит за рамки главы и даже всей книги в целом. Я написал эту функцию, руководствуясь документацией по непосредственному режиму Direct3D и некоторыми примерами, входящими в комплект DirectX 2 SDK. 0 непосредственном режиме подробнее рассказывается в главе 13.

Допустим, нам удалось сохранить текущий вид макета. Что происходит дальше? Мы создаем объект CMovieFrame для хранения этого изображения и включаем кадр в список кадров объекта CMovieTake.

Просмотр клипа

Просмотреть записанную последовательность кадров относительно просто. Если вы знакомились с исходным текстом функции Grablmage, то эта задача может показаться вам тривиальной. Тем не менее существуют детали, которые заметно влияют на скорость работы. Перед тем как воспроизводить набор кадров, мы вызываем функцию CMovie::Stop, чтобы остановить процесс записи. Возможно, это покажется вам несущественным, однако в данной функции имеется одна исключительно важная строка (удастся ли вам найти ее?):

void CMovie::Stop()

(

if (m Displaying) {

m bIsPlaying = FALSE;


// Заново отобразить цвета палитры

m_Take.Optimize() ;

} if (m_b!sRecording) (

m_bIsRecording = FALSE;

}

Покадровая съемка <:^^; 261

Что же делает функция Optimize? Она создает идентичную палитру на основе сохраненной и отображает в ней цвета, используемые в наших кадрах. По крайней мере, так должно быть. На практике выяснилось, что мы можем немного смошенничать и заметно ускорить дело, пропустив этап отображения цветов. Другими словами, мы оптимизируем процесс воспроизведения, создавая новую (идентичную) палитру, но не отображая цвета изображения в созданной палитре. Но как же можно добиться правильного вывода цветов на экран, если палитра, в которой воспроизводятся изображения, отличается от той, в которой они были созданы?

Дело в том, что идентичная палитра очень цохожа на исходную, а в некоторых случаях они будут совпадать. Единственные цвета палитры, которые могут измениться, — 20 зарезервированных системных цветов (10 в начале и 10 в конце палитры). Эти цвета не используются механизмом визуализации, а их изменение не влияет на вид изображения. В любом случае, мелкие детали вряд ли заметно отразятся на качестве изображения. Возможно, мое объяснение вас не убеждает;

меня это вовсе не удивляет. Более подробные разъяснения затронутой деликатной темы можно найти в книге «Animation Techniques in Win32». Я предлагаю вам самостоятельно просмотреть код функции Optimize на досуге.

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

BOOL CMovie::Update() {

if (m_bIsPlaying) {

// Вывести следующий кадр if (m_Take.IsEmptyO) {

m_bIsPlaying = FALSE;

return FALSE;


}

// Найти текущий кадр POSITION pos = m_Take.FindIndex(m_iCurFrame);

if (!pos) {

m_bIsPlaying = FALSE;

return FALSE;

}

CMovieFrame* pFrame = m_Take.GetAt(pos);

ASSERT(pFrame) ;

// Получить текущий контекст устройства (DC) ASSERT(m_p3dWnd) ;

CDC* pdc = m_p3dWnd-»GetDC() ;

// Задать палитру 262 ЩЦ1 Глава 11. Клипы

CPalette* pOldPal ° NULL;

CPalette* pPal = m_Take.GetPalette();

if (pPal) {

pOldPal = pdc-»SelectPalette(pPal, FALSE);

pdc-»RealizePalette () ;

}

// Создать совместимый DC для растра CDC dcMem;

dcMem.CreateCompatibleDC(pdc);

// Выбрать палитру в совместимом DC CPalette* pOldMemPal = NULL;

if (pPal) {

pOldMemPal = dcMem.SelectPalette(pPal, FALSE);

dcMem.RealizePalette() ;

}

// Выбрать растр в совместимом DC

HBITMAP hOldBmp = (HBITMAP) ::SelectObject(dcMem, pFrame-»m hBmp) ;

// Скопировать растр в окно pdc-»BitBlt (0, О,

m_Take.GetWidth(), m_Take.GetHeight(),

SidcMem,

0, 0,

SRCCOPY) ;

// Восстановить прежнее состояние DC if (pOldMemPal) {

dcMem.SelectPalette(pOldMemPal, FALSE) ;

} ::SelectObject(dcMem, hOldBmp);

if (pOldPal) (

pdc-»SelectPalette (pOldPal, FALSE) ;

} m_p3dWnd-»ReleaseDC(pdc) ;

m_iCurFrame++;

return TRUE; // Остальные кадры

}

Покадровая съемка ''''Щ 263

Последовательность действий в приведенном выше фрагменте такова:

1. Найти следующий выводимый кадр.

2. Получить контекст устройства (DC) для окна.

3. Выбрать палитру в DC окна.

4. Создать в памяти совместимый DC.

5. Выбрать палитру в совместимом DC.

6. Выбрать логический номер растра в объекте CMovieFrame и поместить его в совместимый DC.

7. Вызвать функцию BitBIt для копирования изображения из совместимого в оконный DC.

8. Восстановить состояние DC и освободить их.

Ветераны программирования для Windows немедленно узнают в этом чудовищном списке необходимую последовательность действий для вывода растра в окне.

На первый взгляд может показаться, что многократный выбор и реализация палитры должны быть чрезвычайно медленными. Однако Windows знает, что при выполнении этих действий с одной и той же палитрой делать ничего не нужно, поэтому все издержки связаны только с вызовом BitBIt.


Наблюдательный читатель может спросить, почему мы пользуемся BitBIt — ведь в нашем распоряжении имеются функции DirectDraw, и вообще мы работаем с поверхностью DirectDraw. Разумеется, мы можем повернуть вспять процесс сохранения кадра и снова отправить изображение на поверхность DirectDraw с тем же результатом. Подобные вещи обычно называются «упражнениями для самостоятельной работы». Почему? Взгляните на огромный объем кода для сохранения кадра на стр. 256 и представьте себе, что вам нужно сделать то же самое в противоположном направлении. Теперь вы понимаете, почему этим придется заниматься вам, а не мне?! На самом деле конкретный способ выполнения данной задачи не имеет особого значения, и я выбрал то, что кажется мне привычным. Разумеется, вы можете попробовать сделать это по-другому.

Стоит ли игра свеч?

Подведем итог всему, что мы узнали к настоящему моменту: программа получилась довольно большой, и к тому же она использует много памяти. Это — нехороший признак. Если учесть, что свободной памяти обычно не хватает для хранения всего клипа (без выгрузки в файл), в большинстве случаев просмотр будет замедляться из-за работы с диском. Таким образом, методика покадровой съемки вовсе не идеальна. Однако мы можем сделать на этом пути еще один шаг, который стоит рассмотреть подробнее.

Создание AVI-файла

Мы можем взять список кадров и создать по нему AVI-файл, который может быть воспроизведен компонентами Microsoft Video for Windows практически на любой Windows-машине, независимо от того, установлен ли на ней DirectSD или нет. Если на вашем компьютере имеется устройство для сжатия/восстановления видеоинформации (CODEC), данные можно сжать (коэффициент сжатия зависит от CODEC). Клип, сжатый по стандарту MPEG (Motion Picture Expert Group),

/b> ДИГ Глава 11. Клипы

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


И снова я не стану подробно рассматривать представленный фрагмент, поскольку на это уйдет слишком много времени. Лучшая документация по Video for Windows содержится в Microsoft Development Library. Функция CMovie::Save, создающая AVI-файл по списку кадров, выглядит следующим образом:

BOOL CMovie::Save()

{

if (GetNumFrames() «= 0) return FALSE;

// Получить имя файла ASSERT(m_p3dWnd) ;

CFileDialog dig(FALSE,

"avi",

NULL,

OFN_OVERWRITEPROMPT,

"AVI Files (*.avi)¦*.avi¦¦",

m_p3dWnd) ;

if (dIg.DoModal() != IDOK) return FALSE;

// Открыть AVI-файл HRESULT hr;

PAVIFILE pfile = NULL;

hr = ::AVIFileOpen(Spfile,

dig.GetFileName(), OF_CREATE ¦ OF_WRITE, NULL) ;

if (FAILED(hr)) return FALSE;

// Создать видеопоток в файле PAVISTREAM pstream = NULL;

AVISTREAMINFO si;

memset(&si, 0, sizeof(si));

si.fccType = streamtypeVIDEO;

si.fccHandler = mmioFOURCC('M','S','V,'C');

si.dwRate = 100; // Fps si.dwScale = 1;

si.dwLength = 0;

si.dwQuality = (DWORD) -1;

si.rcFrame.top = 0;

si.rcFrame.left = 0;

si.rcFrame.bottom = m Take.GetHeight() ;

si.rcFrame.right = m_Take.GetWidth() ;

strcpy(si.szName, "3dPlus Movie");

hr = ::AVIFileCreateStream(pfile, Spstream, &si);

ASSERT(SUCCEEDED(hr));

// Задать формат CMovieFrame* pFrame = m_Take.GetHead();

ASSERT(pFrame) ;

int iSize » sizeof(BITMAPINFOHEADER)

+

DIBColorEntries((BITMAPINFOHEADER*)(pFraine-

»m_pBMI))

* sizeof(RGBQUAD) ;

hr === : :AVIStreamSetFormat(pstream,

0,

pFrame-»m_pBMI,

iSize) ;

ASSERT(SUCCEEDED(hr));

// Записать кадры POSITION pos = m_Take.GetHeadPosition();

int iSample = 0;

while (pos) (

CMovieFrame* pFrame = m_Take.GetNext(pos);

BITMAPINFOHEADER* pBIH = (BITMAPINFOHEADER*)

& (pFrame-»m_pBMI-»bmiHeader) ;

int iBits = DIBStorageWidth(pBIH) * pBIH-»biHeight;

hr = ::AVIStreamWrite(pstream,

iSample, // Текущий кадр

1, // Всего один

pFrame-»m_pBits, // Графические данные

iBits, // Размер буфера

О,

NULL,

NULL) ;

ASSERT(SUCCEEDED(hr)) ;

iSample++;


}

// Освободить поток ::AVIStreamRelease(pstream);

// Освободить файл AVI : .-AVIFileRelease (pfile) ;

return TRUE;

}

В двух словах происходит следующее: мы создаем новый AVI-фаил и видеопоток в этом файле. Затем тип видеопогока задается с помощью структуры BITMAPINFOHEADER и кадры записываются в поток, после чего поток и файл закрываются. Файл можно просмотреть программой Windows 95 Media Viewer или ее аналогом.

9АА S^S^f Гпапа ЛЛ l^nurll.1

Запись отдельного кадра

Иногда требуется записать лишь один кадр. Зачем? Например, вы провели много времени за созданием великолепного трехмерного макета и хотите сохранить его внешний вид. Приложение Save на прилагаемом диске CD-ROM использует функцию сохранения изображения, приведенную на стр. 252, для записи отдельного кадра в формате DIB для Windows. Предлагаю самостоятельно рассмотреть код приложения Save, если оно вас интересует.

Запись данных объекта

Итак, покадровая запись клипа связана как с большим объемом кода, так и с расходом памяти, а скорость просмотра ограничивается параметрами обмена данными с жестким диском (сетью, дисководом CD-ROM и т. д.). Кроме того, записанные клипы неинтерактивны, что ограничивает их применение во многих приложениях. Если нам захочется исследовать трехмерные объекты в окне, стоит поискать другую методику.

В главе 6 мы уже создавали подвижные объекты, но их перемещение было ограничено круговой траекторией. Тогда я упомянул, что траектория вовсе не обязана быть круговой и что ей можно придать любую желаемую форму. Сейчас мы рассмотрим другой способ задания траектории, при котором запоминаются положение и ориентация объекта в нескольких точках траектории. Затем перемещение воспроизводится интерполяцией по записанным точкам, позволяющей построить гладкую траекторию. Чтобы выдержать единую терминологию с документацией по DirecQD, я буду называть такую методику созданием анимации. Попробовав ее на практике, вы убедитесь, что благодаря высокой производительности механизма визуализации она практически полностью вытесняет покадровую съемку.


Кватернионы

Для задания поворотов в анимационных последовательностях механизм визуализации Direct3D использует кватернионы. Хочу немедленно оправдаться перед читателями и заявить, что перед созданием приложения Movie для этой главы я понятия не имел о том, что же такое кватернион. После знакомства с многочисленными справочниками могу сказать, что кватернион — хитроумная математическая конструкция, которая проделывает всякие интересные штуки по очень малым исходным данным. Объяснить ее работу простым английским языком совершенно невозможно, хотя это не совсем справедливо, поскольку изобретатель кватернионов, В. Р. Гамильтон (W. R. Hamilton), довольно много написал о них в 1843 году для Ирландской Королевской Академии. Однако специально для читателя я все же приведу более строгое определение. Кватернионом называется математический инструмент для описания поворотов объекта в пространстве с использованием минимального количества переменных. В нашем трехмерном мире кватернион определяется всего четырьмя переменными. Итоговый поворот объекта может быть описан с помощью кватерниона, являющегося произведением всех кватернионов, описывающих отдельные повороты объекта. Короче говоря, кватернионы позволяют с высокой эффективностью описывать повороты.

Я довольно долго пытался добиться от класса C3dQuaternion нужного поведения. Тем не менее мне так и не удалось привести его в рабочее состояние, а сроки

Запись ляиных г»(тьйитя ''^Ш 2&7

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

Существует еще одна причина, по которой я отказался от использования кватернионов. Она не имеет никакого отношения к сложностям конструирования: на практике случается, что применение кватернионов для управления перемещениями объектов приводит к нежелательным побочным эффектам. Кватернион описывает общий результат поворота, но не то, как именно вы пришли к этому результату. Следовательно, если вы станете определять промежуточные положения объекта, производя интерполяцию по кватернионам, ваш объект может «неестественно» вести себя при перемещении из одного положения в другое. Проблема решается добавлением новых контрольных точек, однако их появление сводит на нет все преимущества компактности кватернионов.


Запись траектории объекта

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

class C3dAnimKey : public C3d0bject

(

public:

C3dAnimKey(double time,

const D3DVECTOR& pos, const D3DVECTOR& dir, const D3DVECTOR& up) : m_vPos(pos), m_vDir(dir), m vUp(up), m_dTime(time)

t

}

public:

C3dVector m vPos;

C3dVector m vDir;

C3dVector m_vUp;

double m_dTime;

};

Из листинга видно, что объект класса C3dAnimKey представляет собой структуру, которая инициализируется в несложном конструкторе. Анимация строится на основе класса MFC CObjectList и состоит из списка объектов C3dAnimKey, указателя на фрейм перемещаемой фигуры и переменной, содержащей текущее время перемещения по траектории. Единицы времени выбираются произвольно. Мы запоминаем состояние объекта в отдельных точках в заданный момент времени. Точки заносятся в список в хронологическом порядке, а воспроизводящая

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

ПРИМЕЧАНИЕ

На момент написания книги в анимациях DirectSD поддерживалась только линейная интерполяция. К тому времени, когда книга окажется у читателя, в Direct3D появится сплайновая интерполяция, благодаря которой кватернионы станут приносить больше пользы (разумеется, вы можете'написать свой собственный код для работы со сплайнами).

Запись происходит следующим образом: мы очищаем список, размещаем объект в макете и добавляем ключевую точку для каждого состояния объекта, включаемого в анимацию. Во время воспроизведения объект перемещается вдоль своей траектории с приращением в 0,1 единицы времени. Для простоты запись состояния объекта осуществляется с временными интервалами в 1,0 единицы, так что в процессе записи нельзя с полной уверенностью предсказать поведение объекта при воспроизведении. В приложении Movie имеется команда Edit ] Demonstration, которая записывает движение объекта и затем воспроизводит его в неограниченном цикле.


Чтобы упростить код приложения, я включил в него глобальный объект C3dAnimation, который позволяет записывать состояние только одного объекта.

Программная реализация настолько проста, что ее даже не стоит приводить здесь, однако для полноты картины мы все же кратко рассмотрим ключевые функции класса C3dAnimation. Давайте для начала посмотрим, как новая ключевая точка заносится в список. Предполагается, что фигура для анимации была выбрана ранее, при вызове функции C3dAnimation::Attach:

BOOL C3dAnimation::AddKey(double time) {

if (!m_pFrame) return FALSE;

// Определить положение и ориентацию фрейма C3dVector p, d, u;

m pFrame-»GetPosition (p) ;

m_pFrame-»GetDirection (d, u) ;

// Создать новый ключ C3dAnimKey* pKey = new C3dAnimKey(time, p, d, u);

// Внести ключ в список return AddKey(pKey);

}

Функция определяет текущее положение и ориентацию фрейма и создает по ним новый объект C3dAnimKey. Затем ключ вносится в список:

BOOL C3dAnimation::AddKey(C3dAnimKey* pNewKey) {

if (!pNewKey) return FALSE;

.'^япиг^ nauu^iy itfvi-ei/Ta ^IRK: ^AO

// Обновить текущее время m dCurTime = pNewKey-»m_dTime;

// Внести ключ в список if (IsEmptyO) {

AddTail(pNewKey) ;

return TRUE;

}

// Перебрать элементы списка в обратном направлении

POSITION роз = GetTailPositionO ;

ASSERT(pos) ;

do {

POSITION thispos = pos;

C3dAnimKey* pKey = (C3dAnimKey*) GetPrev(pos);

if (pKey-»m dTime «= pNewKey-»m_dTime) { // Вставить новый ключ после текущего InsertAfter(thispos, pNewKey);

return TRUE;

) } while (pos);

// Внести новый ключ в начало списка AddHead(pNewKey) ;

return TRUE;

}

Примечание

Если вы никогда не пользовались классом MFC СОЫ-ist, то можете растеряться при виде функций GetNext (см. выше) и GetPrev (см. ниже). Эти функции извлекают данные по ключу, после чего увеличивают или уменьшают значение ключевой переменной. Я ненавижу их, поскольку их использование противоречит здравому смыслу, однако в целом класс СОЫ-ist достаточно удобен — во всяком случае, до тех пор, пока я не разберусь с шаблонами и стандартными библиотеками C++ и не разработаю более удачное решение.


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

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

/b> Глава 11. Клипы

BOOL CMainFrame::Update() {

if (m bPlayAnimation) {

double 1 = m_Anim.GetLength();

double t = in_Anim.GetCurTime() ;

t += 0.1;

if (t » 1) (

if (m_bLoopAnim) {

m_Anim.SetTime(0.0) ;

} else {

m_bPlayAnimation = FALSE;

Status("End of animation");

} } else (

m Anim.SetTime (t);

}

i

Счетчик времени увеличивается с интервалом в 0,1 единицы. В конце анимация останавливается или запускается снова, в зависимости от состояния флага m_bLoopAnim. Ниже приведена функция C3dAnimationTime::SetTime, задающая состояние объекта для нужного момента времени:

BOOL C3dAnimation::SetTime(double time) (

m_dCurTime = time;

if (!m_pFrame) return FALSE;

if (IsEmptyO) return FALSE;

// Перебрать элементы списка в поисках пары ключей, // между которыми лежит заданная величина // (или точного совпадения) POSITION pos = GetHeadPositionO;

ASSERT(pos) ;

C3dAnimKey* pBefore = (C3dAnimKey*) GetNext(pos) ;

ASSERT(pBefore) ;

if (pBefore-»m_dTime » time) return FALSE;

// Слишком рано, // ключ отсутствует

C3dAnimKey* pAfter = NULL;

while (pos) {

pAfter = (C3dAnimKey*) GetNext(pos) ;

ASSERT(pAfter) ;

if ( (pBefore-»m_dTime «= time) && (pAfter-»m_dTime »= time) ) break;

pBefore = pAfter;

// Вычислить интерполированные значения C3dVector p, d, u;

double dt;

if (pAfter != NULL) {

dt = pAfter-»m dTime — pBefore-»m_dTime;

} else {

dt = pBefore-»m_dTime;

} if ((pAfter == NULL) ¦¦ (dt ==0)) {

p = pBefore-»m_vPos;

d = pBefore-»m_vDir;

u = pBefore-»m_vUp;

} else {

double r = (time — pBefore-»m_dTime) / dt;

p = pBefore-»m_vPos + (pAfter-»m_vPos — pBefore-


»m_vPos) * r;

d =s pBefore-»m_vDir + (pAfter-»m_vDir — pBefore-

»m vDir) * r;

u = pBefore-»m_vUp + (pAfter-»m_vUp — pBefore-

»iri_vUp) * r;

}

// Задать новое положение и направление m pFrame-»SetPosition (p) ;

m_pFrame-»SetDirection (d, u) ;

return TRUE;

}

Мы ищем в списке две точки, в интервале между которыми лежит нужный момент времени, и методом линейной интерполяции определяем по ним положение, направление и верхний вектор объекта. Данный фрагмент наглядно показывает, как благодаря классу C3dVector можно получить легко читаемый код. Только представьте себе, что нам пришлось бы производить вычисления для каждого компонента вектора!

Имена функций AddKey и SetTime были выбраны так, чтобы они напоминали имена функций из анимационного интерфейса Direct3D. Если вам вдруг захочется поэкспериментировать с кватернионами, то в код приложения даже не придется вносить многочисленных изменений.

Упражнения для вас

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

ЧТ> ИИЮ' Гг.-.ОЗ 1-1 1/П..П,-,

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

1. Разработайте для анимаций класс на базе списка и создайте макет с несколькими объектами, движущимися одновременно. Например, постройте модель солнечной системы.

2. Попробуйте закрепить анимационную последовательность непосредственно за объектом класса, производного от CSdShape, чтобы каждый объект содержал свою собственную траекторию. Снова создайте макет для задачи 1. Будет ли такой способ лучше, чем решение задачи I?

3. Модифицируйте функцию C3dAnimation::SetTime так, чтобы вместо линейной интерполяции в ней использовались сплайны.

4. Сравните методики покадровой съемки и точечной анимации. Существуют ли приложения, в которых одна из них обладает несомненными преимуществами?

Наше первое трехмерное

Глава 1
Наше первое трехмерное
приложение
Рисунок. 1-1. Приложение Basic

Наше первое трехмерное

Глава 1. Наше первое трехмерное приложение
Не ахти какое достижение, но вы вскоре убедитесь, что приложение Basic устроено достаточно просто, а это немаловажно. В библиотеках MFC, DirectX и 3dPlus спрятано довольно много кода, однако для построения нашего первого приложения совсем не обязательно знать, как он работает. Вам необходимо лишь чуть-чуть помочь в самом начале. Дальше остается только выбирать функции, которые нужно вызывать для достижения желаемого эффекта. В оставшейся части книги мы рассмотрим структуру библиотеки 3dPlus и разберем код многих программ. Итак, попрощайтесь со своей собакой, сделайте глубокий вдох и прыгайте в море — посмотрим, умеете ли вы плавать.
Построение приложения с самого начала
В мире встречаются две категории программистов. Как мне кажется, соотношение между ними составляет примерно 50:50. Первые берут существующую программу и «рихтуют» ее до тех пор, пока она не станет делать то, что требуется. Представители второй категории предпочитают начинать работу «с пустого места» и делать все собственными руками. Я принадлежу к числу последних. Не люблю пользоваться чьим-нибудь кодом, потому что могу упустить какие-нибудь тонкости в его работе; а значит, дальнейшая отладка превратится в сущий кошмар. На тот случай, если вы не захотите взять готовую программу Basic и повозиться с ней, я опишу процесс ее построения (разумеется, пока вам придется пользоваться моей библиотекой 3dPlus, но к концу книги вы сможете написать свой вариант такой библиотеки). Если вас совершенно не интересует, как создавалось это приложение, пропустите данный раздел. Вы всегда сможете вернуться к нему в случае необходимости.
Для построения приложения Basic следует выполнить следующие действия:
1. С помощью Visual C++ MFC AppWizard (EXE) создайте однодокумент-ное (SDI — Single Document Interface) приложение без поддержки баз данных и OLE. Можете также убрать из него панель инструментов, строку состояния, печать и объемные элементы управления. Получится простейшее приложение для Windows. Мой проект назывался Basic. Я выбрал вариант для работы с MFC в качестве совместно используемой библиотеки DLL (As A Shared DLL), но при желании можно осуществить статическую компоновку.

2. Исключите из проекта файлы для классов вида и документа. В моем случае эти файлы назывались BasicDoc.h, BasicDoc.cpp, BasicView.h и BasicView.cpp. Файлы следует удалить как из проекта, так и из рабочего каталога.

3. Аналогичным образом удалите файлы главного окна (обычно они называются MainFrm.h и MainFrm.cpp). Остаются два файла на C++: Basic.cpp и StdAfe.cpp.

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

5. В файл StdAfx.h добавьте директивы для включения файлов mmsystem.h и d3drmwin.h. Заголовочный файл mmsystem используется в функциях для работы с джойстиком, которые понадобятся нам позднее, а в файле d3drmwin определяются все функции Direct3D.

'l&lfc Построение приложения с самого начала ЩЦ 19

BOOL CBasicApp::Initlnstance() {

// Создать главное окно C3dWnd* pWnd = new C3dWnd;

pWnd->Create("3D Basics",

WS_OVERLAPPEDWINDOW ¦ WS_VISIBLE,

50, 50,

400, 350);

m pMainWnd = pWnd;

pWnd->UpdateWindow() ;

// Создать исходный макет, к которому // будут присоединяться объекты static C3dScene scene;

scene.Create () ;

// Установить интенсивность рассеянного света scene.SetAmbientLight(0.4, 0.4, 0.4);

// Добавить направленный источник света для создания бликов static C3dDirLight dl;

dl.Create(0.8, 0.6, 0.8);

scene.AddChild(Sdl) ;

dl.SetPosition(-2, 2, -5);

dl.SetDirectionfl, -1, 1);

// Создать большую белую сферу static C3dShape shi;

shi.CreateSphere (1) ;

// Присоединить к макету большую белую сферу scene.AddChild(Sshl) ;

// Создать маленькую синюю сферу static C3dShape sh2;

sh2.CreateSphere(0.3) ;

sh2.SetColor(0, 0, 1);

// Присоединить синюю сферу к белой shl.AddChild(&sh2) ;

// Задать положение синей сферы // по отношению к белой sh2.SetPosition (О, О, -2);

// Создать маленькую красную сферу static C3dShape sh3;


sh3.CreateSphere (0.15) ;

sh3.SetColor(l, О, О);

«^ Программа ^Ц 21

// Присоединить красную сферу к белой shl.AddChild(&sh3) ;

// Задать положение красной сферы // по отношению к белой sh3.SetPosition(0, 0, 5) ;

// Начать медленно вращать белую сферу // вокруг оси 1, 1, О shl.SetRotation(l, I, 0, 0.015);

// Присоединить весь макет к сцене pWnd->SetScene(Sscene) ;

return TRUE;

BOOL CBasicApp::OnIdle(LONG ICount) {

BOOL bMore = CWinApp::Onldle(ICount);

if (m pMainWnd) (

CSdWnd* pWnd = (CSdWnd*) m_pMainWnd;

// Приказать трехмерному окну сместить макет

//на одну единицу

//и перерисовать окно

if (pWnd->Update(l)) ( ЬМоге = TRUE;

return bMore;

} ) return bMore;

Функция CBasicApp::lnitlnstance создает окно и те объекты, которые образуют макет, а функция CBasicApp::Onldle обновляет положение этих объектов во время пассивной (idle) работы приложения. Класс CBasicApp был сгенерирован AppWizard во время создания приложения. Он является производным от класса CWinApp, входящего в MFC и обеспечивающего основную функциональность Windows-приложения. Давайте подробно рассмотрим, что же делают эти две функции.

Первое, что происходит в Initlnstance, — построение окна, в котором будет отображаться трехмерный макет. Окно класса CWinApp создается в качестве главного окна приложения. Класс C3dWnd принадлежит библиотеке 3dPlus, как и все остальные рассматриваемые нами классы, имена которых начинаются с префикса C3d (исходный текст библиотеки 3dPlus находится на прилагаемом диске CD-ROM вместе с другими примерами). Указатель на созданное окно присваивается переменной m_pMainWnd, являющейся членом базового класса CWinApp из MFC. Указатель на окно используется кодом MFC при обработке сообщений приложением и т. д. Завершающим шагом в создании окна является вызов функции UpdateWindow для прорисовки окна на экране.

22 Глава 1. Каше первое трехмерное приложение

Затем создается объект класса CSdScene. В него входят все элементы одной сцены, отображаемой в трехмерном окне (например, источники света, трехмерные объекты и т. д.).


Следующим шагом является задание источников света. Мы пользуемся двумя различными источниками света: рассеянным и направленным. Рассеянный свет равномерно освещает все объекты. Если не пользоваться другим освещением, при отсутствии бликов объекты выглядят плоскими. Направленный свет действует по принципу прожектора. Сам по себе он дает очень резкий контраст и не позволяет рассмотреть затемненные части объектов. Когда объект освещается направленным источником света, интенсивность цвета поверхности меняется так, чтобы имитировать яркий луч, падающий из одного направления. Используя оба источника света, мы получаем приличную имитацию объемности и можем рассмотреть все участки объектов, даже если они находятся в тени по отношению к направленному свету. В классе C3dScene имеется встроенный источник рассеянного света, так что нам остается лишь задать его интенсивность. Сцену (stage) можно упрощенно рассматривать как окно приложения, в котором воспроизводится текущий макет. Интенсивность рассеянного света задается следующим образом:

scene.SetAmbientLight(O.4, 0.4, 0.4);

Свет состоит из различных оттенков красного, зеленого и синего цветов. В данном случае интенсивности красной, зеленой и синей составляющих выбираются так, чтобы образованный ими свет был белым и не искажал цветов объектов. Интенсивность цветовых составляющих может меняться от нуля до единицы (от 0,0 до 1,0). В программировании для Windows обычно применяются целые значения цветовых составляющих, лежащие в диапазоне от 0 до 255. Использование значений с плавающей точкой (double) может показаться нерациональным, однако следует помнить, что данные значения используются для математического определения цветов граней объектов, входящих в макет. Для этого приходится производить многочисленные тригонометрические и иные вычисления, так что на самом деле использование значений с плавающей точкой выглядит вполне оправданным. Конечно же, страстные поклонники C++ могут самостоятельно определить класс для цвета и переделать некоторые функции библиотеки 3dPlus, чтобы они в качестве аргумента принимали цветовой объект вместо набора отдельных составляющих. Я не стал этого делать лишь для того, чтобы в некоторых важных случаях можно было сразу увидеть значения RGB-компонент.


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

ПРИМЕЧАНИЕ

Я расположил направленный источник света наверху слева, потому что такое направление используется в объемных элементах управления (кнопках) Windows.

Программа

нет сообщений, подлежащих обработке, и при этом процессор не занят другими работающими приложениями (что на самом деле происходит большую часть времени). Мы используем период пассивности для того, чтобы переместить макет к следующему положению и перерисовать его в окне. Все это происходит при вызове функции C3dWnd::Update (pWnd->Update(1)). Аргумент функции C3dWnd::Update определяет, насколько необходимо сместить макет. Этот аргумент будет использован нами позднее, чтобы обеспечить постоянную скорость перемещения макета даже при изменяющемся периоде пассивности. А пока мы пользуемся принятым по умолчанию значением 1, чтобы макет перемещался на одну (выбираемую достаточно произвольно) единицу.

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

Рискуя повториться, я все же напомню: не стоит волноваться, даже если не все было понятно с первого раза. В дальнейшем мы еще не раз вернемся к тексту этой программы. А пока давайте рассмотрим некоторые важные моменты.

Быстродействие, тени, фреймы и координаты


Если запустить приложение Basic на приличной машине с хорошей видеокартой, выполняющей аппаратную пересылку битовых блоков (бит-блит), на вас наверняка произведет впечатление скорость его работы. Под «приличной» машиной я имею в виду как минимум компьютер 486 от 50 МГц и выше, а лучше — более современный Pentium с PCI-видеокартой. Именно видеокарта оказывается наиболее важным элементом. Если ваша видеокарта обладает «лишней» видеопамятью (то есть объем видеопамяти превышает тот, который необходим для текущего разрешения экрана) и аппаратными средствами для перемещения блоков видеопамяти, то библиотеки DirectX могут извлечь выгоду из такого положения вещей. При этом достигается заметное повышение скорости по сравнению со старыми видеокартами, в которых для перемещения данных в видеопамяти используется системный процессор. В сущности, именно поддержка аппаратных особенностей видеокарт последнего поколения и составляет одну из самых сильных сторон интерфейса Direct3D — благодаря этому вы добиваетесь от своего компьютера максимальной производительности.

Однако вернемся к нашему примеру. Каждая сфера состоит из 256 граней, которые необходимо нарисовать на экране. Для каждой грани требуется вычислить положение трех или четырех точек. Цвет грани должен изменяться в зависимости от ее положения и суммарного воздействия всех источников света, входящих в макет. Все это сопряжено с немалым объемом вычислений, и плавное движение, которое вы видите на экране, обусловлено тем, что эти вычисления происходят с достаточной скоростью. Библиотека Direct3D — впечатляющий набор программных модулей, оптимизированных с расчетом на максимальную производительность.

Быстродействие, тени, фреймы и координаты

ПРИМЕЧАНИЕ

Все трехмерные объекты, с которыми нам придется иметь дело, составлены из плоских многоугольников — чаще всего из треугольников. Эти многоугольники являются гранями объекта. А почему они должны быть непременно плоскими? Только для того, чтобы компьютеру было проще и быстрее рисовать их. Некоторые механизмы визуализации могут работать и с криволинейными гранями, однако используемая нами система не входит в их число. Мы научимся создавать объекты в главе 4.


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

Механизм визуализации Direct3D не осуществляет трассировки лучей, поэтому он не умеет генерировать теней и отражений. Как следствие, мы получаем значительное увеличение быстродействия. Если вы не заметили отсутствия теней до того момента, когда я вам об этом сказал, то согласитесь с тем, что впечатляющие трехмерные анимации могут обходиться и без теней с отражениями. Как мы узнаем позднее, на самом деле можно генерировать тени и даже имитировать отражения при помощи методики, которая называется хромовым покрытием (chrome wrap), рассмотренной в главе 8, — так что не огорчайтесь и продолжайте читать дальше.

Как видно из текста программы на стр. 22, для вращения большой белой сферы в нашем макете применен вызов функции SetRotation. При запуске приложения можно убедиться, что белая сфера вращается — затенение выполнено настолько качественно, что это даже не сразу видно. Тем не менее можно сразу же заметить, что две меньшие сферы вращаются вокруг большей. Но где же программный код, вызвавший их вращение? Секрет заключается в том, как они были присоединены к макету. Обратите внимание на то, что большая сфера присоединялась непосредственно к макету:

scene.AddChild(&shl) ;

тогда как меньшие сферы присоединялись к большой сфере:

scene.AddChild(&sh2) ;

scene.AddChild(&sh3) ;

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


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

26 Глава 1. Наше первое трехмерное приложение

их обсчетом. К фрейму могут присоединяться и другие фреймы-потомки; соответствующие им преобразования выполняются после родительских. В результате фреймы-потомки перемещаются вместе с родительским фреймом и также могут обладать самостоятельным движением по отношению к родителю. Чтобы лучше понять это, представьте, что вы расхаживаете по офису внутри большого здания. Ваш фрейм — это офис, в котором вы находитесь. Родительским фреймом офиса может быть целый этаж, а родительским фреймом этажа — все здание, в котором вы находитесь. Хотя вы замечаете только свое движение по офису, вы также перемещаетесь относительно этажа и всего здания. Эта тема будет подробно рассмотрена в главе 5, где мы займемся преобразованиями, и в главе 6, где показано, как происходит движение объектов.

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

Для чего же все это нужно? Существует множество эффектов, которые чрезвычайно просто реализуются при помощи иерархии объектов/фреймов. Давайте рассмотрим космический тяжелый танк Mark VII, в котором, как известно, для наведения пушек используется допплеровский радар, работающий в Х-диапазоне. Если бы нам понадобилось смоделировать этот танк в нашем трехмерном приложении, мы бы создали геометрическое тело, изображающее радар, присоединили его к башне танка и заставили вращаться вокруг своей оси. Если описывать ситуацию в терминах фреймов, то фрейм радара становится потомком по отношению к фрейму башни. После этого можно сосредоточиться на перемещении тапка и забыть про радар — он всегда будет находиться в правильном положении и вращаться. На Рисунок 1-3 изображен танк Mark VII в действии.


Рисунок* 1-3. Космический тяжелый танк Mark VII с допплеровским радаром

Наше первое трехмерное

'^fi^sfe 07

Быстродействие, тени, фреймы и координаты ^И •-

Последнее, о чем мне хотелось бы упомянуть в этой главе, — это система координат. Поскольку мы работаем с тремя измерениями, положение каждой точки представляется тремя координатами. У нас имеются три оси — х, у и z, организованные в так называемую левостороннюю систему координат. Давайте проведем небольшой эксперимент (если вы читаете эту книгу, лежа в кровати, то предупредите свою лучшую половину о том, что вы сейчас начнете делать странные жесты руками — иначе происходящее может быть воспринято как намек). Вытяните левую руку перед собой и выпрямите пальцы; ладонь обращена вправо, а большой палец находится сверху. Поднимите большой палец, затем подогните мизинец и безымянный палец к ладони и отведите средний палец вправо. Ваша рука должна выглядеть примерно так, как показано на Рисунок 1-4; большой палец изображает ось у, указательный — ось z, а средний — ось х.

Левая рука с левосторонней системой координат

Наше первое трехмерное

В левосторонней системе координат ось у направлена вверх, ось х — вправо, а ось z — в глубь экрана (от пользователя). Разумеется, название обусловлено вовсе не тем, что вы можете превратить свою левую кисть в какую-то странную фигуру. Если взять винт с левой нарезкой и вращать его от оси х к оси у, он начнет двигаться по направлению оси z. Для того чтобы это правило работало в левосторонней системе координат, понадобится винт именно с левой нарезкой.

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


Глава 1. Наше первое трехмерное приложение

Координаты точки в трехмерном пространстве могут передаваться в программные модули DirectX одним из двух способов. Иногда координаты передаются в виде тройки значений типа double, соответствующих координатам х, у и z, а иногда — в виде структуры D3DVECTOR, членами которой являются те же самые координаты. В любом случае координаты представляются значениями с плавающей точкой. Выбор масштаба оставляется исключительно на усмотрение пользователя, но я решил установить камеру и другие параметры сцены так, чтобы единичный куб, помещенный в точку 0,0,0 сцены, имел нормальные размеры. Позднее мы снова вернемся к координатам и всему, что с ними связано.

ПРИМЕЧАНИЕ

Библиотека 3dPlus содержит класс C3dVector, являющийся производным от класса D3DVECTOR. Всюду, где в качестве аргумента функции выступает тип D3DVECTOR, вместо него можно использовать объект C3dVector. Я создал класс C3dVector, поскольку класс C++ в программе приносит больше пользы, чем простая структура. Кроме того, вы можете обратить внимание на то, что функции Direct3D получают аргументы типа float, а не double. Я использовал значения типа double в своем коде, потому что они дают большую точность, легче преобразуются и используются во всех математических runtime-библиотеках С.

Вы еще не передумали?

Вероятно, вам уже надоело читать и размахивать руками. Вы бы предпочли запустить Visual C++, скопировать проект Basic с диска CD-ROM (или запустить программу Setup) и поэкспсриментировать с приложением, заставив его делать что-нибудь другое. Можно попробовать изменить цвета фигур, цвет-освещения, параметры вращения и даже добавить к макету несколько новых фигур — например, кубов, конусов или цилиндров. Класс C3dShape содержит функции для создания всех этих простейших геометрических тел. Но перед тем, как браться за дело, стоит поближе познакомиться с примерами и необходимой настройкой среды разработки.

Перед тем как компилировать программу-пример, необходимо правильно настроить среду разработки. Это делается следующим образом:


1. Запустите программу Setup для DirectX 2 SDK и установите средства разработки, входящие в DirectX 2 SDK. При этом на ваш жесткий диск будут перенесены включаемые файлы, библиотеки DirectX 2 SDK и т. д. Кроме того, будут установлены runtime-библиотеки DirectX 2 SDK, если это не было сделано ранее.

2. Запустите Visual C++ и выполните команду Tools ¦ Options; выберите вкладк\ Directories в окне диалога Options.

3. Добавьте путь к включаемым файлам DirectX 2 SDK в список Include Files, a путь к библиотекам DirectX 2 SDK — в список Library Files. Если вы забудете сделать это, то получите сообщения об ошибках на стадии компиляции или компоновки.

Вы еще не передумали?

ПРИМЕЧАНИЕ

В заголовочных файлах Direct3D содержатся ссылки на два файла— subwtype.h и d3dcom.h, которые не используются при построении Windows-приложений и соответственно не входят в SDK. К сожалению, при проверке взаимосвязей Visual C++ обнаруживает, что эти файлы могут понадобиться, и жалуется на их отсутствие. В качестве решения проблемы я создал два фиктивных файла: subwtype.h и d3dcom.h. Они находятся во включаемом каталоге библиотеки 3dPlus. В этих файлах нет ничего, кроме краткого комментария.

Во всех примерах используется библиотека 3dPlus, поэтому перед построением приложений-примеров вы должны скопировать на жесткий диск по меньшей мере ее включаемые файлы и библиотеки. Проще всего скопировать все дерево каталогов с примерами 3dPlus. В этом случае вы сможете перекомпилировать приложение перед тем, как запускать его — это позволит убедиться, что все необходимые файлы находятся на месте. Если же вы воспользуетесь программой Setup с прилагаемого диска, то вам даже не понадобится вручную копировать каталоги 3dPlus. Тем не менее вам все же придется включить каталог 3dPlus\lndude в список Include Files и каталог 3dPlus\Lib — в список Library Files. При использовании принятых по умолчанию параметров Setup списки со включаемыми и библиотечными файлами должны выглядеть следующим образом:

С \MSDEV\INCLUDE


С \MSDEV\MFC\INCLUDE

С \DXSDK\SDK\INC

С \3D\3DPLUS\IMCLUDE

С \MSDEV\LIB С \MSDEV\MFC\LIB С \DXSDK\SDK\LIB С \3D\3DPLUS\LIB

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

С:\ 3D

3dPlus Include Lib Source Basic Color ... (остальные примеры) Dxsdk (DirectX 2 SDK) sdk

inc

30 Глава 1. Наше первое трехмерное приложение

lib

Msdev (Visual C++)

После того как вы настроите параметры среды и все будет готово к построению проекта, не забудьте выполнить команду Build ¦ Update All Dependencies Visual C++ и убедиться в том, что компилятор находит все заголовочные файлы.

Что же мы узнали в этой главе?

Если вы ответили: «Почти ничего», значит, все мои усилия не произвели на вас особого впечатления. Я надеялся на что-нибудь вроде «Да, мое первое трехмерное приложение действительно оказалось простым» или «Всю жизнь мечтал посмотреть на окно с летающими шариками, и теперь моя мечта сбылась». Впрочем, каждому — свое. Конечно, вы уже готовы задать тысячу вопросов — чем мы будем заниматься теперь, как работают графические библиотеки, как нарисовать слона, как изобразить полет вокруг планеты, сопровождаемый величественной музыкой, как наложить фото президента Никсона на куб и чем вооружен космический танк Mark VII? Все это (и многое другое) будет рассказано в последующих главах. Вернее, почти все — у меня не нашлось портрета Никсона, а танки еще не завезли на склад.

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

Глава 2 Расставляем декорации

Структура приложения

Наше первое трехмерное

Основные принципы архитектуры, выбираемые в начале проекта, нередко оказывают значительное влияние на его развитие. Неудачная структура может привести к тому, что ваш проект станет «обрастать бородавками» быстрее, чем вы будете их удалять. Например, при написании книги «Animation Techniques for Win32» я еще не обладал достаточным опытом программирования на C++ и работы с Microsoft Visual C++ и библиотеками MFC. В начале работы над примерами я совершил то, что сейчас считаю своей грубой ошибкой: воспользовался Visual C++ для построения однодокументного (SDI) приложения и решил, что мне удастся как-нибудь приспособить его для своих целей. Я сделал это лишь потому, что на тот момент приходилось выбирать между однодокументным и многодокументным (MDI) типами приложения, а MDI-приложение явно не подходило для воспроизведения игровой анимации. Сейчас я ясно понимаю, что мог бы существенно упростить все свои примеры, если бы отказался от принятой в Visual C++ метафоры «документ/вид» и воспользовался простым окном с меню.

На этот раз я решил, что структура моих приложений-примеров должна быть проще и ближе к той, которая может понадобиться при разработке компьютерной игры (это вовсе не означает, что она не годится для более серьезных целей — просто я отказался от использования метафоры «документ/вид» для упрощения программы).

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

Структура приложения 'Чр!' 33

Перемещение объектов

Глава 6 Перемещение объектов
в макете
Устройства ввода

Перемещение объектов

По своему опыту могу сказать, что при написании приложений для Microsoft Windows на пользовательский интерфейс обычно уходит больше времени, чем на все основное содержание программы. Думаю, при работе над любым продуктом хотя бы с удовлетворительным пользовательским интерфейсом на последний тратится около 75 процентов всего времени. Честно говоря, в своих приложениях я не вкладываю в него таких усилий (те читатели, которые имели несчастье с ними работать, в этом наверняка убедились).
Пожалуй, тщательнее всего я реализовываю интерфейс приложений, которые я пишу для своих детей. Они (приложения, а не дети) должны быть как можно проще. Например, однажды я написал для своего полуторагодовалого сына Марка простейший графический редактор, в котором пользовательский интерфейс почти полностью отсутствовал. В нем не было ни меню, ни кнопок, ни управляющих клавиш, и при работе с ним можно было даже обойтись без нажатия кнопок мыши. Тем не менее его инструменты позволяли нарисовать вполне нормальную картинку.
На прошлой неделе Марку исполнилось три года, и он начал проявлять интерес к ракетам, и потому я попробовал изобразить ракеты на своем компьютере. Приступая к работе над приложениями для этой книги, я обдумывал множество идей для игр, которые бы понравились Марку. Но вскоре стало очевидно, что клавиатура и мышь не могут обеспечить интерфейс для трехмерного приложения, который подошел бы трехлетнему ребенку. Честно говоря, мои первые эксперименты с перемещением трехмерных объектов не очень обрадовали даже меня самого. Проблема заключалась в том, что я пытался построить интерфейс на основе мыши и нескольких клавиш. Конечно, большинству читателей приходилось играть в трехмерные игры, которые имели вполне нормальный интерфейс на базе мыши и клавиатуры. Однако мой сын решительно не мог удерживать нажатой левую кнопку мыши вместе с клавишей Shift, да еще притопывать правой ногой от нетерпения.
На сцене появляется джойстик. Много лет назад, когда компьютеры приводились в действие энергией пара, а процессор 80286 казался чудом техники, я написал исходный драйвер джойстика для мультимедиа-расширения Microsoft Windows (кстати говоря, довольно сомнительная заслуга). Впрочем, все это было давно, и с тех пор я прочно забыл о существовании джойстиков, пока несколько недель назад не зашел в магазин Microsoft. Именно в этот момент я решил, что мои трехмерные приложения будут работать под управлением джойстика, и купил

/b> ¦¦ у Глава 6. Перемещение объектов в макете

Microsoft SideWinder Pro. Довольно быстро выяснилось, что в трехмерных приложениях джойстик действительно удобнее мыши. SideWinder обладает четырьмя параметрами, генерирующими данные для приложения: координатами Х и Y, угловой и линейной скоростью. С помощью кнопок на боковой поверхности джойстика я мог вращать объект вокруг трех осей и управлять его положением.

Немного позднее мне подарили манипулятор SpaceBall Avenger — весьма эффектное устройство, выпущенное компанией Spacetec IMC Corp. Он обладает шестью степенями свободы, то есть позволяет получить выходные параметры для смещения по осям х, у и z, а также для вращательного движения вокруг осей r, u и v. В конструкции манипулятора использованы датчики давления, которые с высокой чувствительностью реагируют на все толчки и повороты шарика.

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

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

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


Модель устройства ввода

На Рисунок 6- 1 изображена модель устройства ввода* с точки зрения приложения. Когда приложению требуется выполнить цикл обновления, оно вызывает функцию Update объекта, управляющего процессом ввода (контроллера ввода). Контроллер опрашивает устройство ввода и получает обновленную информацию о его аппаратном состоянии. Узнав ее, контроллер изменяет положение и ориентацию фрейма, к которому он присоединен в приложении, а затем уведомляет приложение о причине обновления состояния. Все эти действия обычно совершаются в периоды пассивности приложения. Помимо событий, изображенных на Рисунок 6-1, возможны и другие. В частности, любые сообщения от клавиатуры или мыши, полученные окном, в котором воспроизводится трехмерное изображение, передаются контроллеру и устройству ввода. Это делается для того, чтобы получать входные данные от мыши или клавиатуры без обязательного опроса этих устройств.

• Обратите внимание на то, чти устройством ввода (input device) в данном случае автор называет не физическое устройство (клавиатура, мышь, джойстик), а класс C++, назначением которого является генерация входных данных для программы. — Примеч. персе.

Модель устройства ввода ^Щ' 135

Рисунок. 6-1. Рабочий цикл устройства ввода

Перемещение объектов

Устройство ввода

Назначение устройства ввода состоит в том, чтобы получить данные от аппаратуры и сгенерировать по ним шесть величин: х, у, z, r, u и v. Значения х, у и z представляют собой линейные смещения, а r, u и v — угловые скорости. Оси устройства ввода не связаны с осями макета или конкретного объекта; это не более чем необработанные входные данные, которые используются контроллером для смены положения объекта. На Рисунок 6-2 показаны общие зависимости между осями и угловыми скоростями.

Разумеется, при работе с таким устройством, как SpaceBall, значения х, у, z, г, u и v генерируются просто — достаточно опросить аппаратуру, применить некоторый масштабный множитель и вернуть результат. Для физических устройств, не способных генерировать данные по шести осям, устройство ввода должно получить аппаратные данные и обработать их так, чтобы создать выходные значения для всех шести параметров. Например, устройство ввода из библиотеки 3dPlus, работающее с мышью, получает координаты х и у курсора мыши и в зависимости от состояния клавиш Shift и Ctrl определяет, какие выходные значения следует изменить. Таким образом, если во время перемещения мыши удерживать левую кнопку и не нажимать никаких клавиш, изменяются координаты х и у. При нажатой клавише Shift входное значение координаты х переходит в угловую скорость v, а координата у—в координату z.


/b>

Глава 6. Перемещение объектов в макете

Рисунок. 6-2. Взаимосвязь между значениями r, u, v и х, у, z

Перемещение объектов

Библиотека 3dPlus включает поддержку трех различных устройств ввода: клавиатуры, мыши и джойстика. Каждое устройство реализовано в виде класса C++, производного от C3dlnputDevice.

Устройство ввода с клавиатуры

Устройство ввода с клавиатуры обрабатывает сообщения WM_KEYDOWN, посылаемые ему контроллером. Сообщения клавиатуры используются для увеличения или уменьшения текущих значений параметров x,y,z,r,unv.B табл. 6-1 показано, как различные комбинации клавиш влияют на значения выходных параметров.

Таблица 6-1. Управляющие функции клавиатуры
Клавиша Normal Shift Ctrl
Left arrow X- V- u-
Right arrow X++ V++ U++
Up arrow Y++ Z++ R++
Down arrow Y- z- R-
Знак «+» на цифровой клавиатуре Z-
Знак «-» на цифровой клавиатуре Z++
Page Up V++
Page Down V-
Home U++
End u-
Insert R++
Delete R-
He забывайте, что перед тем, как изменять состояние объекта, все выходные значения должны быть обработаны контроллером, поэтому оси с обозначениями х, у, z, r, u и v вовсе не обязаны соответствовать одноименным осям объекта или макета.

Функция, управляющая работой устройства ввода с клавиатуры, представляет собой оператор switch, в котором обрабатываются сообщения от различных клавиш. Ниже приведена первая часть функции из файла SdlnpDev.cpp каталога Source библиотеки 3dPlus, обрабатывающая нажатия клавиш <— и —>:

void C3dKeyInDev::OnKeyDown(HINT nChar, UINT nRepCnt,

UINT nFlags)

(

double dine = 0.02;

switch (nChar) { case VKJ3HIFT:

in_bShift = TROE;

break;

case VK_CONTROL:

m_bControl = TRUE;

break;

case VK_RIGHT:

if (m_b3hift) { Inc(m_st.dV) ;

} else if (m_bControl) { Inc(m_st.dU) ;

) else (

Inc(m_st.dX) ;

} break;

case VK_LEFT:

if (m_bShift) { Dec(m_st.dV) ;

} else if (m_bControl) { Dec(m_st.dU) ;

) else (

Dec(m_st.dX) ;

} break;

Устройство ввода от мыши


Устройство ввода от мыши выглядит несколько проще. Поскольку мышь обладает только двумя степенями свободы, необходимо определить, каким образом два входных параметра отображаются на шесть выходных осей (табл. 6-2).

Таблица 6-2. Управляющие функции мыши

Входной параметр Normal Shift Ctrl

X X -V -U

Y -Y -Z -R

/b> iiisi^ Глава б. Пепемешение объектов в макете

Обратите внимание на то, что некоторые параметры инвертируются. Я изменил направление осей, чтобы управление стало более логичным. Код устройства ввода от мыши состоит из двух функций: C3dMouselnDev::OnUserEvent и C3dMouselnDev::GetState. Первая функция, исходный текст которой приведен ниже, находится в файле 3dlnpDev.cpp. Данная функция обрабатывает перемещение мыши и захватывает ее указатель (то есть ограничивает его перемещение текущим окном) при нажатии левой кнопки:

void C3dMouseInDev::OnUserEvent(HWND hWnd, UINT uiMsg,

WPARAM wParam, LPARAM IParam) (

switch (uiMsg) { case WM_LBUTTONDOWN:

::SetCapture(hWnd) ;

m_bCaptured = TRUE;

break;

case WM_LBUTTONUP:

if (m_bCaptured) { ::ReleaseCapture () ;

m_bCaptured = FALSE;

} break;

case WM_MOUSEMOVE:

if (m_bCaptured) (

// Внимание: экранные координаты! (см. C3dWnd)

m_ptCur.x = LOWORD(IParam);

m_ptCur.y = HIWORD(IParam);

m_dwFlags = wParam;

} break;

default:

break;

}

\ i

Положение мыши запоминается в m_ptCur, локальной структуре класса CPoint. Вторая функция, исходный текст которой приведен ниже, вызывается, когда контроллер запрашивает текущее состояние устройства ввода:

BOOL C3dMouseInDev::GetState(_3DINPUTSTATE& st) {

if (m_ptPrev.x « 0) { m_ptPrev = m_ptCur;

}

Устоойство ввода '"SH 139

if (m_dwFlags & MK_SHIFT) {

m_st.dz = -d;

} else if (m_dwFlags & MK_CONTROL) {

rri_st.dR = -d;

} else (

m_st.dY = -d;

) )

m_ptPrev = m_ptCur;

st = m_st;

return TRUE;

Функция обрабатывает полученные значения х и у таким образом, чтобы обеспечить небольшую «мертвую зону» для малых смещений и предотвратить случайное перемещение объекта. Затем смещения умножаются на коэффициент пропорциональности, чтобы перемещение объектов всегда происходило в правильном масштабе. Наконец, в зависимости от текущего состояния клавиш Shift и Ctrl функция определяет, какие выходные параметры следует изменить.


Устройство ввода от джойстика

Устройство ввода от джойстика реализуется несколько сложнее, чем ввод от мыши или клавиатуры. На Рисунок 6-3 изображено окно диалога Joystick Settings.

Рисунок. 6-3. Окно диалога Joystick Settings

Перемещение объектов

Значение каждого выходного параметра может определяться по любой из входных осей, а кнопка джойстика может выступать в роли модификатора. Например, из Рисунок 6-3 видно, что значение выходного параметра v определяется значением входного параметра х, но только при нажатой кнопке 4. В столбцах Value изображены текущие значения параметров. Левый столбец показывает те-

Угтплмгтял капля

141

кущее входное значение, полученное от джойстика; темно-серая полоса соответствует «мертвой зоне». Если входное значение лежит внутри «мертвой зоны», выходное значение не изменяется. Наличие «мертвой зоны» позволяет предотвратить мелкие смещения объектов в тех случаях, когда отпущенная рукоять джойстика не возвращается точно к нейтральному положению. Правый столбец Value изображает выходное значение параметра.

Кроме того, вы можете изменить масштабы осей. Увеличение числа в столбце Scale соответствует повышению чувствительности джойстика, причем отрицательные значения меняют направление оси на противоположное. Конфигурация, показанная на рисунке, была выбрана мной для джойстика Microsoft SideWinder. При работе со SpaceBall остается лишь задать коэффициент пропорциональности между параметрами (х отображается на х, у — на у и т. д.). На Рисунок 6-4 изображен типичный график зависимости выходных значений параметров от входных. Плоский участок в центре соответствует «мертвой зоне».

Рисунок* 6-4» Типичный график зависимости вход/выход для джойстика

Перемещение объектов

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


HKEY_CURRENT_USER\Software\3dPlus\<>\Sett.i.i-igs \Joystick\«тип-джойстика»

Большой объем кода для работы с джойстиком не позволяет привести его в книге, поэтому я предлагаю вам просмотреть файл 3dJoyDev.cpp в каталоге Source библиотеки 3dPlus.

Гпаоа R Попоклашаииа /^Дт-ои-г^о о идилата

Контроллер ввода

Задача контроллера ввода заключается в том, чтобы получить от устройства ввода значения параметров х, у, z, r, u и v и определенным образом применить их к объекту CSdFrame. Я создал два различных типа контроллеров ввода: позиционный контроллер (position controller) и контроллер полета (flying controller). Контроллеры обоих типов могут использоваться для манипуляций с объектами макета или с камерой. Контроллеру необходимо указать фрейм, с которым он должен работать, а остальное происходит автоматически. Помимо перемещения объекта, контроллер уведомляет приложение о различных событиях — скажем, об изменении параметра х или о нажатии определенной кнопки, — на которые приложение должно реагировать определенным образом. На Рисунок 6-5 изображено окно диалога, которое вызывается из меню Edit приложения Moving. Здесь можно выбрать разновидность контролируемого объекта, тип контроллера и устройство ввода.

Рисунок. 6-5. Окно диалога Control Device

Перемещение объектов

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

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


Контроллер полета используется для имитации «полета» объекта или камеры внутри макета. Параметры х и у служат для определения углов атаки и крена,

z определяет скорость, а и — угол тонгажа. Идея состоит в том, чтобы привести объект в прямолинейное движение и затем выбирать его траекторию посредством изменения углов атаки, крена и тонгажа. В исходном варианте программы углы крена и атаки умножались на скорость, чтобы имитация получалась более реалистичной. Однако вскоре выяснилось, что пилота из меня не выйдет, поэтому я пошел по более простому пути и допустил изменение ориентации даже для неподвижного объекта. Если вам это покажется нелогичным, попробуйте поработать с текущим вариантом и затем модифицировать его так, чтобы учитывать скорость полета. Что же именно модифицировать, спросите вы? Приведенную ниже функцию, которая находится в файле 3dlnCtlr:

void C3dFlyCtlr::OnUpdate(_3DINPUTSTATE& st,

C3dFrame* pFrame) t

// Определить скорость (по значению параметра z)

double v = st.dZ / 10;

// Получить углы атаки, крена и тонгажа // для осей х, у и и double pitch = st.dY / 3;

double roll = -st.dX / 3;

double yaw = 5t.dU / 5;

// Умножить угол атаки и крена на скорость // для повышения реализма // pitch *= v;

// roll *= v;

pFrame-»AddRotation(l, 0, 0, pitch, D3DRMCOMBINE_BEFORE) ;

pFrame-»AddRotation(0, 0, 1, roll, D3DRMCOMBINE BEFORE);

pPrame-»AddRotation(0, 1, 0, yaw, D3DRMCOMBINE^BEFORE) ;

// Получить вектор текущего направления double xl, yl, zl;

pFrame-»GetDirection (xl, -yl, zl);

// Умножить вектор направления на скорость xl *= v;

yl *= v;

zl *= v;

// Определить текущее положение double х, у, z;

pFrame-»GetPosition (х, у, z);

// Обновить текущее положение х += xl;

У += yl;

z += zl;

/b> ^Р? Глава 6. Перемещение объектов в макете

pFrame-»SetPosition (x, y, z

Функция C3dFlyCtrl::OnUpdate изменяет положение и ориентацию фрейма перемещаемого объекта на основании данных, полученных от устройства ввода. Эта функция вызывается каждый раз, когда требуется обновить положение объекта. Ее аргументами являются описание текущего состояния входного устройства (значения его параметров) и указатель на фрейм, с которым она должна работать. Из всего кода контроллеров и устройств ввода данная функция представляет наибольший интерес, поэтому мы подробно рассмотрим ее.


Начнем со структуры для хранения данных, полученных от джойстика:

typedef struct 3DINPUTSTATE {

double dX // -1«= значение «=1

double dY // -1«= значение «=1

double dZ // -1«= значение «=1

double dR // -1«= значение «=1

double dU // -1«= значение «=1

double dV // -1«= значение «=1

double dpov; // 0 «= значение «=359

// (значения «О являются недопустимыми) DWORD dwButtons;// I = кнопка активна (нажата) ) _3DINPUTSTATE;

Как видно из листинга, значения шести основных параметров лежат в интервале от -1,0 до 1,0. Кроме того, в структуре присутствует член dPov, определяющий направление, в котором вы смотрите, — вперед, влево, вправо и т. д. (на некоторых джойстиках имеется специальная кнопка для выбора направления). Значение dPov представляет собой угол в градусах, измеряемый от направления «вперед».

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

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

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

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

Возможно, вы обратили внимание на то, что в программе используются версии функций GetDirection, GetPosition и SetPosition, в которых значения x, у и z


Устройство ввода

/h2>

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

Последнее замечание: скорость перемещения в нашем случае не является постоянной. Функции обновления вызываются в периоды пассивной работы приложения, а количество времени, которое требуется для перерисовки макета, зависит от взаимного расположения объектов. Если вы захотите добиться постоянной скорости перемещения, придется пойти более сложным путем — например, измерять текущее время функцией timeGetTime (объявленной в файле Mmsystem.h) и определять смещение каждого объекта в зависимости от времени.

Самостоятельно движущиеся объекты

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

Первый пример, находящийся в каталоге Cruise, приводит в движение камеру, чтобы создать иллюзию кругового полета на самолете вокруг горы. На Рисунок 6-6 изображен внешний вид экрана приложения (кроме того, на вкладке имеется цветная иллюстрация).

Рисунок. 6-6. Полет над холмами

Перемещение объектов

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

/h2>

Глава 6. Перемещение объектов в макете

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


BOOL CMainFrame::SetScene() t

// Задать траекторию полета камеры m_vCamera = C3dVector(5, 5, 0);

m_dRadius = 5.0;

// Задать обзорное поле m_pScene-»SetCameraField (1.5) ;

}

Траектория выбирается таким образом, чтобы камера вращалась вокруг заданной точки. На Рисунок 6-7 изображено движение камеры в нашем макете.

Рисунок. 6-7. Траектория полета камеры

Перемещение объектов

Все, что осталось сделать, — организовать совместное перемещение камеры и самолета при каждой итерации:

BOOL CMainFrame::Update(double d)

(

// Обновить положение камеры C3dMatrix r;

r.Rotate(0, 2.0, 0) ;

m vCamera = r * m vCamera;

m_pScene-»SetCameraPosition (m_vCamera) ;

// Задать верхний вектор C3dVector vu(0, 1, 0) ;

// Построить вектор направления C3dVector vf = m_vCamera * vu;

m_p3cene-»SetCameraDirection (vf, vu) ;

// Задать положение самолета относительно камеры r.Rotate(0, 20, 0);

C3dVector vp = r * m vCamera;

m_pPlane-»SetPosition(vp) ;

// Задать направление C3dMatrix rp;

rp.Rotate(0, 0, 10); // Слегка покачаем крьшьями vu = rp * vu;

vf = vp * vu;

m_pPlane-»SetDirection (vf, vu) ;

return m_wnd3d.Update(TRUE);

)

Текущее положение камеры хранится в объекте C3dVector. Для определения ее нового положения вектор умножается на матрицу поворота. Затем камера переносится на новое место — но это еще не все. Необходимо изменить ориентацию камеры, чтобы она по-прежнему была направлена по касательной к окружности. Чтобы вычислить новое направление камеры, мы умножаем (векторно) верхний вектор (vu) на вектор положения камеры (m_vCamera). Вектор-результат совпадает с вектором направления камеры (Рисунок 6-8).

Вычисление вектора направления камеры 148 SUSy Глава 6. Пеоемешение объеет-ов в мякртр

Перемещение объектов

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

Последнее, что осталось сделать, — вычислить положение и направление маленького самолета, летящего перед камерой. Самолет перемещается по той же траектории, что и камера, однако он на несколько градусов опережает камеру. Чтобы определить положение самолета, мы берем вектор положения камеры и поворачиваем его чуть дальше, пользуясь для этого другой матрицей (г). При определении ориентации самолета я сначала вычислял векторное произведение точно так же, как и для камеры. Однако мне показалось, что смотреть на самолет, который идеально ровно летит впереди камеры, довольно скучно. Я слегка повернул вектор вверх, чтобы создать иллюзию покачивания самолета. Эффект не очень впечатляющий, но зато легко реализуемый.


Относительное движение

Наше следующее приложение имитирует часовой механизм, состоящий из нескольких частей, которые находятся в непрерывном движении по отношению друг к другу. На Рисунок 6-9 изображен внешний вид окна приложения, находящегося в каталоге Clock (на вкладке имеется цветная иллюстрация).

Рисунок. 6-9. Часовой механизм

Перемещение объектов

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

/h2>

Относительное движение

Когда я впервые попытался создать это приложение, то изобразил шестеренки в виде дисков, и на «все про все» у меня ушло около часа. Отладка работы шестеренок потребовала уже целых трех часов! Фрагмент кода, в котором конструируется данный механизм, выглядит довольно просто. Сначала мы создаем стержни и присоединяем к ним шестеренки и стрелки в качестве фреймов-потомков. Затем начинаем вращать стержни. Я подобрал частоту вращения таким образом, чтобы создать впечатление, будто механизм действительно приводится в действие шестеренками. Если внимательно присмотреться, можно заметить, что иллюзия получилась не полной.

Чтобы весь часовой механизм вращался в окне, я создал фрейм, являющийся родительским по отношению ко всем трем стержням. Когда этот фрейм поворачивается вокруг оси у, изображение работающего механизма также начинает вращаться (наблюдательные читатели могли заметить, что передаточный коэффициент шестеренок составляет 4:1 вместо более привычного 12:1, как в большинстве часов). Давайте рассмотрим фрагмент файла MainFrm.cpp, в котором создается стержень с минутной стрелкой. Два других стержня определяются аналогичным образом:

BOOL CMainFrame::SetScene() (

// Создать часовой механизм C3dFrame clock;


clock.Create(m_pScene) ;

double dSpin = -0.1;

// Создать стержень с минутной стрелкой C3dFrame si;

si.Create(Sclock) ;

C3dShape rl;

rl.CreateRod(0, 0, -0.5, О, О, 10, 0.4, 16);

rl.SetColor(0, 0, 1);

sl.AddChild(Srl) ;

// Присоединить минутную стрелку CHand bighand(lO) ;

sl.AddChild(Sbighand) ;

bighand.SetPosition(0, 0, 0) ;

// Присоединить шестеренку CGear gl(1.5, 1.5, 8) ;

sl.AddChildf&gl) ;

gl.SetPosition(0, 0, 5.5);

// Привести стержень во вращение sl.SetRotation(0, 0, 1, dSpin);

}

/b> Я»:'?' Глава 6. Перемещение объектов в макете

Фрейм стержня создается как потомок по отношению к фрейму всего механизма. Затем к фрейму стержня присоединяется цилиндрический объект, который является его визуальным представлением. Минутная стрелка создается как объект класса CHand, производного от CSdShape, который мы рассмотрим чуть позже. Шестеренка тоже является объектом отдельного класса CGear, производного от CSdShape, и точно так же присоединяется к фрейму стержня. Последнее, что осталось сделать, — привести фрейм во вращение функцией C3dFrame::SetRotation.

Стрелки создаются из двух цилиндров и конуса:

CHand::CHand(double 1)

{

CreateRod(0, 0, О, О, О, 0.5, 1, 16);

SetColor(l, 1, 0);

CSdShape r;

r.CreateRod(0, 0, 0.25, 0, 1-3, 0.25, 0.20, 16);

r.SetColor(0, 0, 1);

AddChild(&r) ;

CSdShape с;

c.CreateCone(0, 1-3, 0.25, 0.75, TRUE, 0, 1, 0.25, 0, FALSE, 16);

c.SetColor(l, 1, 0);

AddChild(Sc) ;

}

С шестеренками дело обстоит несколько сложнее. Внешний и внутренний радиус зубцов определяется двумя окружностями. Затем окружности разбиваются на части по числу зубцов, что и дает нам положения вершин (Рисунок 6-10). Генерация списка данных для внешних граней зубцов завершает первую стадию создания фигуры.

Рисунок. 6-10. Конструирование зубцов шестеренки

Перемещение объектов

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


Относитепьное лвижймий Ш^ 151

ся какие- то странные треугольные ячейки. Разумеется, за пять минут работы с Autodesk 3D Studio можно было бы создать идеальные шестеренки и без этого кода:

CGear::CGear(double r, double t, int teeth)

{

double twopi = 6.28318530718;

double rl = r - 0.3;

double r2 = r + 0.3;

int nFaceVert = teeth * 4;

int nVert = nFaceVert * 2;

D3DVECTOR* Vertices = new D3DVECTOR[nVert];

D3DVECTOR* pv = Vertices;

double da = twopi / (teeth * 4);

double a = 0;

for (int i = 0; i « teeth; i++) {

pv-»x = rl * cos(a);

pv-»y = rl * sin(a);

pv-»z = 0;

pv++;

a += da ;

pv-»x = r2 * cos (a) ;

pv-»y = r2 * sin(a);

pv-»z = 0;

pv++;

a += da;

pv-»x = r2 * cos(a);

pv-»y = r2 * sin(a);

pv-»z = 0;

pv++;

a += da;

pv-»x = rl * cos(a);

pv-»y = rl * sin(a);

pv-»z = 0;

pv++ ;

a += da;

}

pv = Vertices;

D3DVECTOR* pv2 = SVertices[nFaceVert] ;

for (i = 0; i « nFaceVert; i++) {

*pv2 = *pv;

pv2-»z = t;

pv++;

pv2++;

}

// Сгенерировать данные граней для зубцов.

// Нервных просят не смотреть!

int nf = (teeth * 5 * 4) + (teeth * 26) + 10;

/b>

Глава 6. Перемещение объектов в макете

int* FaceData = new int[nf] ;

int* pfd = FaceData;

for (i = 0; i « teeth*4; i++) {

*pfd++ = 4;

*pfd++ = i;

*pfd++ = (i + 1) % (teeth*4);

*pfd++ = nFaceVert + ((i + 1) % (teeth*4));

*pfd++ = nFaceVert + (i % (teeth*4));

}

// Завершить список *pfd++ = 0;

Create(Vertices, nVert, NULL, 0, FaceData, TRUE);

// Добавить торцевые грани с заданием нормалей D3DVECTOR nvect [] = {

(О, 0, 1},

(О, 0, -1} };

delete [] FaceData;

FaceData = new int [teeth * 9 + teeth * 4 + 10] ;

pfd = FaceData;

for (1=0; i « teeth; i++) {

*pfd++ = 4;

*pfd++ = i*4;

*pfd++ = 1;

*pfd++ = i*4+3;

*pfd++ = 1;

*pfd++ = i*4+2;

*pfd++ = 1;

*pfd++ = i*4+l;

*pfd++ = 1;

}

*pfd++ = teeth*2;

for (i = teeth-1; i »= 0; i-) {

*pfd++ = i*4+3;

*pfd++ = 1;

*pfd++ = i*4;

*pfd++ = 1;

}

*pfd++ = 0;

AddFaces(Vertices, nVert, nvect, 2, FaceData);

pfd = FaceData;

for (i = 0; i « teeth; i++) {


*pfd++ = 4;

Относительное движение тЩ!) 153

*pfd++ = nFaceVert + i*4;

*pfd++ = 0;

*pfd++ = nFaceVert + i*4+l;

*pfd++ = 0;

*pfd++ = nFaceVert + i*4+2;

*pfd++ = 0;

*pfd++ = nFaceVert + i*4+3;

*pfd++ = 0;

}

*pfd++ = teeth*2;

for (i = 0; i « teeth; i++) {

*pfd++ = nFaceVert + i*4;

*pfd++ = 0;

*pfd++ = nFaceVert + i*4+3;

*pfd++ = 0;

}

*pfd = 0;

AddFaces(Vertices, nVert, nvect, 2, FaceData);

delete [] Vertices;

delete [] FaceData;

SetColor(l, 1, 0);

}

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

Перемещение объектов по произвольным траекториям

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

Создание собственного контроллера движения

Перемещение объектов и полеты — это, конечно, хорошо, но что делать, если вам понадобится что-то другое? Давайте посмотрим, как создать контроллер движения для более интересного объекта — космического танка Mark VII с доплеров-ским радаром Х-диапаэона. Танк может передвигаться по поверхности планеты с различной скоростью и поворачивать на ходу. Его башня быстро вращается, а пушка

/b> в!^' Глава 6. Перемещение объектов в макете

поднимается. Кажется, я забыл упомянуть о радаре, который радостно вертится на башне? На Рисунок 6-11 изображен танк Mark VII при выполнении боевого задания.

Рисунок. 6'П. Космический танк Mark VII с доплеровским радаром Х-диапазона

Перемещение объектов

Хмм... вы обратили внимание на то, что у танка нет колес? Могу предложить два объяснения:


• Это летающий танк.

• Мне было лень возиться с колесами.

Решайте сами.

На Рисунок 6- 12 изображена диаграмма подвижных частей танка (вместе с колесами). Иллюстрация приведена на цветной вкладке.

. Составные части танка

Перемещение объектов

Создание собственного контроллера движения

/h2>

В приложении Tank класс C3dTank является производным от C3dFrame. Последовательность, в которой строится танк, такова: сначала мы присоединяем корпус к внешнему фрейму, затем присоединяем башню к корпусу и в последнюю очередь присоединяем пушку и радар к башне. Радар приводится в постоянное вращение. Пушка может подниматься и опускаться, вращаясь вокруг своей горизонтальной оси. Башня может вращаться вокруг вертикальной оси корпуса.

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

C3dTank::C3dTank() {

// Создать фрейм

C3dFrame::Create(NULL) ;

// Загрузить составные части танка и построить танк

m_hull.Load(IDX_HULL) ;

AddChild(&m_hull) ;

m_turret.Load(IDX_TURRET) ;

m hull.AddChild(&m_turret);

m_gun.Load(IDX_GUN) ;

m_turret.AddChild(&m_gun) ;

// Радар имеет собственный фрейм,

// чтобы было удобнее управлять осью вращения

C3dFrame rframe;

rframe.Create(&m_turret);

C3dShape radar;

radar.Load(IDX_RADAR) ;

rframe.AddChild(Sradar) ;

radar.SetPosition(0, 0, -0.3);

rframe.SetPosition(О, О, 0.3);

rframe.SetRotation(0, 1, 0, 0.1);

SetGun(25) ;

}

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

Все объекты, из которых состоит наш танк, были созданы в 3D Studio и преобразованы в формат .X с помощью утилиты conv3ds, входящей в DirectX 2 SDK. Они были включены в файл приложения RC2 в качестве ресурсов:


//

// STAGE.RC2 - resources Microsoft Visual C++ does not edit

directly

//

/b> fy Глава 6. Перемещение объектов в макете

#ifdef APSTUDIO_INVOKED

terror this file is not editable by Microsoft Visual C++

#endif //APSTUDIO_INVOKED

/////////'11/1111/111/1111111111/1111111/1' I Ullll/lt'I'III'I•II/

I I / I I / / / / / I II t / / / I

II Add manually edited resources here...

^include "3dPlus.rc"

// Tank parts

I DX_HULL XO F re S \ T_hul 1. X

IDX_TURRET XOF res\turret.x

IDX_GON XOF res\gun.x

IDX_RADAR XOF res\radar.x

camo.bmp BITMAP res\camo.bmp

camousa.bmp BITMAP res\camousa.bmp // Звуковые эффекты

IDS_BANG WAVE res\bang.wav

/////////////////////////////////////////////////////////// //////////////////

Рисунок. 6-13. Размещение радара внутри фрейма

Перемещение объектов

Башня

Тэг XOF, встречающийся в файле ресурсов, на самом деле можно заменить любой другой строкой. Я выбрал XOF лишь потому, что такое расширение используется в файлах описания фигур. Единственное место программы, где встречается строка XOF — функция C3dShape::Load, где эта строка используется для того, чтобы отличать XOF-файлы от других типов ресурсов.

Создание собственного контроллера движения

/b>

В класс танка вошли три функции, находящиеся в файле 3dTank.cpp и предназначенные для регулирования углов башни и пушки, а также для стрельбы:

#define D2R 0.01745329251994

void C3dTank::SetTurret(double angle)

{

if ((angle « 0) II (angle »= 360)) { angle = 0;

} double x = sin(angle * D2R);

double z = cos(angle * D2R);

m_turret.SetDirection(x, 0, z, &m_hull);

void C3dTank::SetGun(double angle) {

if (angle « 0) ( angle = 0;

} else if (angle »= 60) ( angle = 60;

> double у = -sin(angle * D2R);

double z = cos(angle * D2R);

m gun.SetDirection(0, у, z, &m turret);

}

void C3dTank::FireGun() {

PlaySound(MAKEINTRESOURCE(IDS_BANG), AfxGetResourceHandie(), SND_RESOURCE) ;

}

Как видите, чтобы определить положение башни и пушки, мы вычисляем вектор направления. Затем мы задаем направление объекта по отношению к его родителю', кстати говоря, именно так функция SetDirection действует по умолчанию. Но я хотел сделать свой код максимально простым и очевидным, поэтому при каждом вызове передаю дополнительный аргумент — эталонный фрейм.


Танк готов. Осталось научиться управлять им.

Контроллер танка

Большая часть кода контроллера находится в классах C3dWnd и C3dController. Чтобы создать собственный контроллер, необходимо лишь ввести новый класс, производный от CSdController, переопределить в нем функцию OnUpdate и установить новый контроллер в своем приложении. Однако перед тем, как писать функцию OnUpdate, следует распределить параметры джойстика

/b> Д1' Глава 6. Перемещение объектов в макете

по выполняемым функциям. Конфигурация, на которой я остановился, приведена в табл. 6-3.

Таблица 6-3. Управление танком

Входной параметр Параметр танка

у Скорость

х Поворот

r Поворот POV (кнопка выбора вида) Направление башни

Кнопки 3 и 4 Подъем и опускание пушки

Кнопка 1 Выстрел из пушки

Я решил использовать параметры х и г для поворотов, чтобы даже при наличии самого простого джойстика с двумя осями можно было управлять танком. Я выбрал для этого приложения джойстик SideWinder Pro — он дает более реалистичные ощущения, чем SpaceBall. К тому же кнопка выбора вида, находящаяся на рукояти джойстика, замечательно подходит для поворотов башни.

Определившись с управлением, можно писать программу. Весь код контроллера состоит из двух функций:

CTankCtrl::CTankCtrl () (

m_dGunAngle = 25;

m_bWasFire = FALSE;

}

void CTankCtrl::OnUpdate(_3DIMPUTSTATE& st, C3dFrame* pFrame) {

// Задать скорость (руководствуясь значением у)

double v = st.dY / 2;

// Определить текущее положение C3dVector pos;

pFrame-»GetPosition (pos) ;

// Получить текущий вектор направления C3dVector dir, up;

pFrairie-»GetDirection(dir, up) ;

// Определить новое направление (с учетом

// параметров х и г)

double dr = -st.dX + -st.dR;

C3dMatrix r;

r.Rotate(0, dr * 3, 0) ;

dir = r * dir;

Создание собственного контроллера движения 'т^ 159

// Умножить вектор направления на скорость, // чтобы определить смещение танка C3dVector ds = dir * v;

// Задать новое положение и направление pos += ds;

pFrame-»SetPosition (pos) ;

pFrame-»SetDirection (dir) ;


// Воспользоваться информацией POV для задания

// ориентации башни.

// Для этого необходимо работать с объектом C3dTank,

// а не CSdFrame.

C3dTank* pTank = (C3dTank*) pFrame;

ASSERT (pTank-»IsKindOf (RUNTIME_CLASS (C3dTank) ) ) ;

if (st.dPov »= 0) { pTank-»SetTurret (st.dPov) ;

}

// Кнопки З и 4 поднимают и опускают пушку if (st.dwButtons & 0х04) {

m_dGunAngle += 0.1;

» if (st.dwButtons & 0х08) (

m dGunAngle -= 0.1;

} if (m_dGunAngle « 0) {

m dGunAngle =0;

} else if (m_dGunAngle » 45) {

m dGunAngle = 45;

} pTank-»SetGun (m dGunAngle);

// Проверить, не пора ли стрелять if (st.dwButtons & 0х01) { if (!m_bWasFire) 1 pTank-»FireGun () ;

m_bWasFire = TRUE;

} } else (

m_bWasFire = FALSE;

} }

Конструктор лишь инициализирует некоторые локальные данные; вся настоящая работа выполняется в функции Onllpdate. Параметр у задает текущую скорость. Текущая позиция и направление танка хранятся в объектах C3dVector. Параметры х и г определяют матрицу поворота, которая задает новую ориента-

/b> '^р? Глава 6. Перемещение объектов в макете

цию вектора направления. Вектор направления умножается на скорость — полученный вектор смещения складывается с вектором прежнего положения танка. Затем мы перемещаем танк в новое положение и задаем для танка новое направление.

Кнопка выбора вида определяет направление башни. Мы проверяем состояние кнопок 3 и 4, и если они нажаты, то угол подъема пушки изменяется на небольшую величину. Если держать одну из этих кнопок нажатой, башня будет медленно подниматься или опускаться.

Остается лишь учесть кнопку стрельбы. Проверка локальной переменной m_bWasFire предотвращает повторные выстрелы при нажатой кнопке — автоматическое оружие в США запрещено.

Окончательная сборка приложения

За основу приложения Tank был взят код приложения Moving. Я удалил ненужные команды меню и заменил текущую фигуру объектом CSdTank. Кроме того, я включил в макет фоновое изображение. Ниже приведен фрагмент кода, в котором происходит настройка главного окна приложения:


int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct) {

// Загрузить фоновое изображение m_imgBkgnd.Load(IDB BKGND) ;

NewScene() ;

ASSERT(m_pScene) ;

// Создать объект-контроллер m_pController = new CTankCtrl;

m_pController-»Create (&m wnd3d,

OnGetCtrlFrame,

this) ;

// Восстановить конфигурацию контроллера m_pController-»SelectDevice (m_iObjCtrlDev) ;

return 0;

}

Функция NewScene создает макет и задает начальные условия

BOOL CMainFrame::NewScene() (

// Удалить макет, если он уже существует if (m_p3cene) {

m_wnd3d.SetScene(NULL) ;

delete m_pScene;

m_pScene = NULL;

?

Создание собственного контооллеоа движения

/b>

// Создать исходный макет m_pScene = new CSdScene;

if ( !m_pScene-»Create() ) return FALSE;

// Задать источники света C3dDirLight dl;

dl.Create (0.8, 0.8, 0.8);

m pScene-»AddChild(&dl) ;

dl.SetPosition(-2, 2, -5);

dl.SetDirection(l, -1, 1);

m_pScene-»SetAmbientLight(0.4, 0.4, 0.4);

// Установить положение и направление камеры // в исходное состояние m_pScene-»SetCameraPosition (C3dVector (0, 5, -25));

m_pScene-»SetCameraDirection (C3dVector (О, О, 1));

m_wnd3d.SetScene(m_pScene) ;

// Задать фоновое изображение m pScene-»SetBackground(&m_imgBkgnd) ;

// Разместить танк в макете if (!m_pTank) m_pTank = new C3dTank;

m_pScene-»AddChild(m_pTank) ;

m_pTank-»SetPosition(0, 0, 0) ;

m_pTank-»SetDirection(0, 0, 1) ;

return TRUE;

}

Если танк уедет за край окна и потеряется, можно выполнить команду Fite ¦New, чтобы вызвать функцию NewScene и начать все заново. Осталось сказать о последнем изменении, внесенном мной, — когда контроллер запрашивает указатель на фрейм, с которым он должен работать, функция OnGetCtrlFrame возвращает ему указатель на танк:

C3dFrame* CMainFrame::OnGetCtrlFrame(void* pArg) (

CMainFrame* pThis = (CMainFrame*) pArg;

ASSERT(pThis) ;

ASSERT
return pThis-»m_pTank;

}

Обратите внимание — хотя функция должна возвращать указатель на C3dFrame, на самом деле она передает указатель на объект C3dTank. Мы пользуемся этим обстоятельством в функции Onllpdate, приведенной на стр. 144. Если раньше вам могло показаться, что преобразование указателя на C3dFrame в указатель на

/b> Ш^ Глава 6. Перемещение объектов в макете

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

Пора в дорогу

Хватит возиться с самолетами, часами и танками. Пойдем дальше и посмотрим, как выбираются объекты в макете.

Преобразования

Глава 5 Преобразования
Матрицы и преобразования

Преобразования

Большая часть преобразований, которыми мы будем пользоваться, может быть описана в виде вещественной матрицы 3х3. Однако работа с матрицами 3х3 несколько усложняет вычисления, поскольку некоторые преобразования выполняются не так, как другие. Например, перенос реализуется сложением элементов матрицы, а поворот — умножением. Тем не менее, если вставить элементы матрицы 3х3 в матрицу 4х4 и должным образом заполнить свободные места, все преобразования можно будет выполнять посредством операции матричного умножения, поэтому для описания преобразований мы будем пользоваться матрицами 4х4. Если вы незнакомы с однородной системой координат, которая применяется при описании преобразований в трехмерном пространстве (а кто с ней знаком, кроме математиков?), вы наверняка чувствуете себя сбитым с толку. Давайте немедленно решим эту проблему и начнем с небольшого примера того, как пользоваться матрицами для преобразования координат точки в пространстве. Поскольку формулы для трехмерного случая оказываются довольно длинными, мы вместо этого рассмотрим упрощенный пример на плоскости — уверяю вас, в трехмерном пространстве он работает точно так же.
Двумерные координаты точки х, у в однородной системе координат представляются вектором следующего вида:
х
У 1
Матрица для переноса точки на плоскости выглядит следующим образом:
WlaTDHLlbl и поеобпазпняния "it!!® 121
1 0 dx О 1 dy 00 1
где dx — смещение точки по оси х, а dy — смещение по оси у. Теперь давайте умножим исходный вектор на эту матрицу и посмотрим, что у нас получится. При умножении матрицы на вектор-столбец каждый элемент вектора-результата представляет собой сумму элементов соответствующей строки матрицы, умноженных на элементы исходного вектора:
"1 0 dx] Гх] Г1 * х + 0 - у + 1 * dx] Гх + dx" 01dyxy=0*x+l*y+l*dy=y+dy 001 1 0 * х + 0 * у + 1 • 1 1
Как видит! при умножении вектора на матрицу переноса получается вектор, смещение которого в точности соответствует желаемому. Гениально! Когда я впервые научился пользоваться однородными координатами для сведения всех преобразований к умножению матриц, на меня это произвело глубочайшее впечатление. Впрочем, даже если вы не особенно потрясены, по крайней мере матричные преобразования становятся несколько более понятными.

Теперь давайте попробуем применить полученные знания на практике и воспользуемся объектом C3dMatrix, содержащим массив вещественных величин 4х4, для переноса вектора. Чтобы вы получше представляли себе класс C3dMatrix, привожу исходный текст его конструктора:

C3dMatrix::C3dMatrix()

{

т_00=1.0; т_01°0.0; т_02=0.0; т_03=0.0;

т_10=0.0; т_11=1.0; т_12=0.0; т_13=0.0;

т_20=0.0; т_21=0.0; т_22=1.0; т_23=0.0;

т_30=0.0; т_31=0.0; т_32=0.0; т_33=1.0;

}

В исходном состоянии, задаваемом в конструкторе, эта матрица является матрицей идентичного преобразования, или единичной матрицей. Если умножить вектор на такую матрицу, он не изменится. Приведенный ниже фрагмент, использующий объект C3dMatrix, осуществляет перенос вектора х, у, z со смещениями координат dx, dy, dz:

C3dVector v (х, у, z) ;

C3dMatrix т;

т.Translate(dx, dy, dz);

v = т * v;

Объект C3dVector инициализируется элементами исходного вектора. Затем, после конструирования объекта C3dMatrix (который в первоначальном состоянии совпадает с матрицей идентичного преобразования), вызывается его функция

/b>

Глава 5. Поеобоазования

Translate для занесения в матрицу преобразования переноса. Наконец, вектор умножается на матрицу, и результат присваивается исходному вектору.

Не следует полагать, будто функция C3dMatrix: translate просто инициализирует матрицу; как нетрудно убедиться по приведенному ниже фрагменту, на самом деле она комбинирует преобразование переноса с любыми преобразованиями, уже занесенными в матрицу:

void C3dMatrix::Translate(double dx, double dy, double dz)

{

C3dMatrix tx( 1, О, О, О,

О, 1, О, О, О, 0, 1, О, dx, dy, dz,l);

*this *= tx;

Временная матрица инициализируется элементами для преобразования переноса. Затем текущая матрица умножается на временную матрицу переноса, а результат снова присваивается текущей матрице.

Пользуясь различными функциями класса C3dMatrix, можно комбинировать в одной матрице сразу несколько преобразований и затем применять их к нужному количеству векторов. Удобно также создать отдельные объекты C3dMatrix, каждый из которых соответствует отдельному преобразованию, и просто перемножить их для получения итогового преобразования:


mFinal = ml * m2 * m3;

Преобразования трехмерных объектов

Теперь давайте посмотрим, как матричные преобразования используются на практике, при работе с трехмерными объектами. Чтобы применить матрицу преобразования к объекту C3dShape, вовсе не нужно заниматься умножением. Вместо этого следует скомбинировать новое преобразование с текущим, хранящимся во фрейме объекта. Вспомните — фрейм, определяющий положение и ориентацию объекта в макете, на самом деле представляет собой преобразование, применяемое ко всем точкам фигуры. Кроме того, фрейм объекта является потомком другого фрейма, расположенного выше в иерархии, и для определения окончательного положения объекта необходимо скомбинировать результаты всех преобразований в иерархии фреймов. Мы собираемся изменить преобразование, хранящееся во фрейме объекта, расположенном где-то внизу иерархии фреймов, пример которой изображен на Рисунок 5-1.

Изображенный на Рисунок 5-1 сложный объект состоит из двух фигур, каждая из которых обладает собственным фреймом и визуальным элементом. Для преобразования всего объекта следует модифицировать объединяющий фрейм, который является общим родителем для фреймов обоих компонентов.

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

Поеобоазования тоехмеоных объектов vis' 123

Рисунок. 5-1. Иерархия фреймов

Преобразования

каждом конкретном случае? Я надеюсь, что после знакомства с примерами вы и сами найдете ответ на этот вопрос.

Все преобразования, которые мы будем рассматривать, содержатся в приложении TransFrm. Для демонстрации я выбрал самолет, поскольку его положение в макете и ориентация определяются с первого взгляда. На Рисунок 5-2 изображено начальное состояние самолета, находящегося в начале координат.

Окно приложения до применения преобразований

Преобразования

/h2>

Глава 5. Преобразования

Перенос

Первый тип рассматриваемых нами преобразований — перенос. Переносом называется простое прямолинейное перемещение объекта в одном направлении. Для переноса объекта следует прибавить к его координатам х, у и z величины смещений. На Рисунок 5-3 изображен результат переноса по оси х.


Рисунок. 5-3. Перенос по оси х

Преобразования

Фрагмент программы, в котором был осуществлен этот перенос, выглядит следующим образом:

void CMainFrame::OnEditTranslatex() (

if (!m_pCurShape) return;

C3dMatrix m;

m.Translate (2, О, О);

m_pCurShape-»AddTransform(m, D3DRMCOMBINE_AFTER) ;

}

Сначала мы создаем объект C3dMatrix для хранения матрицы переноса (в данном случае, для смещения на 2 единицы вдоль оси х). Затем преобразование применяется к текущей фигуре. Обратите внимание на аргумент D3DRMCOMBINE_AFTER, который указывает на необходимость применения преобразования после любых существующих преобразований. Другими словами, после завершения всех преобразований появляется дополнительный перенос объекта вдоль оси х.

Поворот

Теперь давайте развернем наш самолет, расположенный в начале координат, на 45 градусов вокруг оси у. Результат изображен на Рисунок 5-4.

Преобразования трехмерных объектов

/b>

Рисунок. 5-4, Поворот вокруг оси у

Преобразования

Ниже приведен текст функции, в которой выполняется поворот:

void CMainFrame::OnEditRotatey() {

if (!m_pCurShape) return;

C3dMatrix m;

m.RotatefO, 45, 0);

m_pCurShape-»AddTransform(m, D3DRMCOMBINE AFTER);

Масштабирование

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

Наш самолет выглядит по меньшей мере странно! Результат последующего применения аналогичного масштабирования по оси z изображен на Рисунок 5-6.

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

void CMainFrame::OnEditscalex() {

if (!m_pCurShape) return;

C3dMatrix m;

m.Scale(2.0, 1.0, 1.0);

m_pCurShape-»AddTransform(m, D3DRMCOMBINE AFTER);

}

/b>

Глава 5. Преобразования

Рисунок. 5-5. Масштабирование объекта по осям х и у

Преобразования


Объект после равномерного масштабирования по осям х, у и z

Преобразования

Обратите внимание на то, что коэффициенты масштабирования по осям у и z равны 1, а не 0. Если присвоить им нулевые значения, вам будет нелегко рассмотреть свои объект!

/h2>

Порядок преобразований

Мы рассмотрели отдельные преобразования переноса, поворота и масштабирования. Теперь давайте посмотрим, что происходит при выполнении серии последовательных преобразований. Начнем с переноса вдоль оси х, за которым следует поворот вокруг оси у. Результат изображен на Рисунок 5-7.

Рисунок. 5-7. Перенос вдоль оси х, за которым следует поворот вокруг оси у

Преобразования

Совпадает ли такой результат с тем, что вы ожидали увидеть? Текст функции приведен ниже:

void CMainFrame::OnEditTranrot() {

if (!m_pCurShape) return;

C3dMatrix m;

m.Translate(3, 0, 0) ;

m.Rotate(0, 45, 0) ;

m_pCurShape-»AddTransform(m, D3DRMCOMBINE_AFTER) ;

Как видите, мы осуществили перенос на 3 единицы вдоль оси х, после чего развернули объект на 45 градусов вокруг оси у. Во время поворота самолет находился на расстоянии в 3 единицы от начала координат. Следовательно, самолет описал дугу в 45 градусов по окружности радиусом в 3 единицы.

Давайте повторим те же самые преобразования, но на этот раз изменим их порядок. Результат изображен на Рисунок 5-8.

Как видите, результат значительно отличается от предыдущего. На этот раз все выглядит так, словно после переноса самолет развернулся вокруг собственной оси, а не вокруг оси у макета.

/h2>

Глава 5. Поеобоазования

Рисунок. 5-8. Поворот вокруг оси у, за которым следует перенос вдоль оси х

Преобразования

Только что мы сделали важное открытие: порядок применения преобразований чрезвычайно важен. Кроме того, мы выяснили, что для того, чтобы повернуть объект вокруг оси макета, следует применять поворот после всех переносов;

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


В нашем приложении имеются команды меню, поворачивающие объект вокруг осей макета, по аналогии с фрагментом на стр. 128. Сюда также включены команды для выполнения поворотов вокруг собственной оси объекта. Ниже приводится пример поворота вокруг оси у объекта:

void CMainFrame::OnEditRobjy() {

if (!m_pCurShape) return;

C3dMatrix m;

m.Rotate(0, 45, 0) ;

m_pCurShape-»AddTransform(m, D3DRMCOMBINE_BEFORE) ;

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

Возвращение на базу

Полет подходит к концу, и настало время возвращаться обратно. Наше приложение содержит команду Edit ¦ Reset, которая возвращает объект в начало координат

Преобразования трехмерных объектов

/b>

и возвращает ему исходное положение и ориентацию. Ниже приведена соответствующая функция:

void CMainFrame::OnEditReset()

{

if (!m_pCurShape) return;

C3dMatrix m;

m_pCurShape-»AddTransform(m, D3DRMCOMBINE_REPLACE) ;

}

Вам может показаться, что здесь допущена какая-то ошибка — ведь для матрицы вообще не задано никакого преобразования. Однако на самом деле именно это нам и нужно! Обратите внимание на использование аргумента D3DRMCOMBINE_REPLACE, заменяющего любое текущее преобразование новой матрицей. Конструктор матрицы инициализирует ее элементами единичной матрицы; заменяя текущую матрицу фрейма на матрицу идентичного преобразования, мы возвращаем объект в исходное состояние.

Экспериментируйте!

В приложении имеется окно диалога, открываемое командой Edit ¦ Transform Shape. Оно используется для задания произвольных преобразований переноса, поворота и масштабирования. В любом случае можно указать, следует ли применять новое преобразование до текущего, после него или же заменить текущее преобразование новым. Несколько опытов с окном диалога Transforms, изображенным на Рисунок 5-9, заполнят все возможные пробелы в вашем понимании того, как же комбинируются преобразования.


Рисунок. 5-9. Окно диалога Transforms

Преобразования

Житейские мелочи

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

/b>

Глава 5. Преобразования

колоды по-прежнему оставались прямыми, но не были перпендикулярны столу, как показано на Рисунок 5-10.

Рисунок. 5-10. «Сдвинутая» колода карт

Преобразования

Итак, мы применили к колоде преобразование сдвига. На Рисунок 5-11 показано, что получится в результате применения сдвига к нашему самолету.

Рисунок 5-П. Результат применения сдвига

Преобразования

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

void CMainFrame::OnEditShear() (

if (!m_pCurShape) return;

Til



Проверка попадания

Глава 7 Проверка попадания
Процесс выделения

Проверка попадания

Выделение объектов мышью выглядит очень естественно — пользователь наводит указатель мыши на объект и нажимает кнопку. Тем не менее приложению не так просто определить, какой же объект был выделен. Прежде всего, пользователь может применять мышь для манипуляций с объектами, поэтому сообщения, посылаемые Windows главному окну (о том, где находится указатель мыши и какая кнопка была нажата), должны попадать сразу в несколько мест. Обычно это делается следующим образом: некий центральный фрагмент программы перехватывает сообщения мыши и направляет их всем другим фрагментам, которым они могут понадобиться. Вы сохраняете текущее состояние мыши в глобальной переменной и позволяете всему коду непосредственно обращаться к ней. Альтернативный вариант — реализовать предложенную мной схему, то есть написать простую функцию, которая обрабатывает сообщения мыши и обращается к другим фрагментам приложения, уведомляя их о наступлении того или иного события.
Предположим, вы знаете, в какой точке экрана находится указатель мыши и что пользователь нажал кнопку, желая выделить объект в макете. Но как же определить, в каком объекте пользователь произвел щелчок мышью? Первым делом необходимо привести экранные координаты мыши к координатам клиент-ной области вашего окна. Затем вам фактически понадобится обратить проекционное преобразование, которым пользуется механизм визуализации, и перевести двумерные координаты на вашем экране в пространственные. Сделать это не так просто, как может показаться с первого взгляда. Как мы вскоре увидим, меха-
Процесс выделения 'тЦЦ 165
низм DirectSD способен выполнить подобную процедуру за нас — в определенной степени. Вместо того чтобы преобразовать экранные координаты мыши в трехмерные, он выдает список всех объектов, которые находятся ниже выбранной точки. Кроме того, список сортируется по глубине, чтобы вы могли определить, какой объект находится перед остальными — именно его (будем надеяться!) и пытается выделить пользователь. Все эти операции требуют довольно сложного жонглирования матрицами преобразований.

В наших приложениях-примерах в любой момент времени разрешается выделить только один объект. Подобное ограничение упрощает код, поскольку можно завести переменную-указатель (m_pCurShape), которая ссылается на текущий выделенный объект или равна NULL, если выделенных объектов нет. Позднее мы увидим, что в наших приложениях также поддерживается концепция текущей выделенной грани объекта — грани текущего выделенного объекта, над которой находился указатель мыши в момент выделения.

Для того чтобы реализовать проверку попаданий в приложениях-примерах, мне пришлось включить в них немало фрагментов, рассредоточенных по разным местам. В последующих разделах я постараюсь объяснить, где должен находиться тот или иной фрагмент и как он работает. Если вы не следите за моими объяснениями и хотите самостоятельно разобраться, как работает программа, советую запустить приложение под отладчиком, установить точку прерывания в функции C3dWnd::OnWndMsg (файл 3dWnd.cpp в библиотеке 3dPlus) и следить за выполнением программы. Еще раз хочу подчеркнуть, что эту задачу можно было решить множеством способов, причем выбранное мною решение, вероятно, ничуть не лучше любого другого. Если вам кажется, что ваша собственная идея даст сто очков вперед предложенной мною, — наверное, вы правы и именно ей вам следует пользоваться в ваших программах.

Выделение всего объекта

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

Перед тем как ваше приложение сможет воспользоваться средствами проверки попадания, заложенными в классе C3dWnd, оно должно вызвать функцию, назначение которой — установка уведомляющей функции, вызываемой при каждом попадании в объект. Наверное, нет особого смысла возиться с проверкой попаданий, если потом ничего не делать с ее результатами. Начнем с создания главного обрамленного окна программы:


int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct) {

// Разрешить выделение объектов мышью m_wnd3d.EnableMouseSelection(OnSelChange, this);

}

/b> lly Глава 7. Проверка попадания

В приведенной выше строке программы устанавливается уведомляющая функция OnSelChange. Функция OnSelChange является статической функцией класса CMainFrame, и потому значение указателя this для нее не определено. Как мы вскоре увидим, второй аргумент функции EnableMouseSetection передается в виде аргумента рАгд при вызове уведомляющей функции — в данном случае мы передаем указатель на объект C++ (если этот момент покажется вам непонятным, просмотрите код функции EnableMouseSelection в файле C3dWnd.cpp). Давайте посмотрим, как щелчок мышью обрабатывается в классе C3dWnd — это снова приведет нас к обсуждению уведомляющей функции. Ниже приведен фрагмент обработчика сообщений окна, связанный с проверкой попадания:

BOOL C3dWnd::OnWndMsg(UINT message, WPARAM wParam,

LPARAM IParam, LRESULT* pResult ) {

// Разрешено ли выделение объектов мышью?

if (m_bEnableMouseSelection

&& (message == WM_LBUTTONDOWN)) (

CPoint pt(LOWORDfIParam), HIWORD(IParam));

C3dShape* pShape = HitTest(pt);

if (m_p3elChangeFn) {

// Вызвать уведомляющую функцию m_pSelChangeFn(pShape, pt, m pSelChangeArg);

} }

return CWnd::OnWndMsg(message, wParam, IParam, pResult);

}

Если выделение мышью было разрешено (рассматривается именно этот случай), мы создаем объект CPoint по координатам мыши, содержащимся в сообщении WM_LBUTTONDOWN, а затем вызываем функцию HitTest, чтобы определить, произошло ли попадание в фигуру. Результат проверки (который равен NULL, если под мышью не оказалось ни одного объекта) возвращается приложению через уведомляющую функцию (которая была указана при разрешении выделения мышью). Давайте посмотрим, как уведомляющая функция используется в приложении:

void CMainFrame::OnSelChange(C3dShape* pShape, CPoint pt,

void* pArg) (

// Получить указатель на класс

CMainFrame* pThis = (CMainFrame*) pArg;


ASSERT(pThis) ;

ASSERT (pThis-»IsKindOf (RUNTIME_CLASS (CMainFrame) ) ) ;

if (pShape) {

// Убедиться, что попадание пришлось

Выделение всего пбъеш-а тТО 1&7

// не в рамку выделения и не в фигуру-указатель if ( !pShape-»IsPartOЈ (pThis-»m_pSelBox)

&& !pShape-»IsPartOf (pThis-»m_pHitPtr) ) {

// Определить грань, в которую мы попали

C3dViewport* pViewport =

pThis-»m_wnd3d.GetStage() -»GetViewport

p3hape-»HitTest (pt, pViewport,

&pThis-»m_iHitFace, &pThis-»m_vHitPoint) ;

1 .'. i ^ i * \

pShape = NULL;

}

i

I / Сделать выделенную фигуру текущей pThis-»MakeCurrent (pShape) ;

}

Выделенный объект передается функции MakeCurrent, которая рисует вокруг него рамку, чтобы отличить от других объектов (мы подробнее рассмотрим функцию MakeCurrent на стр. 177). Самая важная особенность этого фрагмента заключается в том, что 41ункция OnSelChange является статической, и потому, как было сказано выше, не имеет указателя this. Мы справились с данным затруднением, передавая адрес объекта C++ в качестве аргумента функции, разрешившей выделение объектов мышью (EnableMouseSelection). Значение, полученное уведомляющей функцией, преобразуется к типу указателя на наш класс. Хитро, не правда ли? Реализация косвенного вызова (callback) функции класса требует несколько больших усилий, поэтому уведомляющая функция была сделана статической для упрощения программы.

Теперь давайте более подробно рассмотрим, как же происходит проверка попадания. Функция C3dWnd::HitTest просто передает запрос ракурсу:

// Проверить на попадание в видимый объект C3dShape* C3dWnd::HitTest(CPoint pt) (

ASSERT(m_p3tage) ;

return m_pStage-»GetViewport ()-»HitTest (pt) ;

}

Фактическая проверка попадания производится в классе ракурса (код которого находится в файле 3dStage.cpp):

C3dShape* C3dViewport::HitTest(CPoint pt) {

IDirect3DRMPickedArray* pIPickArray = NULL;

/b> ЯНУ Глава 7. Поовеока попадания

ASSERT(m_pIViewport) ;

m_hr = m_pIViewport-»Pick(pt.x, pt.y, SpIPickArray);


if (FAILED(m_hr)) return NULL;

// Проверить, есть ли в массиве элементы if (pIPickArray-»GetSize () == 0) { pIPickArray-»Release () ;

return NULL;

)

// Получить первый (верхний) элемент IDirect3DRMVisual* pIVisual = NULL;

IDirect3DRMFrameArray* pIFrameList = NULL;

m hr = pIPickArray-»GetPick(0, SpIVisual, SpIFrameList, NULL) ;

ASSERT(SUCCEEDED(m_hr)) ;

ASSERT(pIVisual) ;

ASSERT(pIFrameList) ;

// Получить последний фрейм в списке IDirect3DRMFrame* pIFrame - NULL;

pIFrameList-»GetEiement (pIFrameList-»GetSize () - 1, &pI Frame);

ASSERT(pIFrame) ;

// Получить значение 'AppData' фрейма, // которое должно быть указателем на класс C++

C3ctShape* pShape = (C3dShape*) pIFrame-»GetAppData () ;

if (pShape) (

if(!pShape-»IsKindOf(RUNTIME_CLASS(C3dShape))) { pShape = NULL;

}

pIFrame-»Release () ;

pIFrameList-»Release () pIVisual-»Release () ;

pIPickArray-»Release ()

return pShape;

Первым делом мы требуем от интерфейса ракурса создать то, что в механизме визуализации называется списком выбора (pick list), то есть список всех визуальных элементов, находящихся под определенной точкой окна. Список визуальных элементов (pIPickArray) упорядочен так, чтобы верхний элемент находился в начале списка. Затем мы определяем значение указателя (pIVisual) на первый визуальный элемент и получаем по нему список фреймов (pIFrameList), к которым присо-

<::::й^

Выделение всего объекта т¦¦¦¦ 169

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

Список фреймов начинается с корневого фрейма (макета), за которым последовательно перечисляются все фреймы-потомки до самого последнего, содержащего отмеченный визуальный элемент.

Рисунок. 7-1. Структура списка выбора

Проверка попадания

Последнее, что нам осталось сделать, — извлечь значение переменной AppData из интерфейса фрейма. Объекты классов CSdFrame и C3dShape хранят в этой переменной значения своих указателей this. Мы преобразуем значение AppData к типу указателя на объект C3dShape и проверяем, не равен ли получившийся указатель NULL. Если не равен, то выполняется дополнительная проверка того, что данный указатель является указателем на объект C3dShape, с использованием функции MFC, возвращающей runtime-информацию о классе.


AppData может принимать значения трех типов: NULL, указатель на объект C3dFrame, указатель на объект C3dShape. Код проверки попадания работает лишь в том случае, если указатель ссылается на объект C3dShape и если объект класса C++, создавший визуальный элемент и фрейм, не был уничтожен. Деструктор класса C3dFrame (базового для C3dShape) присваивает AppData значение NULL, так что можно не опасаться случайного получения указателя на удаленный объект C++. Из всего сказанного следует, что функция HitTest полезна лишь при работе с теми трехмерными фигурами, с которыми связан текущий объект класса C3dShape.

Отображение выделения на экране

Если запустить приложение Select и щелкнуть на каком-нибудь объекте, вокруг последнего появляется рамка, похожая на изображенную на Рисунок 7-2.

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

/b>

Глава 7. Проверка попадания

тами О, О, О. В случае сферы на Рисунок 7-2, начало координат находится внутри объекта.

Рисунок. 7-2. Выделенный объект

Проверка попадания

Функция, рисующая рамку, несложна, хотя несколько длинна:

void CMainFrame::ShowSelection() {

// Определить граничную область объекта double xl, x2, yl, y2, zl, z2;

BOOL b = m_pCurShape-»GetBox (xl, yl, zl, x2, y2, z2);

ASSERT(b) ;

// Создать новую рамку вокруг фигуры m_pSelBox = new CSdShape;

double r = 0.03;

double re = r * 2;

C3dShape rl, r2, r3, r4, r5, r6, r7, r8, r9,

rIO, rll, rl2, rd, ru, cd, cu;

// Создать цилиндры, из которых состоит рамка

m_pSelBox-»AddChild(&r3) ;

r4.CreateRod(xl, y2, zl, x2, y2, zl, r) ;

m_pSelBox-»AddChild(&r4) ;

r5.CreateRod(x2, yl, zl, x2, y2, zl, r) ;

m_pSelBox-»AddChild(&r5) ;

r6.CreateRod(xl, y2, zl, xl, y2, z2, r) ;


^йй ВЫЛРПЙНИА япйгп nfi'^Rkra ''m^.

171

m_pSelBox-»AddChild(&r6) ;

r7.CreateRod(x2, y2, zl, x2, y2, z2, r) ;

m_pSelBox-»AddChild(&r7) ;

r8.CreateRod(x2, yl, zl, x2, yl, z2, r) ;

m_p3elBox-»AddChild(&r8) ;

r9.CreateRod(xl, yl, z2, xl, y2, z2, r) ;

m_pSelBox-»AddChild(&r9) ;

rIO.CreateRodfxl, yl, z2, x2, yl, z2, r) ;

m_pSelBox-»AddChild(&rlO) ; • rll.CreateRod(x2, yl, z2, x2, y2, z2, r) ;

m_pSelBox-»AddChild(&rll) ;

rl2.CreateRod(xl, y2, z2, x2, y2, z2, r) ;

m_pSelBox-»AddChild(&rl2) ;

//Создать цилиндры и конусы для отображения векторов rd.CreateRod(0, 0, 0, 0, 0, z2 * 1.2, r) ;

m pSelBox-»AddChild (&rd) ;

cd.CreateCone(0, 0, z2 * 1.2, re, TRUE, О, О, z2 * 1.4, 0, FALSE);

m_p3elBox-»AddChild(&cd) ;

ru.CreateRod(0, 0, 0, 0, y2 * 1.1, 0, r) ;

m_pSelBox-»AddChild(&ru) ;

cu.CreateCone(0, y2 • 1.1, 0, re, TRUE, 0, y2 * 1.2, О, О, FALSE) ;

m_pSelBox-»AddChild(&cu) ;

// Задать положение и ориентацию рамки

// в соответствии с положением и ориентацией фигуры

double х, у, z, xd, yd, zd, xu, yu, zu;

m pCurShape-»GetPosition (х, y, z) ;

m_pCurShape-»GetDirection (xd, yd, zd, xu, yu, zu) ;

m_pSelBox-»SetPosition (х, y, z) ;

m_pSelBox-»SetDirection (xd, yd, zd, xu, yu, zu) ;

// Присоединить рамку к текущей фигуре,

// чтобы обеспечить их совместное перемещение

m_pCurShape-»AddChild (m_pSelBox) ;

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

Поскольку рамка также состоит из нескольких визуальных элементов, можно задать резонный вопрос — почему их нельзя выделить мышью? В уведомляющей функции на стр. 167 можно найти следующий оператор:

pShape-»IsPartOf (pThis-»m_pSelBox) 172 ад1'' Глава 7. Проверка попадания


Он является составной частью проверки, которая не позволяет выделять видимые элементы рамки. Функция IsPartOf проверяет, совпадает ли данный фрейм с фреймом-аргументом или с одним из его родителей. Другими словами, она проверяет, входит ли данный фрейм в иерархию другого фрейма.

Возможно, вы также вспомните мои слова о том, что видимые объекты, не имеющие присоединенных объектов C++, нельзя выделить. Если посмотреть на исходный текст функции, которая строит рамку, можно убедиться в том, что объекты C++, использованные при создании цилиндров и конусов, уничтожаются после создания рамки. Спрашивается, зачем же тогда нужна проверка IsPartOf? Дело в том, что один объект C++ все же остался — тот, на который ссылается переменная m_pSelBox. Чтобы заведомо устранить все возможные проблемы, мы идем на эту дополнительную проверку, хотя она и не является абсолютно необходимой. Кроме того, когда-нибудь в будущем функция, которая создает рамку, может измениться, и созданные объекты C++ не будут удаляться. Даже если это произойдет, выделение должно работать по-прежнему.

Мы научились выделять объекты. Запустите приложение Select, вставьте в текущий макет несколько объектов и пощелкайте на них мышью. При установке флажка Selection Box вы можете перемещать текущий выделенный объект и при этом видеть его рамку. Команда-флажок View ¦ Selection Box разрешает (и запрещает) отображение рамки в окне.

Выделение отдельной грани

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

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


Зная положение указателя мыши в окне, необходимо выяснить, какая грань объекта будет содержать проекцию точки. Более того, если под указателем мыши находится сразу несколько граней, необходимо выбрать ближнюю (с минимальным значением z).

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

Единственное затруднение может возникнуть при проектировании вершин грани на плоскость вида; этот процесс состоит из трех шагов. Сначала необходимо преобразовать координаты каждой вершины из локальных координат фрейма в систему общих, «мировых» координат. Затем мировые координаты преобразуются в однородные координаты в плоскости вида. Наконец, вектор однородных координат преобразуется в точку окна.

Выделение отдельной грани 4¦^i 173

Рисунок. 7-3. Грань, спроектированная в трехмерное окно

Проверка попадания

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

BOOL C3dShape::HitTest(CPoint pt,

C3dViewport* pViewport, int* piFace, D3DVECTOR* pvHit)

int iHitFace = -1;

double dHitZ = 0;

// Просмотреть список граней D3DVECTOR lv;

D3DVECTOR wv;

D3DRMVECTOR4D sv;

for (int i = 0; i « nFaces; i++) {

// Получить данные грани IDirect3DRMFace* piFace ° NULL;

m hr = pIFac@List-»GetElement (i, spIFace) ;

ASSERT;SUCCEEDED(m_hr)) ;

// Получить количество вершин и разместить массив // для хранения экранных координат int nVert = pIFace-»GetVertexCount () ;

ASSERT(nVert » 2) ;

/h2>

Глава 7. Проверка попадания

POINT* pScrnVert = new POINT [nVert];


// Преобразовать каждую вершину к экранным координатам double dZ = 0;

for (int v = 0; v « nVert; v++) {

// Получить вектор в локальных координатах

// (координатах фрейма)

m_hr = pIFace-»GetVertex(v, &lv, NULL) ;

ASSERT(SUCCEEDED(m_hr)) ;

// Преобразовать их в мировые координаты m_hr = m_pIFrame-»Transform(&wv, &lv) ;

ASSERT(SUCCEEDED(m_hr)) ;

// Преобразовать мировые координаты // в экранные

m_hr = pIViewport-»Transform(&sv, &wv) ;

ASSERT(SUCCEEDED(m_hr)) ;

// Преобразовать однородные координаты

// в абсолютные координаты пикселей

double w = sv.w;

if (w != 0) {

pScrnVert[v].x = (int) sv.x / w;

pScrnVert[v].у = (int) sv.y / w;

dZ += sv.z / w;

) else {

pScrnVert[v].x = 0;

pScrnVert[v].у = 0;

} } dZ /= nVert;

// Проверить, лежит ли точка попадания внутри // многоугольника на экране

if (::_3dPointInPolygon(pScrnVert, nVert, pt)) { if (iHitFace « 0) { iHitFace = i;

dHitZ = dZ;

} else (

if (dZ « dHitZ) {

iHitFace = i;

dHitZ •= dZ;

} } )

// Освободить грань после завершения delete [] pScrnVert;

Выделение отдельной грани 'тЩ 175

pIFace-»Release () ;

}

// Установить возвращаемое значение *piFace = iHitFace;

return TRUE;

}

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

Однородный вектор состоит из координат х, у, z и w. К чему так много координат, когда для представления точки в окне достаточно х и у? Причина заключается в том, что если вершина окажется очень близко к камере, то преобразование, отображающее координаты на плоскость вида, приведет к состоянию, близкому к делению на ноль, — разумеется, это нежелательно. Использование однородного вектора для представления результата позволяет избежать деления на ноль (поверьте мне на слово или покопайтесь в справочниках). При наличии однородного вектора мы вычисляем координаты точки в окне делением координат х, у и z на величину w. Если значение w равно нулю, мы считаем, что результат совпадает с точкой 0, 0. Но для чего нам нужно значение z? Разве точка на экране может обладать координатой z? Нет, не может, однако по координате z можно судить о положении на оси z той грани, которую мы проектируем на плоскость вида. С помощью этой информации мы выберем из множества возможных граней, содержащих точку попадания, ту, что находится ближе остальных.


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

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

Рисунок, 7-4. Проверка принадлежности точки многоугольнику

Проверка попадания

/b> ily Глава 7. Проверка попадания

Сначала я поступил, немного глупо и решил написать код проверки самостоятельно. Я провел несколько часов, стараясь учесть все возможные случаи, и довольно близко подошел к ответу, когда здравый смысл все же взял верх. Я подумал, что на эту задача наверняка уже есть ответ и что найти его, вероятно, нетрудно. И оказался прав! Я отыскал нужный код в серии книг «Graphic Gems» (Academic Press) и приготовился вставить его в свою программу, когда меня посетила еще одна мысль. Вдруг такая функция уже имеется в Windows? После быстрого просмотра Microsoft Developer Library я нашел то, что искал, и окончательный вариант функции проверки (которая находится в файле SdMath.cpp) выглядит следующим образом:

BOOL _3dPointInPolygon(POINT* Points, int nPoints,

CPoint pt) {

HRGN hrgn = ::CreatePolygonRgn(Points, nPoints, WINDING);

ASSERT(hrgn) ;

BOOL b = ::PtInRegion(hrgn, pt.x, pt.y);

::DeleteObject(hrgn) ;

return b;

}

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

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


Последнее замечание: реализованный мною код проверки попадания в грань не учитывает направления, в котором она обращена. Таким образом, теоретически можно попасть в грань, повернутую от пользователя и потому невидимую. Это может случиться тогда, когда за невидимой гранью имеется другая, видимая (если допустить, что до этого было обнаружено попадание в объект), поскольку код выбора, входящий в интерфейс, не возвращает объектов, у которых попадания произошли только в «теневые» грани.

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

void CMainFrame::MakeCurrent(C3dShape* pShape) f

HideSelection() ;

m pCurShape = pShape;

Выделение отдельной грани ''до 177

ShowSelectionf) ;

if (m_pCurShape != NULL) { if (m_iHitFace »= 0) {

Status("Selected: %s (%d @ %3.1f,%3.1f,%3 if)"

ni_pCurShape-»GetName () , m_iHitFace, ni_vHitPoint.x, m_vHitPoint.y, m_vHitPoint.z) ;

"PCurShape-»SetFaceColor(m_iHitFace, 1, О, О).

J Q -L SG { '

^ Status ("Selected: %s", m_pCurShape-»GetMame () ) ;

) else {

Status("No selection");

Определение точки попадания

Рисунок. 7-5. Проектирование точки попадания на объект

Проверка попадания

/h2>

Глава/. Проверка попадания

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

// Вычислить положение точки попадания на грани // Подготовить вектор, описывающий точку экрана // В качестве z используется среднее значение. sv.x = pt.x;

sv.y = pt.y;

sv.z = dHitZ;

sv.w = 1.0;

// Привести к мировым координатам m hr = pIViewport-»InverseTransform ( &wv, &sv) ;


ASSERT(SUCCEEDED(m_hr)) ;

// Привести к локальным координатам фрейма m_hr = m_pIFrame-»InverseTransform(&lv, &wv);

ASSERT(SUCCEEDED(m_hr));

// Вернуть результат *pvHit == Iv;

Для упрощения кода я воспользовался грубой аппроксимацией значения z для точки на экране. В программе это значение определяется как среднее арифметическое координат z всех вершин, образующих грань проекции. На самом деле такой способ неверен, поскольку в нем никак не учитывается положение точки попадания в многоугольнике. Более корректный способ определения z изображен на Рисунок 7-6.

р

Рисунок. 7-6» Вычисление координаты z точки Р

Проверка попадания

Определение точки попадания ''"Щ 179

Точки А, В и С на Рисунок 7-6 — вершины грани. Соединим точку А с С, а В — с точкой попадания Р. Отношение длин отрезков АХ и ХС используется для вычисления координаты z точки Х по значениям координат z точек А и С. Отношение длин отрезков РВ и ХВ используется для вычисления координаты z точки Р по значениям координат z точек В и X. Оставляю вам завершить рассуждение в качестве домашнего задания.

В приложении Select имеется команда меню, которая показывает положение точки попадания на объекте (View ¦ Hit Point). Соответствующий фрагмент кода, расположенный в конце функции CMainFrame-:ShowSelection, создает конус и присоединяет его вершину к точке попадания, при этом ориентация конуса задается по нормали к выделенной грани:

// Получить точку попадания на фигуре

// и перейти от локальных координат к мировым

C3dVector vh = m pCurShape-»Transform(m vHitPoint);

// Определить направление нормали к грани ASSERT(m_pCurShape) ;

C3dVector vn = m_pCurShape-»GetFaceNormal (m iHitFace);

// Изменить длину нормали, прибавить нормаль к точке // попадания и преобразовать в мировые координаты C3dVector vx = vn * 0.5 + m vHitPoint;

vx = m pCurShape-»Transform (vx) ;

// Направить вершину конуса в точку попадания m_pHitPtr = new C3dShape;

m_pHitPtr-»CreateCone (vh.x, vh.y, vh.z, 0, FALSE, vx.x, vx.y, vx.z, 0.1, TRUE);

// Присоединить конус к фигуре m_pCurShape-»AddChild(m_pHitPtr) ;


}

}

Чтобы правильно определить положение конуса, необходимо привести локальные координаты объекта к системе мировых координат. На Рисунок 7-7 показано, как выглядит экран с конусом (кроме того, на цветной вкладке имеется изображение конуса, указывающего на выделенную сферу).

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

Проверка попадания на практике

Чтобы показать, как проверка попадания используется на практике, я создал приложение Blobs, в котором можно нарисовать на экране потрясающее космичес-

/b> ЦУ Глава 7. Проверка попадания

Рисунок. 7-7. Конус, показывающий положение точки попадания на объекте

Проверка попадания

кое существо, наподобие изображенного на Рисунок 7-8 (более качественная иллюстрация из приложения Blobs приведена на цветной вкладке).

Приложение Blobs

Проверка попадания

Приложение добавляет новое «пятно» к объекту, в точке где вы щелкаете мышью. Пятна на самом деле являются сферами, состоящими из четырех полос;

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

/h2>

Проверка попадания на практике

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

void CMainFrame::OnSelChange(C3dShape* pShape, CPoint pt,

void* pArg) {

// Получить указатель на объект класса

CMainFrame* pThis = (CMainFrame*) pArg;

ASSERT(pThis);

ASSERT(pThis-»IsKindOf (RUNTIME_CLASS(CMainFrame))) ;

if (pShape) {

// Определить, в какую грань пришлось попадание C3dViewport* pViewport =

pThis-»m_wnd3d.GetStage () -»GetViewport () ;

int iFace;

C3dVector vHit;

if (pShape-»HitTest (pt, pViewport, &iFace, &vHit) ) ( pThis-»AddBlob (pShape, vHit) ;

}

l

}

Как видно из листинга, после определения точки попадания вызывается функция AddBlob, которая присоединяет к фигуре новое пятно:


void CMainFrame::AddBlob(C3dShape* pShape, C3dVector& vHit) {

// Определить точку попадания

// и перейти от локальных координат к мировым

ASSERT(pShape) ;

C3dVector vh = pShape-»Transform (vHit) ;

// Создать новое пятно C3dShape* pBlob = new C3dShape;

pBlob-»CreateSphere (0.5, 4);

// Установить его центр в точке попадания pBlob-»SetPosition(vh) ;

// Присоединить пятно к фигуре pShape-»AddChild (pBlob) ;

// Включить новое пятно в список фигур,

// чтобы можно было проверять попадание в него

m_pScene-»m_ShapeList. Append (pBlob) ;

}

/b> Уу Глава 7. Проверка попадания

Координаты точки попадания приводятся к мировым координатам. Мы создаем новое пятно, задаем его положение и присоединяем к текущей фигуре. Обратите внимание на то, что новый объект (пятно) присоединяется к списку фигур, тем самым мы обеспечиваем его удаление при уничтожении всего макета. Помните, что для правильной работы проверки попаданий нельзя удалять использованные объекты C++.

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

Итоги

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

Расставляем декорации

Глава 2 Расставляем декорации
Структура приложения

Расставляем декорации

Основные принципы архитектуры, выбираемые в начале проекта, нередко оказывают значительное влияние на его развитие. Неудачная структура может привести к тому, что ваш проект станет «обрастать бородавками» быстрее, чем вы будете их удалять. Например, при написании книги «Animation Techniques for Win32» я еще не обладал достаточным опытом программирования на C++ и работы с Microsoft Visual C++ и библиотеками MFC. В начале работы над примерами я совершил то, что сейчас считаю своей грубой ошибкой: воспользовался Visual C++ для построения однодокументного (SDI) приложения и решил, что мне удастся как-нибудь приспособить его для своих целей. Я сделал это лишь потому, что на тот момент приходилось выбирать между однодокументным и многодокументным (MDI) типами приложения, а MDI-приложение явно не подходило для воспроизведения игровой анимации. Сейчас я ясно понимаю, что мог бы существенно упростить все свои примеры, если бы отказался от принятой в Visual C++ метафоры «документ/вид» и воспользовался простым окном с меню.
На этот раз я решил, что структура моих приложений-примеров должна быть проще и ближе к той, которая может понадобиться при разработке компьютерной игры (это вовсе не означает, что она не годится для более серьезных целей — просто я отказался от использования метафоры «документ/вид» для упрощения программы).
В сущности, как мы вскоре убедимся, используемый нами оконный объект может выступать в роли главного окна приложения, как это было в примере
Структура приложения 'Чр!' 33
Basic, или в роли дочернего окна, как в приложении Stage. Можно даже переключиться в полноэкранный исключительный режим, взять на себя все управление экраном и установить другой видеорежим вместо того, в котором находилась Microsoft Windows перед началом работы приложения. Хотя подобная структура приложения значительно проще той, которую я использовал раньше, она оказывается достаточно гибкой и подходит для самых разных целей.
Я не предлагаю вам создавать реальные программы на основе нашего примера. Тем не менее он может пригодиться для экспериментов с новыми идеями. Библиотека 3dPlus проектировалась для обучения, а не для создания коммерческих продуктов. Если вы захотите применить ее при реальной разработке, придется добавить немалый объем кода для обработки ошибок, исключений и т. д.

Для построения базовой структуры приложения в Visual C++ следует выполнить следующие действия:

1. Воспользуйтесь Visual C++ MFC AppWizard для создания однодокументного приложения, в котором отключена поддержка баз данных, поддержка OLE и вывода на печать. В результате получается самое простое оконное приложение, создаваемое с помощью AppWizard. Я назвал свой проект Stage. Вы можете выбрать как статическую компоновку библиотек MFC, так и динамическую, с использованием DLL. Во всех своих примерах я пользовался DLL, чтобы сократить размер ЕХЕ-файла.

2. Исключите из проекта файлы классов документа и вида. В моем случае это были файлы StageDoc.h, StageDoc.cpp, StageView.h и StageView.cpp. Удалите эти файлы из каталога проекта. У вас остаются три программных файла на C++: Stage.cpp, MainFrm.cpp и StdAfx.cpp.

3. Отредактируйте исходные файлы и исключите из них любые ссылки на заголовочные 41айлы классов документа или вида.

4. Вставьте в файл StdAfx.h директивы для включения файлов •^mmsystem.t^ и . Первый из них используется функциями для работы с джойстиком, которые понадобятся нам позднее, а во втором определяются все функции Direct3D.

5. Включите файл <3dplus.h> в StdAfx.h или Stage.h. Я включил его в Stage.h, чтобы при внесении изменений в библиотеку мне не приходилось бы заново строить предварительно компилированный заголовочный файл в приложении, с которым я работаю.

6. В окне диалога Project Settings (команда Build ¦ Settings) поместите библиотеки Direct3D и 3dPlus в список Link. Во все мои примеры включались файлы SdPlusd.lib, d3drm40f.lib, ddraw.lib и winmm.lib. Обратите внимание: проект библиотеки 3dPlus позволяет работать как с отладочной OdPlusd.lib), так и с окончательной версией библиотеки OdPlusd.lib). В своих примерах я пользовался отладочной версией 3dPlus, чтобы вы могли просмотреть все символы, входящие в библиотеку, а при желании — трассировать ее модули. Библиотека d3drm содержит все трехмерные функции, вызываемые в примере. Библиотека ddraw обеспечивает работу DirectDraw, a winmm — ряд мультимедиа-функций для воспроизведения звука.


Подготовка завершена. Нам еще предстоит добавить в наше приложение довольно много программного кода перед тем, как его можно будет откомпилировать и построить, однако делать это придется уже без помощи AppWizard. На Рисунок 2-1 показана структура приложения Stage.

34 1У Глава 2. Расставляем декорации

Рисунок. 2-1. Структура приложения

Расставляем декорации

Блок с пометкой Механизм визуализации Direct3D чем-то напоминает Рим — все дороги ведут к нему. Мы рассмотрим каждый из этих блоков, когда будем описывать процесс взаимодействия классов семейства C3d с механизмом визуализации Direct3D.

Отображение главного окна

Далее необходимо изменить инициализирующий код приложения, чтобы обеспечить создание главного окна. Для этого следует отредактировать функцию CStage::lnitlnstance в файле Stage.cpp. Когда AppWizard строит базовое приложение SDI, он включает в функцию Initlnstance код для создания пустого документа, который, в свою очередь, создает главное окно. Поскольку мы удалили код, относящийся к документу, придется строить главное окно самостоятельно. Новая версия функции Initlnstance выглядит следующим образом:

BOOL CStageApp::Initlnstance()

{

// Стандартная инициализация

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

#ifdef _AFXDLL

Enable3dControls(); // Вызывается при использовании MFC //в виде совместной DLL-библиотеки

#else

Enable3dControlsStatic(); // Вызывается при статической

// компоновке MFC ttendif

LoadStdProfileSettings();// Загрузить стандартные параметры

// из INI-файла (включая MRU) // Загрузить главное обрамленное окно CMainFrame* pFrame = new CMainFrame;

if (!pFrame->LoadFrame(IDR_MAINFRAME,

35

Отображение главного окна

WSJ3VERLAPPEDWINDOW WS VISIBLE)) {

return FALSE;

}

// Сохранить указатель на главное окно

m pMainWnd = pFrame;

return TRUE;

}

Обратите внимание на два важных действия: вызов функции LoadFrame для загрузки и отображения обрамленного окна и сохранение указателя на него в переменной m_pMainWnd. Указатель сохраняется для того, чтобы классы MFC могли передавать сообщения главному окну приложения, тем самым обеспечивая его правильную работу. Кроме того, необходимо отредактировать файл MainFrm.h и объявить конструктор CMainFrame открытым (public) — по умолчанию он является защищенным (protected). Заодно включите в перечень открытых членов CMainFrame объявление переменной C3dWnd m_wnd3d.


Если на этой стадии откомпилировать и запустить программу, на экране появится главное окно с меню и панелью инструментов. Давайте продолжим работу и закончим программу Stage, cpp, чтобы можно было перейти к созданию трехмерного окна.

В файл Stage.cpp осталось добавить две важные функции: Onldle и OpenDocumentFile. Первая из них обновляет изображение в окне во время пассивной работы приложения, а вторая необходима для правильного открытия файлов. Воспользуйтесь ClassWizard и включите в класс CStageApp функцию Onldle, затем отредактируйте ее и приведите к следующему виду:

BOOL CStageApp::Onldle(LONG ICount) {

BOOL ЬМоге = CWinApp::Onldle(ICount);

// Получить указатель на главное обрамленное окно CMainFrame* pFrame = (CMainFrame*) m_pMainWnd;

if (pFrame) (

// Обновить изображение в трехмерном окне

if (pFrame->m_wnd3d.Update(1)) { ЬМоге = TRUE;

} ! return ЬМоге;

}

Функция Onldle вызывается во время периода пассивности приложения. Основной принцип ее работы заключается в том, что, когда нашему приложению нечего делать, функция возвращает FALSE, а управление передается другому приложению. Если же оно должно выполнять полезные действия (скажем, приводить в движение трехмерный макет), возвращается значение TRUE, свидетель-

36 iii^1' Глава 2. Расставляем декорации

ствующее о необходимости выделения дополнительных квантов пассивного времени. Функция C3dWnd::Update, вызываемая из Onldle, возвращает значение TRUE в том случае, если отображаемые объекты существуют, и FALSE, если их нет. При таком подходе наше приложение не станет зря требовать дополнительные кванты в том случае, когда ему нечего рисовать.

Вторая функция, которую необходимо включить в Stage.cpp, OpenDocumentFile, предназначена для работы со списком последних открывавшихся 41айлов в меню. Если добавить в программу код, приведенный в следующем разделе («Модификация главного окна»), но не включить эту функцию, то все будет нормально до тех пор, пока вы не щелкнете на каком-либо имени файла в нижней части меню. В этот момент MFC выдает ASSERT, а ваше приложение останавливается. Жаль, конечно, что работа MFC так тесно привязана к архитектуре «документ/вид», но здесь уж ничего не поделаешь, и нам придется решать те проблемы, которые возникли в тот момент, когда мы удалили из своего проекта 4'>айлы Doc и View и тем самым вмешались в работу AppWizard. К счастью, сделать это несложно. Все, что необходимо, — переопределить функцию CWinApp::OpenDocumentFile (включите эту функцию в класс CStageApp с помощью ClassWizard):


CDocument* CStageApp::OpenDocumentFile(LPCTSTR

IpszFileName)

{

// Функция вызывается при выборе пользователем // строки из меню последних открывавшихся файлоь. // Конкретное возвращаемое значение неважно, оно лишь // должно отличаться от NULL при успешном выполнении // и быть равным NULL при неудаче.

CMainFrame* pFrame = (CMainFrame*) m_pMainWnd;

if (pFrame) {

return (CDocument*) pFrame->OpenFile(IpszFileName);

} else {

return NULL;

} }

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

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

Модификация главного окна

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

Модификация главного окна '^ИЯ 37

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

Создание трехмерного окна

AppWizard включает в функцию OnCreate класса CMainFrame довольно много кода, предназначенного для создания самого окна, панели инструментов и строки состояния. Нам придется добавить специальный фрагмент, в котором трехмерное окно будет создаваться как потомок главного обрамленного окна. Заодно мы создадим исходный макет с трехмерным объектом — по крайней мере, мы сможем проверить, работает ли наша программа. Одно их худших разочарований в жизни программиста — ввести несколько сотен строк программы, откомпилировать и запустить ее лишь для того, чтобы увидеть большое черное окно (вряд ли кто-нибудь при этом станет прыгать от радости). Я привожу текст 41ункции OnCreate за исключением фрагментов, сгенерированных AppWizard, чтобы вы могли увидеть, что же именно мы добавили в нее (полный текст функции находится в проекте Stage на прилагаемом к книге CD-ROM):


int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct) (

// Создать трехмерное окно if (!m_wnd3d.Create(this, IDC_3DWND)) ( return -1;

}

NewScene () ;

ASSERT(m_pScene) ;

// Создать трехмерный объект C3dShape shi;

shi.CreateCube(2) ;

m_pScene->AddChild(&shl) ;

shl.SetRotation(l, 1, 1, 0.02);

return 0;

}

Этот фрагмент во многом напоминает тот, который использовался в программе Basic для отображения исходного макета. Обратите внимание на то, что константа IDC_3DWND включается в проект командой View] Resource Symbols в меню Visual C++. Класс CMainFrame пополнился двумя членами: m_wnd3d и m_pScene. Переменная m_wnd3d была включена нами в MainFrm.h ранее, после редактирования функции CStageApp::lnitlnstance (стр. 35). Переменная m_pScene добавляется следующим образом:

38 ¦¦У Глава 2. Расставляем декорации

class CMainFrame : public CFrameWnd

public:

C3dWnd m_wnd3d;

CSdScene* m_pScene;

};

ПРИМЕЧАНИЕ

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

Теперь в нашем главном окне содержится трехмерное окно и указатель на текущий макет. Возвращаясь к функции OnCreate на предыдущей странице, проследим за последовательностью действий: сначала мы создаем трехмерное окно, затем при помощи функции NewScene строим трехмерный макет (работа этой функции будет рассмотрена ниже), после чего мы создаем куб, присоединяем его к макету и начинаем вращать. Если взглянуть на текст функции C3dWnd::Create в библиотеке 3dPlus, нетрудно увидеть, что она создает трехмерное окно в качестве окна-потомка, а передаваемый при ее вызове указатель this (см. предыдущую страницу) используется для определения окна-родителя. Конечно, достижением программистской мысли это не назовешь, и все же данный факт достаточно важен для понимания основ.


Настройка размеров окна

На моем компьютере установлено разрешение экрана 1280х 1024. Microsoft Windows обладает одной скверной привычкой — по умолчанию она создает громадное окно лишь потому, что у меня установлено большое разрешение экрана. При работе с приложениями, для которых такое большое окно не требуется, я обычно устанавливаю исходный размер окна, включая пару лишних строк в CMainFrame::PreCreateWindow. В приведенном ниже фрагменте задается начальный размер окна 300х350 пикселей:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) {

// Задать исходный размер окна

cs.cx = 300;

cs.су = 350;

return CFrameWnd::PreCreateWindow(cs);

ЧЁЙЙ-Й*

Модификация главного окна "тЩЦ: 39

Функция NewScene

Настало время поближе познакомиться с функцией NewScene, о которой было сказано выше:

BOOL CMainFraitie : : NewScene ()

{

// Удалить макет, если он существует if (m pScene) {

m_wnd3d.SetScene(NULL) ;

delete m_pScene;

m_pScene = NULL;

}

// Создать исходный макет m_pScene = new CSdScene;

if (!m_pScene->Create()) return FALSE;

// Установить источники света C3dDirLight dl;

dl.Create(0.8, 0.8, 0.8);

m_pScene->AddChild(&dl) ;

dl.SetPosition(-2, 2, -5);

dl.SetDirectionfl, -1, 1);

m_pScene->SetAmbientLight(0.4, 0.4, 0.4);

m wnd3d.SetScene(m pScene);

return TRUE;

t

Мы включим функцию NewScene в файл MamFrm.cpp. Она удаляет существующий макет и создает новый, со стандартным расположением источников света. Затем макет присоединяется к сцене, которая является частью трехмерного окна. Экспериментируя с трехмерными объектами, я хочу быть уверенным в том, что они уничтожаются и создаются без всяких проблем. Данная функция позволяет удалить все созданные ранее объекты и заново начать работу со макетом (она принесла большую пользу, когда мой малолетний сын схватил джойстик и загнал все объекты куда-то за пределы экрана).

Пересчет размеров трехмерного окна

Так как AppWizard создал панель инструментов и строку состояния, занимающие место в клиентной области окна, нам необходимо иметь возможность заново вычислить размеры трехмерного окна в том случае, если пользователь перемещает или убирает панель инструментов или же скрывает строку состояния. Воспользуйтесь ClassWizard и создайте функцию CMainFrame::RecalcLayout, которая переопределяет функцию CFrameWnd::RecalcLayout:


void CMainFrame::RecalcLayout( BOOL bNotify) 40 iiy Глава 2. Расставляем декорации

\

/I Заново разместить служебные области

//и поместить трехмерное окно в центр.

// Размещение служебных областей выполняется

// обрамленным окном.

CFrameWnd::RecalcLayout(bNotify) ;

// Определить размеры свободного места //в клиентной области // для размещения трехмерного окна CRect re;

RepositionBars(О,

OxFFFF,

IDC_3DWND,

CWnd::reposQuery,

&rc) ;

if (IsWindow(m_wnd3d.GetSafeHwnd())) f m_wnd3d.MoveWindow(&rc, FALSE);

}

В сущности, данный фрагмент определяет размеры свободного места в клиент-ной области и использует его для размещения трехмерного окна. Более подробные объяснения можно найти в документации по MFC.

Уничтожение окна

Все хорошее когда-нибудь приходит к концу — в том числе и окна. Включая в программу функцию OnDestroy, мы сможем освободить память, занятую нашими объектами (для этого следует вызвать ClassWizard и указать обрабатываемое сообщение WM_DESTROY):

void CMainFrame::OnDestroy() {

CFrameWnd::OnDestroy() ;

// Уничтожить текущий макет m_wnd3d.SetScene(NULL) ;

if (m_pScene) { delete m_pScene;

}

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

Модификация главного окна '''^ 41

Подготовка к отображению трехмерного окна

Одна из самых интересных особенностей механизма визуализации DirectSD заключается в том, что он работает непосредственно с видеопамятью и не пользуется интерфейсом графических устройств Windows (GDI). Следовательно, крайне важно, чтобы механизм визуализации точно знал экранное положение того окна, с которым он работает. Если это положение будет указано неверно, то он либо не станет рисовать вообще, либо, что еще хуже, примется рисовать поверх других окон. Кроме того, механизм визуализации должен знать, активно приложение или нет и получило ли оно какие-либо сообщения, связанные с палитрой. Если приложение переходит в фоновый режим, механизм визуализации должен освободить палитру, чтобы ей могли воспользоваться другие приложения. Все эти требования выполняются функциями, предназначенными для обработки сообщений WM_ACIVATEAPP, WM_PALETECHANGED и WM_MOVE:


void CMainFrame::OnActivateApp(BOOL bActive, HTASK hTask) {

CFrameWnd::OnActivateApp(bActive, hTask) ;

// Сообщить трехмерному окну об изменении состояния m_wnd3d.SendMessage(WM_ACTIVATEAPP, (WPARAM)bActive, (LPARAM)hTask) ;

}

void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd) {

// Сообщить трехмерному окну об изменении палитры m_wnd3d.SendMessage(WM_PALETTECHANGED, pFocusWnd ?

(WPARAM)pFocusWnd->GetSafeHwnd() : 0);

\

void CMainFrame::OnMove(int x, int y) {

CFrameWnd::OnMove(x, y) ;

// Сообщить трехмерному окну о перемещении обрамленного

окна

m_wnd3d.SendMessage(WM_MOVE,

О,

MAKELPARAM(0, 0)) ;

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

42 'ЭДГ Глава 2. Расставляем декорации

Меню File

Остается лишь предусмотреть обработку команд меню File ¦ New и File ¦ Open. Ранее мы уже построили функцию для удаления текущего и создания нового макета, поэтому команда File ¦ New реализуется тривиально (воспользуйтесь ClassWizard для добавления идентификатора объекта ID_FILE_NEW):

void CMainFrame::OnFileNew()

{

NewScene ();

}

Команда File ¦ Open обрабатывается двумя следующими функциями:

BOOL CMainFrame::OpenFile(const char* pszPath) {

// Попытаться открыть файл с фигурой

C3dShape sh;

const char* pszFile = sh.Load(pszPath);

if (!pszFile) return FALSE;

// Создать новый макет NewScene ();

ASSERT(m pScene);

// Присоединить новую фигуру к макету m pScene->AddChild(&sh);

sh.SetRotation(l, 1, 1, 0.02);

// Включить имя в список последних открывавшихся файлов AfxGetApp()->AddToRecentFileList(pszFile) ;

return TRUE;

}

void CMainFrame::OnFileOpen() f

OpenFile(NULL) ;

}

Теперь давайте посмотрим, как работает функция OpenFile. Сначала мы создаем новый объект C3dShape и вызываем его функцию Load. Эта функция либо пытается открыть файл, либо, при отсутствии заданного имени файла, выводит окно диалога, в котором пользователю предлагается выбрать файл. В том случае, если файл имеет правильный формат, код класса C3dShape открывает его и создает трехмерный объект на основании данных из файла. Понятно, правда? Далее мы присоединяем новый объект к макету и приводим его во вращение, чтобы увидеть макет во всей красе. Имя файла заносится в список последних открывавшихся файлов, что облегчает его повторное открытие в будущем (вспомните, что функция OpenFile также вызывается в функции


Модификация главного окна 'vS 43

OpenDocumentFile в Stage.cpp, при выборе пользователем одного из файлов в меню).

Осталось добавить несколько завершающих штрихов, после которых проект Stage будет нормально компилироваться и работать. Прежде всего необходимо инициализировать переменную m_pScene в конструкторе CMainFrame, для этого следует включить в конструктор строку m_pScene = NULL. Кроме того, поскольку функции NewScene и OpenFile не были созданы с помощью ClassWizard, придется вручную добавить их объявления в конструктор CMainFrame в файле MainFrm.h.

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

Окна, ракурсы и устройства

Термин окно обладает множеством значений. В традиционном графическом программировании он соответствует способу проектирования изображения на плоскую поверхность, а у многих из нас он вызовет ассоциации с объектами в знакомой операционной системе с громадным количеством разных API. Как бы лично вы ни понимали этот термин (я не собираюсь никого обижать), он неимоверно перегружен значениями и уступает в этом смысле разве что термину объект. Как бы тщательно я ни подбирал слова при описании механизма визуализации, кто-нибудь все равно не поймет меня или будет настаивать па том, что термин используется неверно. Лично я считаю, что любая общепринятая терминология, будь она трижды неверна, вполне подходит и для меня. То же самое относится и к документации. Если в ней что-то именуется «окном», то я не буду называть это «что-то» иначе. Следовательно, даже если вам не нравится мой подход к описанию механизма визуализации, моей вины в этом нет — термины выбирали другие.

Давайте начнем с самого начала. Окно — объект Microsoft Windows, который обрабатывает сообщения и отображается в приложениях. Ракурсом называется математическое описание того, как набор объектов в трехмерном пространстве отображается в окне. Устройством (device) называется программа, связанная с реальным устройством, отвечающим за работу видеосистемы на вашем компьютере. Чтобы создать трехмерный макет в приложении, необходимо иметь окно, ракурс и устройство. На самом деле с одним устройством может быть связано несколько ракурсов и несколько окон, однако мы построим систему с одним окном, одним ракурсом и одним устройством. Вы управляете работой окна; управление ракурсом и устройством осуществляет механизм визуализации.


GDI и DirectDraw

Давайте посмотрим, что же на самом деле происходит, когда мы открываем окно на рабочем столе Windows. На Рисунок 2-2 изображено окно, помещенное в произвольном месте рабочего стола.

Теперь спустимся на аппаратный уровень и посмотрим на карту памяти видеоадаптера (Рисунок 2-3).

44 lly Глава 2. Расставляем декорации

Окно

Рисунок. 2-3. Карта памяти видеоадаптера

Расставляем декорации

Окна, ракурсы и устройства

Графический вывод в Windows

Расставляем декорации

Например, для того чтобы нарисовать в окне прямоугольник, следует вызвать функцию Rectangle. GDI спрашивает драйвер видеоустройства, умеет ли тот рисовать прямоугольники; если не умеет, GDI «договаривается» с драйвером о каком-нибудь другом способе рисования прямоугольника (построение множества линий или чего-нибудь в этом роде). Затем драйвер устройства обращается к содержимому видеопамяти или, если нам повезло, пользуется аппаратными особенностями видеокарты для непосредственно рисования прямоугольника. Как вы думаете, быстро проходит этот процесс или медленно? Правильный ответ — не очень медленно, но и быстрым его никак не назовешь. Все дело в универсальности GDI, за которую приходится расплачиваться.

Разве не замечательно было бы обойти GDI и драйвер видеоустройства и напрямую работать с видеопамятью? Конечно, это будет гораздо быстрее, но тогда вам придется досконально изучить работу всех видеокарт на планете. Библиотека DirectDraw предлагает идеальный вариант — вы обращаетесь к драйверу видеоустройства с запросом на прямой доступ к видеопамяти, и если драйвер разрешит, вы сможете непосредственно изменять значения пикселей на экране. Если же драйвер не сможет предоставить прямого доступа к видеопамяти, он по крайней мере создаст иллюзию того, что вы работаете с ней, хотя часть работы при этом будет выполняться самим драйвером. Приложение может пользоваться

46 ^ЦУ Глава 2. Расставляем декорации

функциями GDI или функциями DirectDraw в зависимости от своих требований к производительности. При установке DirectDraw процесс графического вывода происходит в соответствии с Рисунок 2-5.


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

Графический вывод с использованием DirectDraw

Расставляем декорации

DirectDraw обладает еще одной важнейшей особенностью. Посмотрите внимательнее на Рисунок 2-3 на стр. 45. Для хранения изображения на экране используется менее половины имеющейся видеопамяти, а весь остаток пропадает даром. Нельзя ли распорядиться им более разумно? Если видеокарта на Рисунок 2-3 обладает аппаратным блиттером (специальная система, предназначенная для выполнения блито-вых операций), то в свободной памяти можно хранить вспомогательные спрайты, текстуры и т. д. Аппаратный блиттер позволит напрямую переносить изображения из внеэкранной видеопамяти в активную. Вам уже не приходится тратить время на пересылку видеоданных по компьютерной шине данных, благодаря чему возрастает скорость графического вывода. DirectDraw управляет свободной видеопамятью и позволяет создавать в ней внеэкранные поверхности или использовать ее любым другим способом. Фрагмент свободной памяти можно даже выделить под вторичный буфер, размеры которого совпадают с буфером главного окна, и построить в нем следующий кадр анимации, после чего воспользоваться исключительно быстрой блитовой операцией для обновления содержимого активной видеопамяти и смены изображения.

Пересылка видеоданных

Чтобы понять, почему данные в пределах видеопамяти копируются значительно быстрее, чем из основной памяти, необходимо понимать, как работает аппаратная часть компьютера. На Рисунок 2-6 изображена упрощенная модель работы основных компонентов видеосистемы компьютера.

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


Окна, ракурсы и устройства ^1^' 47

Рисунок. 2-6. Примерная архитектура видеосистемы

Расставляем декорации

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

На самом деле пересылку данных в видеопамяти можно ускорить и дальше. Если ваше приложение будет работать в полноэкранном исключительном режиме (full-screen exclusive mode), видеокарта сможет переключаться между двумя страницами видеопамяти, благодаря чему анимация достигает производительности, присущей разве что играм для DOS.

Так какое же отношение все сказанное имеет к механизму визуализации? Он должен очень быстро производить графический вывод (прямо в видеопамять или, еще предпочтительнее, с использованием аппаратных средств видеокарты для поддержки работы с трехмерной графикой). К тому времени, когда вы будете читать эту книгу, на рынке уже появятся видеокарты с аппаратным ускорением трехмерной графики, стоимость которых не будет превышать $200. Как же происходит пересылка данных в этих условиях? Механизм визуализации обращается к функциям промежуточного программного уровня (в данном случае — непосредственного режима Direct3D), который сообщает видеокарте о необходимости выполнить ряд примитивных операций по обсчету трехмерной графики. Если видеокарта не может справиться с подобной задачей, необходимые функции эмулируются с привлечением программных драйверов Direct3D. При наличии таких программных компонентов мы получаем новую модель (Рисунок 2-7), при которой любое приложение (не только механизм визуализации) сможет вызвать набор трехмерных функций, которые будут реализованы с максимальной производительностью.


Кто же выигрывает от всего этого? Конечно же, приложения. Отныне ваша программа может выбрать любой путь, который обеспечит ей необходимую производительность при работе под Windows.

48

Глава 2. Расставляем декорации

Рисунок. 2-7. Работа с трехмерной графикой с участием промежуточного уровня Direct3D

Расставляем декорации

Создание устройства и ракурса в приложении Stage

Давайте рассмотрим, какое же место в этой схеме занимает устройство и ракурс. Устройством называется программный компонент механизма визуализации, работающий с промежуточным уровнем Direct3D (см. Рисунок 2-7), а ракурс определяет, каким образом устройство используется при выводе в область видеопамяти, соответствующую окну. Следовательно, единственное назначение создаваемого окна заключается в том, чтобы указать устройству область видеопамяти, с которой оно будет работать. При создании трехмерного окна мы одновременно создаем устройство и указываем, к какому участку видеопамяти оно будет обращаться. Сделать это можно несколькими способами, но мы рассмотрим лишь два из них.

Проще всего вызвать функцию, которая непосредственно создает устройство по логическому номеру (handle) окна. Это очень удобно, поскольку вам совершенно не приходится думать о том, как работает промежуточный уровень Direct3D — вы указываете логический номер окна, а механизм визуализации делает все остальное. Кроме того, можно воспользоваться функциями DirectDraw для выделения памяти под видеобуферы, а также функциями Direct3D для создания Z-буфера (Z-буфером называется специальный видеобуфер, содержащий информацию о «глубине» каждого пикселя изображения). Затем вся эта информация передается механизму визуализации, который и создает устройство.

Разумеется, создать устройство непосредственно по логическому номеру окна гораздо проще, чем возиться с поверхностями DirectDraw, — и все же я пошел вторым путем. В начале работы над библиотекой 3dPlus я действительно создавал устройство по логическому номеру окна. Потом на выходных я разошелся и решил «поиграть» с 4)ункциями DirectDraw. В результате у меня появился набор классов-оболочек для функций DirectDraw, так что создать устройство на основе поверхностей DirectDraw стало ничуть не сложнее, чем по логическому номеру окна. Вероятно, мои слова вас не убедили, поэтому я приведу фрагмент программы, в котором создается объект-сцена в трехмерном окне нашего проекта Stage:


Окна, ракурсы и устройства

49

BOOL C3dWnd::CreateStage()

// Инициализировать объект DirectDraw if (!m_pDD) {

m pDD = new CDirectDraw;

} if (!m_pDD->Create()) return FALSE;

// Установить экранный режим для окна if ( !m_pDD->SetWindowedMode (GetSafeHwnd () ,

m_iWidth,

m_iHeight)) { return FALSE;

}

// Создать объект Direct3D if (!m_pD3D) {

m_pD3D = new CDirect3D;

} if (!m_pD3D->Create(m_pDD)) return FALSE;

// Задать цветовую модель if (!m_pD3D->SetMode(m_ColorModel)) return FALSE;

// Создать сцену if (!m_pStage) {

m_pStage = new C3dStage;

} if (!m_pStage->Create(m_pD3D)) return FALSE;

// Присоединить текущий макет m pStage->SetScene(m_pScene);

return TRUE;

}

Первая половина функции C3dWnd::CreateStage посвящена созданию объектов DirectDraw и Direct3D, предоставляющих основу для рисования трехмерных объектов в окне. Затем мы выбираем оконный режим для объекта DirectDraw (в отличие от полноэкранного режима) и задаем монохромную цветовую модель MONO для объекта Direct3D (цветовые модели рассматриваются в главе 10). Несколько последних строк создают объект C3dStage по объекту DirectDraw и присоединяют текущий макет к сцене. В свою очередь, объект-сцена C3dStage содержит объекты C3dDevice (устройство) и C3dViewport (ракурс), которые отвечают за взаимодействие с компонентами DirectDraw и Direct3D. Кроме того, сцена содержит объект C3dCamera; мы рассмотрим его ниже. Функция, в которой происходит фактическое создание сцены по объекту Direct3D, выглядит следующим образом:

50 ВД!8' Глава 2. Расставляем декорации

BOOL C3dStage::Create(CDirect3D* pD3D)

{

// Удалить существующий макет SetScene(NULL) ;

// Создать новое устройство по поверхностям Direct3D if (!m_Device.Create(pD3D)) return FALSE;

// Задать качество m Device.SetQuality(m_Quality);

// Создать ракурс if (!m_Viewport.Create(&m_Device,

&m Camera,

0,~0,

m_Device.GetWidth(),

m_Device.GetHeight())) ( return FALSE;

}

return TRUE;

Как видите, приведенная выше функция сводится к построению объектов C3dDevice и CSdViewport. Чтобы создать устройство, мы вызываем соответствующую функцию Direct3D и передаем ей указатель на используемые компоненты DirectDraw:


BOOL C3dDevice::Create(CDirect3D* pD3D)

{

if (m_pIDevice) {

m_pIDevice->Release();

m_pI Device = NULL;

}

m_hr = the3dEngine.Get!nterface()->CreateDeviceFromD3D(

pD3D->GetD3DEngine(),

pD3D->GetD3DDevice (),

&m pIDevice) ;

if (FAILED(m_hr)) { return FALSE;

} ASSERT(m_plDevice) ;

return TRUE;

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

Окна,.ракурсы и устройства "^ 51

Раз уж у меня имелась рабочая программа, в которой использовались поверхности, а не логический номер, я решил оставить все без изменений, в качестве примера. Если когда-нибудь вам захочется самостоятельно поэкспериментировать с механизмом визуализации вместо того, чтобы пользоваться 3dPlus, можно подумать о создании устройства по логическому номеру окна. Разумеется, при работе с классами библиотеки 3dPlus можно совершенно не интересоваться их внутренней реализацией и считать их чем-то вроде «черных ящиков». Тем не менее знакомство с основами работы библиотеки 3dPlus поможет при дальнейшем расширении ее функций.

Проекционная система

После завершения краткого экскурса в аппаратную область давайте займемся более абстрактными вещами и рассмотрим работу проекционной системы. Поскольку объекты могут находиться в произвольной точке трехмерного пространства, нам нужно как-то определить, что же будет видно в нашем окне. Говоря на языке фотографов, нам необходимо указать направление камеры и фокальное расстояние линз. Кроме того, ради повышения эффективности необходимо задать две отсекающих плоскости: переднюю и заднюю. Все, что находится дальше задней или ближе передней плоскости, не будет воспроизводиться на экране. Усеченная пирамида, изображенная на Рисунок 2-8, определяет границы видимой области.

Положение передней и задней отсекающих плоскостей задается функциями IRLViewport::SetFront и IRLViewport::SetBack. СОМ-интерфейс IRLViewport применяется для управления ракурсом. Величину угла камеры можно изменить функцией IRLViewport::SetField. При создании объекта C3dStage некоторым параметрам присваиваются начальные значения, а другие остаются как есть. Если вы посмотрите на реализацию класса C3dStage, то увидите, что в нем отсутствуют специальные функции для изменения параметров видимой области, поскольку стандартные значения хорошо подходят для наших примеров (хотя при желании


Рисунок. 2-8. Видимая область

Расставляем декорации

52

Глава 2. Расставляем декорации

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

Чтобы точно определить положение трехмерного объекта на экране, необходимо применить к его вершинам преобразование, которое отображает трехмерные пространственные координаты на двумерные координаты окна. Преобразование координат осуществляется с помощью матрицы размеров 4х4, которая является суперпозицией отдельных преобразовании перспективы, масштабирования и переноса. В сущности, для получения двумерных координат вершины следует умножить вектор трехмерных координат на матрицу преобразования. Если вам захочется поближе познакомиться с теорией, я бы порекомендовал книгу «Computer Graphics Principles and Practice» (см. библиографию на стр. 333) или какой-нибудь другой учебник по компьютерной графике.

Однако на практике все оказывается несколько сложнее (а разве бывает иначе?). В главе 1 я упоминал о том, что для представления иерархии преобразований в механизме визуализации применяются фреймы. Присоединенные к фреймам объекты могут перемещаться (трансформироваться) по отношению к другим фреймам. Для вычисления двумерных координат любого заданного объекта необходимо совместить результаты преобразований всех 4>реймов, расположенных в иерархии выше данного объекта, и таким образом определить окончательное преобразование объекта.

ПРИМЕЧАНИЕ

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

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


Объект C3dStage является корнем иерархического дерева фреймов в проекционной системе нашего приложения. Все остальные фреймы так или иначе являются потомками, присоединенными к фрейму сцены. Фрейм камеры и фрейм текущего макета присоединяются непосредственно к фрейму сцены. Все фреймы,

Проекционная система ''^Ц: 53

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

При создании фрейма сцены камера находится перед сценой и направляется на ее центр. Другими словами, камера располагается в отрицательной области оси Z. На Рисунок 2-9 показано взаимное расположение фреймов камеры и сцены.

Мы еще вернемся к фреймам и их влиянию на положение объекта, когда будем подробнее рассматривать преобразования. А пока давайте рассмотрим, как происходит загрузка фигур из файлов.

Рисунок. 2-9. Фрейм камеры и фрейм сцены

Расставляем декорации

Создание фигур

Последний фрагмент кода, добавленный нами в главное обрамленное окно на стр. 43, предназначался для загрузки объекта C3dShape из файла на диске и его включения в текущий макет. Фигуры будут подробно рассмотрены в главе 4, однако я хочу показать вам, что представляют собой объекты C3dShape, и показать, почему я сделал их именно такими.

DirectX 2 SDK позволяет создавать приложения, работающие с механизмом визуализации. Однако SDK не содержит ни отдельных программ, ни простых функций для создания фигур — предполагается, что у вас имеются собственные

54

Глава 2. Расставляем декорации

средства для построения трехмерных объектов, текстур и т. д. Тем не менее я рассмотрел другой сценарии. Я представил себе небольшую компанию, которая желает оценить механизм визуализации перед тем, как вкладывать средства в инструменты, необходимые для работы с графикой в крупном проекте. Но как можно экспериментировать, не имея возможности создать собственную фигуру? На самом деле SDK все же содержит функции для построения фигур: вы составляете список координат вершин и набор списков лицевых вершин, после чего вызываете функцию для создания фигуры. Я решил, что для неопытного пользователя такой уровень работы с фигурами покажется слишком низким, и потому включил в библиотеку 3dPlus функции для создания распространенных геометрических фигур — кубов, сфер, цилиндров и конусов. Кроме того, я добавил код, облегчающий использование растров (bitmaps) Windows в качестве текстур; на момент написания книги такая возможность отсутствовала в SDK. Но перед тем, как реализовывать все это, я нашел функцию для загрузки фигуры из файла с расширением .X. Поэтому моя начальная реализация класса CSdShape состояла буквально из одного конструктора и функции Load. Даже этот минимальный объем кода позволил мне получить трехмерные объекты для отображения в окне.


В документации по DirectX 2 SDK сказано, что к 4) рейму могут присоединяться визуальные элементы (один и более). Визуальным элементом (visual) называется фигура или текстура, отображаемая на экране. Визуальный элемент не имеет собственного положения; его необходимо присоединить к фрейму таким образом, чтобы при выполнении преобразования он появился в нужном месте окна. Простоты ради я реализовал объекты CSdShape так, что с каждым из них связан ровно один фрейм и один визуальный элемент. Наличие фрейма и визуального элемента позволяет определить положение объекта 3dShape и его геометрическую форму, благодаря чему он становится больше похож на реальный объект. Недостаток такой схемы заключается в том, что если в макет входят 23 совершенно одинаковых дерева, то для создания леса понадобится 23 фрейма и 23 визуальных элемента, а это не очень эффективно. Гораздо лучше было бы создать всего одну фигуру (визуальный элемент) и воспроизвести ее в 23 различных местах. Другими словами, мы бы присоединили один визуальный элемент к 23 разным фреймам и добились существенной экономии памяти за счет данных, необходимых для определения 22 оставшихся фигур.

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

Давайте посмотрим, как устроена функция C3dShape::Load. При этом я познакомлю вас с некоторыми подробностями реализации и на примере продемонстрирую работу с СОМ-интерфейсами механизма визуализации.

const char* CSdShape::Load(const char* pszFileName) {

static CString strFile;

if (!pszFileName ¦¦ !strlen(pszFileName)) {

// Вывести окно диалога File Open CFileDialog dig(TRUE,

Создание фигур '^1 55

NULL, NULL,

OFN_HIDEREADONLY, _3DOBJ_LOADFILTER, NULL) ;

if (dIg.DoModal() != IDOK) return NULL;

// Получить путь к файлу strFile = dlg.m_ofn.IpstrFile,'


} else (

strFile = pszFileName;

}

// Удалить любые существующие визуальные элементы New () ;

// Попытаться загрузить файл ASSERT(m_pIMeshBld) ;

m hr = m_pIMeshBld->Load((void*)(const char*)strFile,

NULL,

D3DRMLOAD_FROMFILE ¦ D3DRMLOAD_FIRST,

C3dLoadTextureCallback,

this) ;

if (FAILED(m_hr)) ( return NULL;

}

AttachVisual(m_pIMeshBld) ;

m strName = "File object: ";

m strName += pszFileName;

return strFile;

}

Функцию Load можно использовать двумя способами. Если вам известно имя открываемого файла, вызывайте ее следующим образом:

C3dShape shape;

shape.Load("egg.x") ;

Если же вы хотите просмотреть файлы и выбрать из них нужный, вызов функции будет выглядеть так:

C3dShape shape;

shape.Load(NULL);

56 lly Глава 2. Расставляем декорации

Если имя файла нс указано, появляется окно диалога, в котором строка-фильтр равна *.х, так что по умолчанию в окне диалога отображаются только те файлы, которые может открыть данная функция. После получения имени открываемого файла вызывается локальная функция New, удаляющая из объекта-фигуры любые существующие визуальные элементы. Поскольку я всегда стараюсь создавать объекты, подходящие для повторного использования, вы можете вызывать функцию Load для объекта 3dShape произвольное количество раз. Мне кажется, что это гораздо удобнее, чем создавать новый объект C++ каждый раз, когда мне захочется поиграть с очередной фигурой.

Истинное волшебство происходит в следующем фрагменте, и его следует рассмотреть поподробнее:

ASSERT(m_pIMeshBld) ;

m_hr = m_pIMeshBld->Load((void*)(const char*)strFile, NULL,

D3DRMLOAD_FROMFILE ¦ D3DRMLOAD_FIRST, C3dLoadTextureCallback, this) ;

if (FAILED(m_hr)) { return NOLL;

}

Сначала мы проверяем, не равно ли NULL значение указателя m_plMeshBld. Подобные директивы ASSERT довольно часто встречаются в коде библиотеки 3dPlus. Затем мы вызываем 4'>ункцию IRLMeshBuilder::Load, которая загружает файл и создает на его основе сетку (mesh). СОМ-интерфейс IRLMeshBuilder предназначен для создания и модификации сеток. Сеткой называется набор вершин и граней, определяющих форму объекта (на самом деле в сетку входит еще кое-что, но на данном этапе такого определения будет вполне достаточно). Данная функция, как и большинство других СОМ-функций, возвращает значение типа HRESULT, в котором передаются сведения о том, успешно ли была вызвана функция. Для проверки значения HRESULT и определения того, успешно ли завершилась данная функция, служат два макроса — SUCCEEDED и FAILED. Эти макросы определяются среди функций OLE и не являются специфичными для Direct3D. Я сделал своим правилом присваивать результаты всех обращений к СОМ-интерфейсам, производимых в библиотеке 3dPlus, переменной m_hr, которая присутствует в любом классе семейства C3d. Если при этом вызов завершается неудачно и функция класса возвращает FALSE, можно проанализировать переменную класса m_hr и выяснить причину ошибки. Подобная уловка не претендует на гениальность, но сильно помогает при отладке.


Переменная m_plMeshBld инициализируется при конструировании объекта C3dShape:

C3dShape::C3dShape() (

m_pIVisual = NULL;

C3dFrame::Create(NULL) ;

ASSERT(m_pIFrame) ;

m_pIFrame->SetAppData((OLONG)this) ;

Создание фигур '"^l 57

m strName = «3D Shape»;

m^pIMeshBId = NULL;

the3dEngine.CreateMeshBuilder(&m_pIMeshBld);

ASSERT(m_pIMeshBld) ;

AttachVisual(m_pIMeshBld) ;

}

Глобальный объект the3dEngine пользуется некоторыми глобальными функциями Direct3D для создания различных интерфейсов трехмерной графики. Чтобы вы не подумали, будто я от вас что-то скрываю, покажу, откуда возникает интерфейс IRLMeshBuilder:

BOOL C3dEngine::CreateMeshBuilder(IDirect3DRMMeshBuilder**

pIBId)

(

ASSERT(m_pIWRL) ;

ASSER'. 'oIBId) ;

m_hr = m_pIWRL->CreateMeshBuilder(pIBld) ;

if (FAILED(m_hr)) return FALSE;

ASSERT(*pIBld);

return TRUE;

Пока я не стану объяснять, откуда берется значение m_plWRL, но вы наверняка уловили общий принцип: обращения к СОМ-интерфейсам мало чем отличаются от вызовов функций объектов в C++. Сходство настолько велико, что я использую префикс р1 для СОМ-интерфейсов. Чтобы понять отличия между ними, давайте посмотрим, что происходит с указателями на СОМ-интерфейсы при уничтожении объекта C3dShape:

C3dShape : : --C3dShape () {

if (m_pIVisual) m_pIVisual->Release() ;

if (m_pIMeshBld) m_pIMeshBld->Release();

m ImgList.DeieteAll () ;

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

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


58 1ЦУ Глава 2. Расставляем декорации

нехорошо перекладывать свою работу на других, однако создание мощной функциональной программы существенно увеличит ее объем, а я стараюсь по возможности упростить свой код, чтобы вам было проще разобраться с ним. Я уже упоминал во вступлении о том, что моя библиотека — не коммерческий продукт, а всего лишь набор примеров. Разработку коммерческой версии я оставляю вам. Если вы хотите научиться создавать мощные классы, которые должным образом обрабатывают исключения, я сильно рекомендую обратиться к книге Скотта Мей-ерса (Scott Meyers) «More Effective C+++: Thirty-Five More Ways to Improve Your Programs and Design» (Addison-Wesley, 1996).

Что же мы узнали в этой главе?

Мы увидели, как устроена базовая структура приложения, которым мы будем пользоваться во всей оставшейся части книги. Кроме того, мы узнали, для чего предназначена библиотека DirectDraw и как трехмерные объекты проектируются на плоский экран. Напоследок мы рассмотрели загрузку трехмерного объекта из файла .X и его отображение в текущем макете. Попутно я познакомил вас с некоторыми принципами функционирования графических библиотек. Если вы захотите получить более подробную информацию о работе библиотек, обращайтесь к документации по DirectX 2.

Разработка большей части примеров в оставшейся части книги начинается с копирования приложения Stage, и вы также можете воспользоваться этим путем. Скопируйте приложение, уберите из него все ненужное — и вы готовы к созданию своего первого макета.

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

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

«Папа, а зачем там вертится чайник?» Я не знал, что ответить. Тем не менее у меня появился повод подумать о том, как много предстоит сделать, если я хочу создать что-нибудь мало-мальски впечатляющее.

Не беспокойтесь, дальше все пойдет легче.

Создание фигур

Глава 4 Создание фигур
Геометрия

Создание фигур

Простите меня. Я знаю, что на обложке книги ни слова не сказано о том, что вам придется заниматься геометрией, но перед тем, как создавать собственные фигуры, необходимо познакомиться с терминологией и посмотреть, какие законы действуют в трехмерном мире. Для начала еще раз взглянем на систему координат.
В механизме визуализации используется левосторонняя система координат (разумеется, проницательный читатель поднимает левую руку и вертит пальцами, показывая, куда направлены оси х, у и z). Если же вы проспали весь разговор о системах координат в главе 1, посмотрите на Рисунок 4-1 — на нем изображена левосторонняя система, которой мы будем пользоваться.
Рисунок. 4-1. Левосторонняя система координат

Создание фигур

Геометрия ^¦ 89
Положительная часть оси у направлена вверх, х — вправо, a z — в глубь экрана, от пользователя.
Разобравшись с направлениями осей, следует договориться о единицах измерения для создаваемых объектов. Единицы измерения — понятие условное. Например, можно измерять размеры создаваемых объектов в километрах. Возможно, это звучит нелепо, но если отодвинуть камеру на несколько километров от таких гигантских объектов, вид будет вполне нормальным — в нашем трехмерном мире нет загрязнения воздуха, которое бы могло испортить картину.
Давайте ненадолго отвлечемся от темы. Знаете ли вы, что одна из проблем, возникавших у астронавтов при выходе в космос, как раз и заключалась в том, что им не удавалось нормально оценить размеры объектов и расстояния до них? Дело в том, что интенсивность света, проходящего через космический вакуум, остается постоянной (тогда как в земной атмосфере свет рассеивается). Следовательно, объекты в космосе выглядят исключительно четко, даже если они удалены от вас на многие километры. Интересно, правда? Так и хочется обратиться в NASA и записаться на курсы астронавтов.
Аналогичнее проблемы возникают и с нашими фигурами, поскольку их размер на экране зависит только от размеров объектов и их расстояния от камеры. Остается лишь произвольно задать положение камеры и по нему определить, какими должны быть размеры объектов. В создаваемых нами макетах камера по умолчанию находится в 10 единицах от начала координат, то есть в точке с координатами О, О, -10. Объект размером в 1 единицу (скажем, куб с единичным ребром), находящийся в начале координат (О, О, 0), при стандартном расположении камеры смотрится на экране вполне нормально. Следовательно, длина, ширина и высота создаваемых нами объектов обычно должна составлять несколько единиц.

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

Рисунок. 4-2. Оси координат в типичном масштабе

Создание фигур

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

90

Глава 4. Создание фигур

Одна вершина, две вершины, получается собачка

Давайте нарисуем собачку. Возьмите карандаш и соедините точки на Рисунок 4-3, начиная с точки 1 и следуя по порядку чисел.

Рисунок. 4-3. Соедините точки

Создание фигур

— Папа, что ты делаешь?

— Изучаю работу механизма визуализации для компьютерной трехмерной графики.

— Как просто — я так тоже могу.

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

Так в чем же суть всех наших рассуждений о собаках и точках? Дело в том, что компьютер умеет рисовать только прямые линии. Если мы захотим создать криволинейную фигуру, придется либо составлять ее из множества вершин, либо изобретать какой-нибудь другой способ для улучшения ее внешнего вида. Мы еще вернемся к рисованию криволинейных фигур в разделе «Создание твердых тел» на стр. 101. А пока достаточно запомнить на будущее определение вершин.


Геометрия ^^ 91

Векторы


Для определения вектора в трехмерном пространстве необходимо указать три координаты: х, у и z. Началом координат нашей системы является точка О, О, О. Рассмотрим точку в левом верхнем углу (и немного в глубь экрана), которая имеет координаты -2, 3, 4. Чтобы определить положение вершины, можно задать вектор, направленный из начала координат в точку трехмерного пространства. На Рисунок 4-4 изображен вектор, который определяет точку с координатами -2, 3, 4.

Рисунок. 4-4. Вектор, определяющий точку -2, 3, 4

Создание фигур

Кроме того, с помощью вектора можно определить направление. Например, вектор О, 1, 0 определяет верхнее, то есть положительное направление оси Y (Рисунок 4-5).

В тех случаях, когда вектор используется для определения направления, его длина не имеет значения. Если хотя бы одна из координат вектора отлична от нуля, такой вектор однозначно задает направление. Тем не менее для определения направлений принято пользоваться единичными векторами. Длина единичного вектора равна 1, то есть:

x^+z2 = 1

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

92

Глава 4. Создание фигур

Рисунок. 4-5. Вектор, определяющий направление

Создание фигур

ным вектором? При изменении ориентации объекта, единичные векторы упрощают вычисления с вершинами объекта. Тем не менее, вызывая функции механизма визуализации, вы не обязаны передавать им единичные векторы, поскольку перед использованием вектора он автоматически нормируется. Необходимость нормирования возникает только при проведении ваших собственных вычислений с векторами (другое распространенное применение единичных векторов заключается в определении нормали к плоскости; см. раздел «Нормали» на стр. 95).


Но довольно о векторах. Давайте подведем итог: вектор может определять положение вершины или направление.

Ориентация

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

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

Три вектора однозначно определяют позицию и ориентацию объекта.

Геометрия

93

Грани

В нарисованной вами собачке (Рисунок 4-3 на стр. 91) множество вершин использовалось для определения одной грани неправильной формы. Трехмерные объекты состоят из нескольких граней, причем для компьютера эти грани являются абсолютно плоскими. Чтобы изобразить «гладкую» сферу, потребуется довольно много плоских граней. Грани могут иметь любую форму — от простейших треугольников до сложных многоугольников, так что вам удастся собрать свой трехмерный объект из треугольников, квадратов, пятиугольников и вообще из любых фигур.

Рисунок. 4-6. Три объекта, обращенные в одном направлении

Создание фигур

Для окончательного определения ориентации объекта необходимо задать верхние векторы

Создание фигур

94

Глава 4. Создание фигур

Следует учесть, что независимо от заданной формы граней механизм визуализации во время рисования обычно разбивает многоугольные грани на треугольники. Конечно, если в вашем компьютере установлена какая-нибудь сверхмощная видеокарта вместе с такими же мощными драйверами, она может обойтись без деления и рисовать многоугольники непосредственно на аппаратном уровне. Вы спросите, какое это имеет значение? Если вы захотите построить свою фигуру из каких-нибудь особенных многоугольников, чтобы сократить количество граней и тем самым ускорить ее прорисовку, возможно, разумнее с самого начала собирать ее из треугольников или других многоугольников с более простой формой — все равно ваш компьютер воспроизводит грани как совокупности треугольников. Кроме того, иногда это может приводить к довольно неожиданным эффектам. Если просто задать вершины многоугольника и воспроизвести его в окне, ваш «плоский» многоугольник может обрасти треугольными выступами. Причины такого явления мы рассмотрим чуть позже, в разделе «Создание простых фигур» на стр. 97. А пока попробуем построить наши объекты из треугольников и квадратов и посмотрим, что получится.


Последнее, о чем следует помнить — при задании вершин, определяющих грань, необходимо перечислять их по часовой стрелке (Рисунок 4-8). Это очень важно, поскольку механизм визуализации воспроизводит только одну сторону грани. Ограничиваясь лишь передней поверхностью граней, механизм визуализации избавляется от необходимости рисовать невидимые грани и тем самым экономит время. Будьте внимательны при задании вершин грани и следите за тем, чтобы грань была видна с нужной стороны.

Рисунок. 4-8. Порядок обхода вершин грани

Создание фигур

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

Нормали

Нормалями называют людей, которые не станут заниматься геометрией ради собственного развлечения. Конечно, это шутка — например, я люблю геометрию, но при этом вполне нормален (я уверен в этом, потому что мне так говорили окружающие — честное слово!). Так что же такое нормаль?

Нормалью (по отношению к грани) называется вектор, определяющий ориентацию грани. На Рисунок 4-9 изображена грань вместе с нормалью к ней.

Геометрия '''^il 95

Рисунок. 4-9. Грань и нормаль

Создание фигур

Нормалью грани является вектор, перпендикулярный ее поверхности. Для чего же нужны нормали? Знание ориентации граней может пригодиться при наложении текстур на объект. По нормалям можно определить, как следует ориентировать текстуру для достижения желаемого эффекта. На данный момент это несущественно, поэтому давайте рассмотрим другое применение нормалей.

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


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

Предположим, у нас имеется прямоугольная грань и с каждой ее вершиной связан вектор нормали, как показано на Рисунок 4-10.

. Грань с нормалями вершин 96 ЯП' Глава 4. Создание фигур

Создание фигур

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

Изменим положение нормалей и посмотрим, к" каким последствиям это приведет. На Рисунок 4-11 изображена та же самая грань с другими, неперпендикулярными нормалями*.

. Грань с неперпендикулярными нормалями

Создание фигур

При освещении такая грань выглядит «вогнутой», поскольку направления нормалей изменяются от вершины к вершине так, что углы словно приподнимаются над плоскостью грани.

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

Создание простых фигур

После небольшого экскурса в геометрию давайте займемся написанием программы, создающей несколько простых фигур. Данное приложение называется Shapes и находится в одноименном каталоге. Возможно, во время чтения стоит запустить приложение и посмотреть, как оно работает. На первый взгляд приложение Shapes ничем не отличается от Stage из главы 2, однако стоит щелкнуть в меню, как вы сразу увидите новые команды. Мы рассмотрим их назначение и реализацию. Весь интересующий нас код находится в файле MainFrm.cpp.


Начнем с исключительно простой фигуры — прямоугольника, который имеет четыре вершины, определяющие одну грань. Запустив приложение Shapes, вы найдете в меню Edit несколько команд с названиями фигур. Выберем команду Flat Face (то есть «плоская фигура»). Вот как выглядит ее программная реализация:

void CMainFrame::OnEditInsface() {

// Вставить простую плоскую грань

//и задать список вершин

D3DVECTOR vlist [] = (

На первый взгляд понятие «ненерпендикулярная нормаль» кажется внутренне протинорсчи-liliiM, однако если гопорить о нормалях к псршипам, а нс к граням, то ему можно придать смысл. — Иршчеч. пере».

Создание простых фигур <¦Ц 97

(-2, -2, -2},

t-2, -2, 2>,

{ 2, -2, 2},

{ 2, -2, -2} };

// Получить количество вершин в массиве

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

// Определить вершины, входящие в каждую грань, // в формате:

// количество, вершина!, вершина2 и т. д. int flist [] = (4, О, 1, 2, 3, // 4 вершины, ...

О // Конец списка данных грани );

// Создать для фигуры новый макет NewScene ();

// Создать фигуру по списку вершин // и списку данных грани m pShape = new C3dShape;

m_p3hape-»Create (vlist, nv, flist);

// Присоединить фигуру к макету m_pScene-»AddChild(m_pShape) ;

}

Вершины грани содержатся в массиве векторов vlist. Каждый вектор определяет одну из четырех вершин, лежащих в плоскости у =-2. Прямоугольник имеет размеры 4х4 единицы. Первый вектор определяет ближнюю левую вершину, второй — дальнюю левую, третий — дальнюю правую и четвертый — ближнюю правую. Другими словами, мы обходим вершины по часовой стрелке.

Для определения фигуры используется целочисленный массив с информацией о грани, flist. Каждая грань в списке описывается количеством входящих в нее вершин, за которыми следует индекс каждой вершины в массиве vlist. Список граней завершается нулем. Он может содержать информацию о нескольких гранях, но в нашем случае определяется всего одна. Обратите внимание на то, что вершины пронумерованы в порядке О, 1, 2, 3, что соответствует их обходу по часовой стрелке, начиная с ближней левой, если смотреть на грань сверху. Поскольку грань находится в плоскости у =-2, она расположена ниже камеры (точки О, О, -10) и, следовательно, попадает в кадр.


Чтобы создать фигуру, следует вызвать функцию Create объекта C3dShape и передать ей в качестве аргументов список вершин, их общее количество и список данных грани. Не обращайте внимания на то, что происходит внутри объекта C3dShape, там нет ничего интересного — векторы и данные грани передаются в функцию механизма визуализации, которая и создает фигуру. Затем объект C3dShape присоединяется к макету, чтобы появиться в окне приложения. Если запустить приложение Shapes и выполнить команду Edit ¦ Flat Face, вы увидите что-нибудь похожее на Рисунок 4-12.

98 iy Глава 4. Создание фигур

Рисунок. 4-12. Приложение Shapes с одной плоской гранью

Создание фигур

Обратите внимание на то, что грань выглядит плоской — это означает, что нормали ко всем вершинам имеют одинаковое направление. Вы можете проверить это командой View ¦ Normals, которая рисует возле каждой вершины небольшую стрелку, показывающую направление нормали. На Рисунок 4-13 изображена грань вместе с нормалями.

. Плоская грань с нормалями к вершинам

Создание фигур

99

Создание простых фигур

Теперь давайте с теми же самыми данными создадим новую грань, но на этот раз укажем набор нормалей, направленных к центру грани. Ниже приведена функция для создания вогнутой грани, наподобие изображенной на Рисунок 4-11 на стр. 97:

void CMainFrame::OnEditDishface()

(

// Вогнутая грань D3DVECTOR vlist [] = (

(-2, -2, -2),

(-2, -2, 2),

( 2, -2, 2},

{ 2, -2, -2)

);

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

D3DVECTOR nlist [] = {

( 1, 1, 1),

{ 1, 1, -1),

(-1, 1, -1),

{-1, 1, 1} };

int nn = sizeof(nlist) / sizeof(D3DVECTOR);

int flist [] = (4, О, О, 1, 1, 2, 2, 3, 3,

0 };

NewScene() ;

m_pShape = new C3dShape;

m_p3hape-»Create (vlist, nv, nlist, nn, flist);

m_pScene-»AddChild(m_pShape) ;

1

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


Список данных грани (flist) изменился и теперь содержит количество вершин, за которым следуют пары индексов для вершин и нормалей. В этом случае используется другая версия функции Create объекта C3dShape, которая получает массивы вершин и нормалей, а также данные граней.

Результат выполнения этой функции изображен на Рисунок 4-14. Вы можете увидеть его на экране, для этого следует запустить приложение Shapes и выполнить команду Edit ¦ Dished Face.

Вы обратили внимание на то, что грань кажется вогнутой в середине? (Этот эффект особенно четко проявляется, если привести грань во вращение.) На случай, если вы забыли, напомню, что источник света находится в левом верхнем углу макета. С помощью приложения Shapes можно также убедиться в том, что

/b> lll^ Глава 4. Создание фигур

нормали, направленные за пределы грани, создают иллюзию выпуклости. Для того чтобы увидеть этот эффект, достаточно выполнить команду Edit ¦ Bulging Face.

Рисунок. 4-14. Грань с неперпендикулярными нормалями

Создание фигур

Создание твердых тел

Довольно об отдельных гранях! Давайте немного изменим нашу программу и создадим куб:

void CMainFrame::OnEditDefcube() (

D3DVECTOR vlist [] = {

{-1, -1, -1),

{-1, -1, 1},

{ 1, -1, 1},

{ 1, -1, -1),

{-1, 1, -1),

{-1, 1, 1),

{ 1, 1, 1),

{ 1, 1, -1) };

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

int flist [] = (4, 0, 3, 2, 1,

4, 3, 7, 6, 2,

4, 4, 5, 6, 7,

4, 0, 1, 5, 4,

4, 0, 4, 7, 3,

4, 2, 6, 5, 1,

n

Создание твердых тел

/b>

};

NewScene() ;

m_pShape = new C3dShape;

m_pShape-»Create (vlist, nv, flist) ;

m_pScene-»AddChild(m_pShape) ;

} .

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


Рисунок. 4-15. Нумерация вершин куба

Создание фигур

Прежде, чем читать дальше, следует самостоятельно убедиться в том, что вы поняли смысл описания граней. Может, на это придется потратить немного времени, зато в дальнейшем вы не будете создавать невидимые поверхности, ориентированные в неверном направлении. Функция OnEditDefcube строит фигуру, изображенную на Рисунок 4-16. Чтобы увидеть куб на экране, выполните команду Edit] Default Cube.

Как вы думаете, почему закраска граней выглядит такой однородной? Выполните команду View ¦ Normals, и вы увидите, что нормали к вершинам куба обращены от углов, по направлению от центра куба. Поскольку мы не стали задавать свои нормали, механизм визуализации создал их по умолчанию, усредняя нормали всех граней, прилегающих к вершине. Подобное решение может показаться до-

/h2>

Глава 4. Создание фигур

Рисунок. 4-16. Вращающийся куб с принятым по умолчанию расположением нормалей

Создание фигур

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

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

void CMainFrame::OnEditFlatfacecube() {

D3DVECTOR vlist [] = {

(-1, -1, -1),

(-1, -1, 1),

( 1, -1, 1),

{ 1, -1, -1),

{-1, 1, -1),

{-1, 1, 1),

{ 1, 1, 1),

( 1, 1, -1} );

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

D3DVECTOR nlist [] = { { i, 0, 0}, < 0, 1, 0), < 0, 0, 1), (-1, 0, 0),

Создание твердых тел

/h2>

{ 0, -1, 0), { 0, 0, -1}

);

int nn = sizeof(nlist) / sizeof(D3DVECTOR);

int flist [] = (4, 0, 4, 3, 4, 2, 4, 1, 4,

4, 3, 0, 7, 0, 6, 0, 2, 0, 4, 4, 1, 5, 1, 6, 1, 7, 1, 4, 0, 3, 1, 3, 5, 3, 4, 3, . 4, 0, 5, 4, 5, 7, 5, 3, 5, 4, 2, 2, 6, 2, 5, 2, 1, 2, 0


};

NewScene();

m_pShape = new C3dShape;

m_pShape-»Create (vlist, nv, nlist, nn, flist);

m_pScene-»AddChild(m_pShape) ;

t

Если запустить приложение и отобразить куб с плоскими гранями вместе с нормалями (сначала выполните команду Edit ¦ Flat-Faced Cube, затем — команду View ¦ Normals), вы увидите нечто похожее на Рисунок 4-17.

Рисунок. 4-17. Куб с плоскими гранями и векторами нормалей

Создание фигур

Итак, теперь мы умеем создавать фигуры с плавными переходами граней (при которых нормали генерируются механизмом визуализации) или с более резкими переходами (при которых нормали задаются программистом). Иногда бывает нужно создать криволинейный объект, на котором присутствуют острые ребра. На Рисунок 4-18 изображен конус с закругленными сторонами и плоским основанием (команда Edit ¦ Cone).

/h2>

Глава 4. Создание фигур

Рисунок. 4-18. Конус

Создание фигур

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

Стороны конуса строятся точно так же, как и наш первый куб на стр. 101 — мы передаем механизму визуализации координаты вершин треугольников, образующих боковую поверхность, и он самостоятельно генерирует нормали, усредняя векторы нормалей для прилегающих граней. Это создает иллюзию кривизны боковой поверхности. Если бы мы просто включили основание конуса в список граней, использованных для создания боковой поверхности, то нормали для основания также были бы определены посредством усреднения нормалей основания и боковых граней. В результате нижняя грань казалась бы выпуклой, а граница между боковой поверхностью и основанием выглядела бы размытой.

Существуют два основных способа для получения плоского основания и резкой границы между основанием и боковой поверхностью. Самое простое, что можно сделать, — это внести основание в список граней с отдельным набором вершин. Разумеется, координаты этих вершин должны совпадать с координатами нижних сторон боковых граней, иначе в фигуре появятся «дырки». Задавая отдельные вершины для нижней грани, мы тем самым указываем, что нижняя грань не имеет прилегающих граней; когда механизм визуализации будет генерировать нормали для вершин основания, он просто использует для этой цели нормаль основания. Такой подход приводит к желаемому результату (плоскому основанию с резким переходом), однако он немного расточителен, поскольку нам приходится задавать лишний набор вершин (в данном случае — 16).


Более разумное решение состоит в том, чтобы использовать для основания конуса те же самые вершины, что и для боковых граней, но при этом задать для них нормали. Именно так работает функция, построившая конус на Рисунок 4-18. Ее преимущество заключается в том, что нам не придется задавать лишние вершины.

Создание твердых тел

/h2>

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

Тела вращения

Тела вращения я впервые увидел в раннем детстве. Мой отец работал токарем, стоял целый день у токарного станка и точил фланцы, стержни, винты с нарезкой, трубы и т. д. (Рисунок 4-19). Токарный станок вращает металлическую заготовку, зажатую в патроне. К заготовке подносится резец, который срезает ненужный материал при вращении заготовки; положение резца определяет радиус детали. Продольное перемещение резца с одновременным вращением заготовки позволяет выточить стержень (гладкий или нарезной). Основная идея моего рассказа заключается в том, что твердый объект можно описать с помощью простой функции, определяющей радиус объекта в любой точке его длины.

Рисунок. 4-19. Токарный станок

Создание фигур

Процесс построения тел вращения мало чем отличается от создания других трехмерных объектов. Необходимо задать набор вершин, определить, к каким граням они относятся, и затем при желании указать нормали. Поскольку тело вращения можно описать функцией зависимости радиуса от продольной координаты, нетрудно создать фрагмент программы, в котором такая функция используется для генерации вершин и данных граней. На Рисунок 4-20 изображено тело вращения, построенное именно этим способом. Чтобы увидеть тело вращения на экране, выполните команду Edit ¦ Solid of Revolution.

Ниже приводится фрагмент приложения Shapes, в котором создается изображенный на рисунке объект:


double RevolveFn(double z, void* pArg) (

if (z « -1.1) (

return sqrt(l — (z + 2)*(z + 2));

} else if (z » 0.8) (

/b> Щ^' Глава 4. Создание фигур

. Тело вращения

Создание фигур

return sqrt(2 - (z - 2)*(z - 2));

} else (

return 0.5;

} }

// Создать тело вращения

void CMainFrame::OnEditSolidr()

{

// Создать объект по заданной функции

NewScene () ;

m_pShape = new C3dShape;

m_pShape-»CreateRSolid(-3.0, 2.2, 0.2, TRUE, TRUE, RevolveFn, NULL, 16);

m_pScene-»AddChiid(m_pShape) ;

// Развернуть объект, чтобы показать его со стороны m_pShape-»SetDirection(0, -1, 0) ;

)

Как видите, в данном случае мы имеем дело с двумя функциями. RevolveFn возвращает значения радиуса для заданной продольной координаты, а функция OnEditSolidr вызывается при выполнении команды меню Edit ¦ Solid of Revolution. Для построения объекта внутри функции OnEditSolidr вызываются функции RevolveFn и C3dShape::CreateRSolid. Аргументы CreateRSolid выглядят несколько необычно, поэтому позвольте мне объяснить их назначение.

Создавая функцию CreateRSolid, я не собирался делать ее универсальной. Вместо того чтобы обрабатывать координаты концов объекта, я решил всегда строить фигуру вдоль оси z. Таким образом, написанная вами функция (в данном

Тепа вращения

/h2>

случае RevolveFn) возвращает радиус объекта как функцию координаты по оси г. Аргументами CreateRSolid являются максимальное и минимальное значения координат по оси z, приращение по оси z, две логические величины, определяющие необходимость замыкания концов фигуры, указатель на функцию радиуса, необязательный аргумент, передаваемый в функцию радиуса, и наконец количество граней в круговой поверхности.

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

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


Давайте посмотрим, как работает функция C3dShape::CreateRSolid. Приведенный ниже фрагмент взят из библиотеки 3dPlus:

// Создание тел вращения

BOOL CBdShape::CreateRSolid(double zl, double z2, double dz, BOOL bClosedl, BOOL bClosed2, SOLIDRFN pfnRad, void* pArg, int nFacets) (

New () ;

ASSERT(pfnRad) ;

ASSERT(dz != 0) ;

int iZSteps = (int)((z2 - zl) / dz);

if (iZSteps « 1) return FALSE;

int iRSteps = nFacets;

if (iRSteps « 8) iRSteps = 8;

double da = _twopi / iRSteps;

// Создать массив для вершин int iVertices °= (iZSteps + 1) * iRSteps;

D3DVECTOR* Vertices = new D3DVECTOR [iVertices];

D3DVECTOR* pv - Vertices;

// Создать массив для данных граней.

// Каждая грань имеет 4 вершины, за исключением торцов.

int iFaces = iZSteps * iRSteps;

int iFaceEntries = iFaces * 5 + 1;

if (bClosedl) iFaceEntries += iRSteps + 1;

if (bClosed2) iFaceEntries += iRSteps + 1;

int* FaceData = new int [iFaceEntries] ;

int* pfd = FaceData;

// Заполнить координаты вершин double z = zl;

/b> ¦¦¦¦1' Глава 4. Создание фигур

double r, a;

for (int iZ = 0; iZ «= iZSteps; iZ++) { r = pfnRadfz, pArg) ;

a = 0;

for (int iR = 0; iR « iRSteps; iR++) {

pv-»x = D3DVAL(r * sin(a));

pv-»y = D3DVAL(r * cos(a));

pv-»z = D3DVAL(z) ;

pv++;

a += da;

} z += dz;

}

// Заполнить список граней

int iFirst = iRSteps;

for (iZ = 0; iZ « iZSteps; iZ++) {

for (int iR = 0; iR « iRSteps; iR++) {

*pfd++ =4; //No. of vertices per face

*pfd++ = iFirst + iR;

*pfd++ = iFirst + ((iR + 1) % iRSteps);

*pfd++ = iFirst - iRSteps +

((iR + 1) % iRSteps) ;

*pfd++ = iFirst - iRSteps + iR;

} iFirst += iRSteps;

} *pfd =0; // Завершить список

// Создать круговую поверхность с автоматической // генерацией нормалей

BOOL b = Create(Vertices, iVertices, NULL, 0, FaceData, TRUE) ;

delete [] FaceData;

FaceData = new int [iRSteps * 2 + 2] ;

D3DVECTOR nvect [] = { {0, 0, 1}, {О, 0, -1}

};

if (bClosedl) { pfd = FaceData;

*pfd++ = iRSteps;

for (int iR = 0; iR « iRSteps; iR++) (

*pfd++ = iR;

*pfd++ = 1;


}

Тела вращения тЩ^ 109

*pfd = 0;

m_hr = m_pIMeshBld-»AddFaces(iVertices, Vertices, 2, nvect,

ULONG*)FaceData, NULL) ;

ASSERT(SUCCEEDED(m_hr)) ;

}

if (bClosed2) { pfd = FaceData;

*pfd++ = iRSteps;

iFirst = iRSteps * iZSteps;

for (int iR = 0; iR « iRSteps; iR++) (

*pfd++ = iRSteps - 1 - iR + iFirst;

*pfd++ = 0;

}

*pfd = 0;

m hr = m pIMeshBld-»AddFaces (iVertices, Vertices, 2, nvect,

(ULONG*)FaceData, NULL) ;

ASSERT(SUCCEEDED(m_hr)) ;

}

delete [] Vertices;

delete [] FaceData;

m strMame = "Solid of revolution";

return b;

}

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

Возможно, вы обратили внимание на то, что фигуре присваивается имя (в данном случае — Solid of Revolution, хранящееся в переменной m_strName). Когда мы будем заниматься выбором объектов в макете, имя сообщит пользователю, какая фигура выбрана им в настоящий момент.

Мы очень кратко пробежались по большому фрагменту программы, и у вас наверняка осталось много вопросов по поводу его работы и назначению отдельных функций. Если вы хотите понять, как устроена функция CreateRSolid, возьмите лист бумаги в клетку, сверните его в трубку и затем представьте себе, что вам потребовалось описать каждое продольное ребро и каждую грань этой решетки. Именно это и происходит в приведенном выше фрагменте, а эксперимент с трубкой описывает мой подход к его написанию. Во фрагменте присутствуют несколько вызовов функции интерфейсов DirecQD, назначение которых можно узнать в документации по DirectX 2 SDK. Заодно найдите в SDK макрос D3DVAL и посмотрите, что он делает.

/b> ЩЦУ Глава 4. Создание фигур

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


Построение ландшафтов

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

Построение ландшафта по решетке

Давайте сначала рассмотрим алгоритм с решеткой. На Рисунок 4-21 изображен пример ландшафта, построенного по решетке из точек со случайной высотой (команда Edit ¦ Landscape 1).

Рисунок. 4-21. Случайный ландшафт, построенный с помощью решетки

Создание фигур

Функция для построения поверхности, образующей ландшафт, напоминает функцию для создания тел вращения на стр. 108. В данном случае мы передаем функцию, которая генерирует высоту точки по значениям х и z. Ниже приведен фрагмент приложения Shapes:

Построение ландшафтов

double LandscapeFn(double x, double z, void* pArg)

{

return -2.0 + (double)(rand() % 100) / 100;

// Создать поверхность, изображающую ландшафт void CMainFrame::OnEditInsland()

{

// Создать поверхность с использованием функции высоты

NewScene() ;

m_pShape = new C3dShape;

m_p3hape-»CreateSurface (-5, 5, Т., -10, 10, 1, LandscapeFn, NULL) ;

m_pScene-»AddChild(m_pShape) ;

}

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

Ландшафт, который вы видите на своем экране, может отличаться от изображенного на Рисунок 4-21. Иногда в изображении вдруг появляются странные пики; это явление обусловлено ошибкой в текущей версии DirectX. Более подробная информация, включающая возможные пути борьбы с пиками, приводится в файле Readme.txt на прилагаемом диске CD-ROM. А пока можно попробовать изменить размер окна, чтобы пик исчез из него.


Построение ландшафта делением граней

От смехотворно простого алгоритма перейдем к более серьезному и посмотрим, как строится поверхность в алгоритме деления граней. На Рисунок 4-22 изображен ландшафт, построенный по новому алгоритму (команда Edit ¦ Landscape 2).

Все грани на Рисунок 4-22 выглядят плоскими и имеют острые края, потому что создавшая их функция пользуется отдельным набором вершин для каждой новой грани. Как мы убедились раньше, в разделе «Создание простых фигур» на стр. 97, для той грани, у которой отсутствуют прилегающие грани, по умолчанию создаются нормали вершин, направления которых совпадают с нормалью к грани. Получившаяся грань выглядит плоской. При желании можно модифицировать программу так, чтобы она генерировала нормали вместе с вершинами, или же немного усложнить код и создавать грани с общими вершинами. Давайте сначала рассмотрим алгоритм, по которому создавалась поверхность на Рисунок 4-22, а затем — исходный текст программы.

Внутри каждой грани поверхности выбирается точка. Я решил определять положение этой точки, усредняя координаты всех вершин грани. Выбрав точку,

/b> ^^?1 Глава 4. Создание фигур

Рисунок. 4-22. Ландшафт, построенный по алгоритму деления гранен

Создание фигур

которой суждено превратиться в отдельную вершину, мы генерируем случайную высоту для новой вершины и создаем новый набор граней. На Рисунок 4-23 показана прямоугольная грань, разделенная в соответствии с алгоритмом.

. Грань, разделенная на четыре новые грани

Создание фигур

После того как будут созданы четыре новые грани, процесс повторяется — четыре новые треугольные грани делятся на новые треугольники, как показано на Рисунок 4-24.

На рисунке довольно трудно понять пространственное положение всех граней, однако вы наверняка уловили общий принцип. Если немного поработать над алгоритмом, он позволит строить довольно реалистичные горы. Давайте посмотрим, как выглядит функция для построения ландшафтов, подобных изображенному на Рисунок 4-22:

Построение ландшафтов

/h2>

Рисунок. 4-22. Грани после повторного деления

Создание фигур


void CMainFrame::OnEditInsland2() (

// Создать исходную фигуру, которая является

// простейшей прямоугольной гранью. double х = 10;

double zl = -10;

double z2 = 20;

D3DVECTOR vlist [] = {

(-x, -4, zl},

{-x, -4, z2},

( x, -4, z2},

{ x, -4, zl) };

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

int flist [] = {4, 0, 1, 2, 3,

0 };

NewScene();

m_pShape •= new CSdShape;

m_p3hape-»Create (vlist, nv, flist);

// Делить грани на более мелкие int iCycles = 5;

double dHeight = 1.0;

while (iCycles-) (

// Получить текущий список граней int nPaces = m_pShape-»GetFaceCount () ;

IDirectSDRMMeshBuilder* pMB = m_p3hape-»GetMeshBuilder () ;

ASSERT(pMB) ;

IDirect3DRMFaceArray* pIFA = NULL;

HRESULT hr;

hr = pMB-»GetFaces (SpIFA) ;

ASSERT(SUCCEEDED(hr)) ;

/h2>

Глава 4. Создание (ЬИГУО

// Создать новую фигуру, к которой // будут добавляться новые грани C3dShape* pNewShape = new C3dShape;

// Перебрать грани из списка

for (int iFace = 0; iFace « nFaces; iFace++) {

IDirect3DRMFace* pIFace = NULL;

hr = pIFA-»GetElement (iFace, • SpIFace) ;

ASSERT(SUCCEEDED(hr)) ;

ASSERT(pIFace) ;

// Получить количество вершин DWORD nVert = pIFace-»GetVertexCount () ;

// Разместить буферы D3DVECTOR* pVert = new D3DVECTOR [nVert];;

// Получить данные вершин

hr = pIFace-»GetVertices (&nVert,

pVert, NULL) ;

ASSERT(SUCCEEDED(hr));

ASSERT(pVert) ;

ASSERT(nVert » 2) ;

// Выделить память для новых списков вершин и граней D3DVECTOR* NewVert = new D3DVECTOR [nVert + 1];

int* NewFaceData = new int [4 * nVert + 1] ;

// Скопировать старые вершины //и определить суммы координат C3dVector vNew(0, О, О);

for (DWORD i = 0; i « nVert; i++) (

NewVert [i] =pVert[i];

vNew.x += pVert[i].x;

vNew.y += pVert[i].y;

vNew.z += pVert[i].z;

}

// Вычислить положение новой вершины // на плоскости грани vNew.x /= nVert;

vNew.y /= nVert;

vNew.z /= nVert;

// Прибавить случайное отклонение высоты double dh " dHeight * (1.0 - ((double)(rand() * 100)) / 50.0);

Построение ландшафтов '•'^Щ 115


vNew.y += dh;

// Добавить новую вершину NewVert[nVert] = vNew;

// Создать данные граней int *pfd = NewFaceData;

for (i = 0; i « nVert; i++) (

*pfd++ = 3;

*pfd++ = i;

*pfd++ = (i+1) % nVert;

*pfd++ = nVert; // Новая вершина

} *pfd = 0;

// Включить новые грани в фигуру pNew3hape-»AddFaces (NewVert, nVert+1, NULL, 0, NewFaceData) ;

// Удалить списки вершин и граней delete [] NewVert;

delete [] NewFaceData;

// Удалить данные вершин delete [] pVert;

// Освободить грань pIFace-»Release () ;

}

// Освободить массив граней pIFA-»Release () ;

pMB-»GenerateNormals () ;

// Примечание: не освобождайте интерфейс // построения сеток!!!

// Удалить старую фигуру и сделать текущей новую. delete m_pShape;

m_pShape = pNewShape;

}

// Присоединить итоговую фигуру к макету m pScene-»AddChild(m pShape) ;

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

/b> 1W Глава 4. Создание фигур

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

Создание леса

Давайте попробуем изобразить на экране сценку из жизни леса: в ветвях весело щебечут птички, журчит ручеек, где-то ухает филин. Неплохая мысль — но для начала придется нарисовать что-нибудь, хотя бы отдаленно напоминающее дерево. На Рисунок 4-25 изображена елка (команда Edit ¦АТгее).

Рисунок. 4-25. Елка

Создание фигур

Конечно, елки бывают и покрасивее, но пока сойдет и такая. Наше дерево состоит из 25 вершин и 19 граней. Если мы хотим создать целый лес из 100 деревьев (из остальных елок сделали бумагу для книги, которую вы читаете), можно включить в макет еще 99 деревьев, похожих на Рисунок 4-25. Самое время подумать о том, как приказать механизму визуализации нарисовать еще 99 деревьев, в точности аналогичных первому, но расположенных в других местах макета.


Помните, что мы говорили о фреймах и визуальных элементах в главе З? Объект C3dShape содержит фрейм, определяющий его положение, размер, ориентацию и т. д., а также визуальный элемент, который по сути дела описывает набор вершин, граней и т. д. для объекта, который мы хотим увидеть на экране. Функция C3dShape::Clone позволяет включить один и тот же визуальный элемент в несколько разных фреймов. Она создает новый объект C3dShape по существующему объекту, но вместо того, чтобы строить для него новый визуальный элемент, она присоединяет к объекту визуальный элемент исходной фигуры. Таким образом, чтобы изобразить лес, можно создать одно дерево и продублировать его 99 раз. Результат изображен на Рисунок 4-26 (команда Edit ¦ A Forest).

Создание леса

/h2>

Рисунок. 4-26. Лес

Создание фигур

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

void CMainFrame::OnEditForest() {

NewScene();

// Создать первое дерево вместе со стволом m_pShape = new C3dShape;

m_pShape-»CreateCone (0, О, О, 1, TRUE, 0, 4, О, О, FALSE) ;

C3dShape trunk;

trunk.CreateRod(0, -2, О, О, О, О, 0.2);

m_pShape-»AddChild(&trunk) ;

// Присоединить дерево к макету m_pScene-»AddChild(m_pShape) ;

// Получить позицию и ориентацию ствола // по отношению к родителю (дереву) C3dVector p, d, u;

trunk.GetPosition(p) ;

trunk.GetDirection(d, и);

// Дублировать дерево 99 раз for (int i = 0; i « 99; i++) {

// Дублировать крону и ствол

/h2>

Глава 4. Создание фигур

C3dShape* pTree == m_pShape-»Clone () ;

C3dShape* pTrunk = trunk.Clone();

pTree-»AddChild(pTrunk) ;

// Задать относительную позицию ствола //по отношению к кроне pTrunk-»SetPosition (p) ;

pTrunk-»SetDirection(d, u) ;

// Присоединить дубли как потомков первого дерева, // чтобы можно было вращать весь лес m_pShape-»AddChild(pTree) ;

// Задать положение нового дерева в макете

pTree-»SetPosition( ( (double) (rand () % 100) / 5)

- 10.0,

0,

(double)(rand() % 100) / 5,


ni_pScene);

// Удалить контейнеры delete pTrunk;

delete pTree;

} )

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

Второй важный момент заключается в том, что деревья-дубликаты становятся потомками самого первого дерева, чтобы можно было вращать весь лес с помощью одной переменной m_pShape (первое дерево). Поскольку дубликаты являются потомками первого дерева, их позиция должна быть задана по отношению ко всему макету. Если удалить из вызова pTree-»SetPosition необязательный аргумент-фрейм (m_pShape), позиция будет указываться по отношению к родителю, и деревья расположатся неверно. Проверьте!

Довольно о фигурах

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

Спрайты

Глава 9 Спрайты
Рисунок. 9-1. Картонные фигурки в макете

Спрайты

Ради правдоподобия фигурки на Рисунок 9-1 стоят на подставках. Любой ребенок скажет вам, что без подставок фигурка немедленно упадет. У наших спрайтов нет никаких подставок, но они будут стоять вполне нормально.
Давайте прикинем, как будет выглядеть макет с картонными фигурками на Рисунок 9-1 для зрителя. Если наблюдатель постоянно находится перед макетом, то персонажи будут смотреться вполне нормально, причем дальние фигурки будут казаться меньше ближних. Возможный вид макета изображен на Рисунок 9-2.
/h2>
Рисунок. 9-2. Вид спереди для макета, изображенного на Рисунок 9-1

Спрайты

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

/b>

Глава 9. Спрайты

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

До настоящего момента мы не рассматривали прозрачных изображений, потому что большинство трехмерных объектов представляют собой твердые тела, и при их раскрашивании от прозрачности не будет никакого прока. Прозрачность реализуется достаточно просто — один из цветов назначается «прозрачным», и во время воспроизведения все пиксели данного- цвета просто не копируются из источника (обычно текстурного покрытия) в приемник (буфер воспроизведения).

Текстуры не обладают лишь одним атрибутом, необходимым для создания спрайтов — определенным положением в макете. Эту проблему можно решить, присоединяя текстуру к фрейму. Фрейм предоставляет информацию о положении, а текстура — визуальный компонент.

Механизм визуализации особым образом работает с текстурами, присоединенными к фреймам. Они всегда воспроизводятся в плоскости ракурса — и потому они гарантированно обращены лицевой стороной к наблюдателю. Кроме того, текстуру можно масштабировать в зависимости от ее удаленности по оси г. Положение текстуры выбирается таким образом, чтобы базовая точка текстуры (выбираемая произвольно) находилась в определенной точке фрейма. По умолчанию базовая точка растрового изображения расположена в левом верхнем углу, поэтому при позиционировании текстуры левый верхний угол должен находиться в точке фрейма с координатами х, у, z. Как мы увидим позднее, базовую точку можно переместить — например, в середину нижнего края изображения, и такое ее положение будет более логичным при размещении персонажей в макете. На Рисунок 9-3 изображены: стандартное положение базовой точки (слева) и более логичный вариант (справа).

Рисунок. 9-3. Различные положения базовой точки при размещении спрайта

Спрайты


Реализация спрайтов

/h2>

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

Работа с несколькими изображениями

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

Тривиальное решение

Самый очевидный способ создания спрайта с несколькими изображениями заключается в том, чтобы создать для каждой фазы отдельное изображение в отдельном растровом файле и загружать их в массив текстур, как показано на Рисунок 9-4.

Рисунок. 9-4. Использование нескольких отдельных изображений для создания спрайта

Спрайты

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

/h2>

Глава 9. Спрайты

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


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

Лучшее решение

Я предпочитаю создавать спрайты по одному растровому изображению, в котором хранятся все возможные состояния спрайта. Работа с одним растром во многих случаях оказывается более удобной, поскольку при этом очень мало избыточных данных. Например, на Рисунок 9-5 показано растровое изображение с различными фазами спрайта бегущей собаки, использованного в одном из приложений книги «Animation Techniques in Win32» (Microsoft Press, 1995).

Рисунок. 9-5. Полоска из нескольких изображений, используемых при создании спрайта с несколькими фазами, объединенных в одном растре

Спрайты

Работа с несколькими изображениями '''lit 221

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

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

Создание текстуры по полоске изображений

Следующий вопрос: как же создать текстуру по полоске изображений?

Для того чтобы создать текстуру, необходимо присоединить изображение к структуре данных, которая описывает работу с изображением при воспроизведении текстуры. Изображение состоит из заголовка и собственно графических данных. Связь между ними изображена на Рисунок 9-6.


Рисунок. 9-6. Строение текстуры

Спрайты

Сначала я решил, что самый естественный способ сменить фазу спрайта — просто обновить значение указателя в заголовке, чтобы он ссылался на другой набор графических данных. К сожалению, на момент написания книги механизм визуализации требовал, чтобы адрес графических данных оставался неизменным. Следовательно, вместо того чтобы изменять указатель, нам придется обновлять данные. На практике это означает, что в вашей полоске изображений должен присутствовать один пустой кадр, используемый в качестве рабочей области. В этом кадре будет создаваться исходная текстура. Если нам понадобится изменить вид спрайта, мы просто копируем нужные графические данные в рабочую область и сообщаем механизму визуализации об изменении текстуры. На Рисунок 9-7 показано, как это происходит.

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

/b> ЩУ Глава 9. Спрайты

Создание текстуры на основе растра с несколькими кадрами

Спрайты

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

ПРИМЕЧАНИЕ

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

Создание растровых изображений

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

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


Для создания изображений я воспользовался белой картонной ширмой и фотографическим прожектором. Поставив игрушку на ширму, я установил камеру так, как показано на Рисунок 9-8.

Создание растровых изображений 'i^ 223

Рисунок. 9-8. Домашняя киностудия

Спрайты

Я придал игрушке агрессивную позу, заснял ее и перенес изображение с видеокамеры на компьютер. Затем я передвинул игрушку в новое положение и сделал новый снимок. Это повторялось до тех пор, пока у меня не появился полный набор кадров с игрушкой, размахивающей руками с явно недружелюбными намерениями. Изображения были сняты в виде 24-битных растров в разрешении примерно 300х200 пикселей.

Я обрезал и масштабировал все изображения до размеров 256'<256 пикселей, а затем перевел их в 8-битный формат (256 цветов) с произвольной палитрой. После того как все изображения были преобразованы, я скопировал одно из них в качестве заполнителя для рабочей области в финальном растре. Затем я присвоил рабочей области имя SOO, а кадры назвал S01, S02 и т. д.

Далее я воспользовался программой, написанной мной для книги по анимации, для того, чтобы взять серию изображений, преобразовать их к единой палитре и построить полоску со всеми изображениями. Программа называется Viewdib и находится в каталоге Tools. В результате получился растровый файл, напоминающий Рисунок 9-5, за исключением того, что верхний кадр в нем представляет собой копию одного из других кадров.

Затем я удалил все посторонние детали (тени, отражения и т. д.), чтобы оставить чистое изображение игрушки, и заполнил окружающий фон черным (прозрачным) цветом. Разумеется, это значит, что в вашем изображении не может использоваться черный цвет. Если вы хотите использовать черный в качестве прозрачного цвета и при этом ваше изображение должно содержать черные участки, замените черный цвет в изображении другим оттенком (например, вместо RGB: 0, 0, 0 можно использовать RGB: О, О, 1). Наконец, я воспользовался Microsoft Imager, чтобы сократить количество цветов с 256 до 8. Результат изображен на Рисунок 9-9.


/b> ЦЩ^ Глава 9. Спрайты

Изображения для составного спрайта

Спрайты

ПРИМЕЧАНИЕ

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

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

Класс C3dSprite

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

/b>

Создание растровых изображений

кода находится в классе C3dlmage, а другая — в классе C3dSprite. Класс C3dlmage содержит функции для загрузки растрового изображения и деления его на кадры. Для работы с текстурами, необходимыми для создания анимационного спрайта, класс C3dSprite пользуется услугами класса C3dTexture, производного от C3dlmage. На самом деле здесь нет ничего сложного, поэтому давайте рассмотрим процесс создания спрайта и на ходу заполним возможные пробелы. Класс C3dSprite определяется в 3dPlus.h следующим образом:

class C3dSprite : public C3dFrame

{

public:

DECLARE_DYNAMIC(C3dSprite) ;

C3dSprite() ;

virtual --C3dSprite () ;

BOOL Create(C3dScene* pScene,

double x, double y, double z, double scale, UINT uiIDBitmap, int iPhases = 1);

BOOL SetPhase(int iPhase) ;

int GetNumPhases() {return m_Tex.GetNumPhases (); } int GetPhasef) {return m_Tex.Get Phase();}

protected:

C3dTexture m_Tex;

};

Обратите внимание — класс C3dSprite является производным от C3dFrame, и его членом является объект C3dTexture. Мы подробно рассмотрим две функции, Create и SetPhase, поскольку именно они выполняют основную работу объекта-спрайта. Функция Create спроектирована так, чтобы включение спрайта в макет происходило как можно проще. Давайте подробно рассмотрим ее, шаг за шагом.

BOOL C3dSprite::Create(C3dScene* pScene,

double x, double у, double z, double scale, UINT uiIDBitmap, int iPhases) (

ASSERT(pScene) ;

ASSERT(iPhases » 0);


ASSERT(uiIDBitmap) ;

// Создать фрейм и включить его в список фигур макета C3dFrame::Create(pScene) ;

pScene-»m_ShapeList .Append (this) ;

// Установить фрейм в заданное положение SetPosition(x, у, z);

/b> Д¦^ Глава 9. Спрайты

// Загрузить растровое изображение для создания текстуры if ( !m_Tex.C3dIniage: :Load (uiIDBitmap) ) {

TRACE("Failed to load texture image\n");

return FALSE;

}

// Задать количество фаз m_Tex.C3d!mage::SetNumPhases(iPhases) ;

// Создать текстуру по растровому изображению m_Tex.Create ();

// Задать свойства IDirect3DRMTexture* pITex = m_Tex.Getlnterface ();

ASSERT(pITex) ;

// Разрешить глубинное масштабирование m_hr = pITex-»SetDecalScale (TRUE) ;

ASSERT(SUCCEEDED(m_hr)) ;

// Задать исходный размер double a = (double)m_Tex.GetWidth() / (double)m_Tex.GetHeight();

m__hr = pITex-»SetDecalSize (scale * a, scale);

ASSERT(SUCCEEDED(m_hr)) ;

// Разрешить прозрачность m hr = pITex-»SetDecalTransparency(TRUE);

ASSERT(SUCCEEDED(m_hr)) ;

// Назначить прозрачным цветом черный m hr = pITex-»SetDecalTransparentColor(RGB_MAKE(0, 0, 0)) ;

ASSERT(SUCCEEDED(m_hr)) ;

// Перенести начало координат фрейма // в середину нижней стороны m_hr = pITex-»SetDecalOrigin (m_Tex. GetWidth () /2, m_Tex.GetHeight 0-1) ;

ASSERT(SUCCEEDED(m_hr)) ;

// Присоединить текстуру в качестве визуального элемента return AddVisual(&m_Tex) ;

}

Прежде всего необходимо создать объект C3dFrame (базового класса C3dSprite). Мы включаем спрайт в макет и задаем начальное положение фрейма в макете. Далее загружается растровое изображение для текстуры из ресурса BITMAP, добавленного с помощью AppStudio. Обратите внимание на то, что мы вызываем

Создание оастоовых изобоажении ^Sil 227

функцию C3dlmage::Load базового класса, а не функцию класса C3dTexture. Это сделано для того, чтобы иметь возможность слегка изменить изображение перед тем, как создавать по нему текстуру.

После того как изображение загружено, для него задается количество кадров (iPhases). Кадр, используемый в качестве рабочей области, не считается. После того как изображение будет разделено на кадры, мы создаем текстуру. Возможно, код получился некрасивым, но зато он достаточно эффективен и позволяет создавать многофазные спрайты, практически не обращаясь к коду классов текстуры и растрового изображения.


После того как мы получаем указатель на интерфейс IDirect3DRMTexture вызовом функции Getlnterface, необходимо разрешить глубинное масштабирование, чтобы размер спрайта изменялся в зависимости от его положения по оси z. Исходный размер изображения задается в соответствии с масштабным коэффициентом, передаваемым в виде аргумента функции Create. Масштабный коэффициент позволяет задавать размер спрайта независимо от размера растрового изображения. Небольшой дополнительный фрагмент кода обеспечивает сохранение пропорций изображения-оригинала. Поскольку текстуры (и, следовательно, ваши спрайты) разрешается масштабировать в обоих направлениях, при желании вы можете сделать своих персонажей низенькими и толстыми или, наоборот, — высокими и тощими. Думаю, что графические изображения большей частью создаются такими, какими они должны выглядеть на экране, поэтому важно сохранять правильные пропорции.

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

Последним шагом в создании текстуры является перенос начала координат фрейма в середину нижнего края изображения. Мне показалось, что с этой точкой будет удобно работать, особенно тогда, когда спрайт должен стоять «на поверхности земли» — в данном случае для правильного расположения спрайта достаточно присвоить его координате у значение координаты у поверхности.

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

Изменение фазы

Чтобы изменить фазу спрайта, следует вызвать функцию C3dSprite::SetPhase и передать ей номер новой фазы:


BOOL C3dSprite::SetPhase(int iPhase) f

return m_Tex.SetPhase(iPhase);

Работа этой функции сводится к вызову C3dlmage::SetPhase:

/b> Я? Глава 9. Спрайты

BOOL C3dlmage::SetPhase(int iPhase)

{

if ((iPhase « 0) ¦I (iPhase »= m_iPhases)) f return FALSE;

} m iCurPhase = iPhase;

// Скопировать графические данные нужной фазы в рабочую

область

int iBytes = m_rlimg.bytes_per_line * m_rlimg.height;

memcpy(m rlimg.bufferi,

(BYTE*)m_rlimg.bufferl + (m_iCurPhase + 1) *

iBytes,

iBytes) ;

// Известить производные классы об изменениях _OnImageChanged() ;

return TRUE;

}

Приведенный выше фрагмент реализует операцию копирования, изображенную на Рисунок 9-7. Мы вычисляем адрес нужного кадра, затем функцией memcpy копируем данные кадра в рабочую область. После завершения копирования вызывается функция _OnlmageChanged. Эта виртуальная функция класса C3dlmage не делает ничего, однако она может быть переопределена в производных классах (таких, как C3dTexture), которым необходимо сообщать о внесении изменении в изображение. Класс C3dTexture с помощью этой функции, расположенной в файле 3dlmage.cpp, уведомляет механизм визуализации об изменении графических данных текстуры:

// виртуальная функция

void C3dTexture::_OnImageChanged()

{

if (!m pITexture) return; // Возможно, текстура еще не

создана

// Новое изображение — известить механизм визуализации // об изменении графических данных (но не палитры!) m hr = m pITexture-»Changed(TRUE, FALSE);

ASSERT(SUCCEEDED(m_hr)) ;

}

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

Создание растровых изображений ''IfHj 229

Приложение Sprites

Когда я пишу приложение-пример, то стараюсь избегать всего, что не является абсолютно необходимым для раскрытия темы. Стоило мне наладить работу класса C3dSprite в приложении Sprites, как тут же немедленно захотелось добавить в него массу новых возможностей. Тем не менее я устоял перед искушением, и результат моих усилий можно в первом приближении назвать игрой.


Приложение Sprites — это DOOM- образная игра. Если вам еще не приходилось иметь дело с DOOM, ребята из Id Software наверняка с большим удовольствием продадут вам ее (но пожалуйста, не заказывайте игру сейчас, иначе вы никогда не закончите читать эту книгу!)

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

Для этой игры вам понадобится джойстик. Я воспользовался своим SideWinder Pro и настроил его так, чтобы кнопка 1 (гашетка на SideWinder) выполняла стрельбу. Если у вас есть только мышь, вы все равно сможете находить врагов, однако выстрелы придется вообразить. На Рисунок 9-10 показан вид экрана в начале игры — собственно, ничего больше вам знать и не потребуется.

Рисунок. 9-10. Похоже, хозяев нет дома

Спрайты

При разработке этого приложения я воспользовался планом лабиринта, изображенным на Рисунок 9-11 (для самых недогадливых он станет подсказкой).

/b> ¦¦¦И' Глава 9. Спрайты

. План лабиринта

Спрайты

Каждая стена лабиринта состоит из четырех граней, и на каждую грань наложена красная текстура. Для стен я создал класс CWall, производный от C3dShape, код которого мало отличается от того, что мы видели в предыдущих главах, и потому я не стану приводить его. Построение лабиринта с помощью класса CWall происходит следующим образом:

BOOL CMainFrame::NewScene() {

WALLINFO wi [] = {

(-6, 5, 6, 6, 1},

{ 5, -5, 6, 5, 0.9},

{-6, -6, 6, -5, 1.1},

(-6, -5, -5, 5, 0.8},

(-3, 2, 2, 3, 0.5},

( 2, -3, 3, 3, 0.9},

(-4, -1, 0, 0, 1},

(-1, -5, 0, -2, 1.3}

};

int nWalls = sizeof(wi) / sizeof(WALLINFO);

WALLINFO* pwi = wi;

for (int i = 0; i « nWalls; i++) {

CWall* pWall = new CWall(pwi);

m_pScene-»AddChild(pWall) ;


m_pScene-»m_ShapeList .Append (pWall) ;

pwi++;

}

Приложение Sprites

/h2>

Данные для каждой стены состоят из двух наборов угловых координат х и у и высоты. Стены поочередно создаются и включаются в макет.

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

m_pScene-»SetBackground(0, О, 1);

CFloor* pFloor = new CFioor(-6, -6, 6, 6) ;

m_pScene-»AddChild (pFloor);

m_pScene-»m_ShapeList .Append (pFloor) ;

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

Последнее, что необходимо добавить в наш лабиринт, — врагов и взрывы. И снова я создал специальные классы для каждого из этих объектов. Они являются производными от C3dSprite, и поскольку оба класса похожи, мы рассмотрим лишь один из них. Ниже приведено определение класса CSoldier (находящееся в файле maze.h), с помощью которого мы создаем себе врагов:

class CSoldier : public C3dSprite

{

public:

CSoldier(C3dScene* pScene, double х, double z) ;

void Update 0;

>;

Как видите, здесь нет ничего сложного: конструктор создает объект, а функция Update заведует его перемещением. Код конструктора (из файла maze.cpp) выглядит следующим образом:

CSoldier::CSoldier(C3dScene* pScene, double х, double z) (

Create(pScene, х, 0, z, 0.5, IDB_SOLDIER, 4);

Должно быть, выше приведена одна из самых простых функций, которые нам приходилось видеть в этой книге. Функция C3dSprite::Create выполняет всю основную работу по созданию объекта-спрайта. Ей передается идентификатор растрового ресурса. Последний аргумент (4) определяет количество кадров в спрайте (не считая рабочей области).

Поскольку спрайты должны двигаться, нам понадобится функция Update для изменения фазы:

void CSoldier::Update() {

SetPhase((GetPhaseO + 1) % GetNumPhases ());

/b> fli"' Глава 9. Спрайты

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

Постойте, какого еще взрыва?!? А это вы можете выяснить самостоятельно (или в компании врагов).

Довольно о плоских фигурах

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

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

Свет и тень

Глава 10 Свет и тень
Цветовые модели

Свет и тень

Перед тем как приступить к полноценной работе с источниками света, мы сделаем небольшой шаг в сторону и посмотрим, каким образом механизм визуализации генерирует цвета. Для того чтобы определить окончательный цвет пикселя в окне, необходимо принять во внимание следующие факторы:
• Цвет и материал поверхности, находящейся непосредственно под пикселем.
• Угол наклона поверхности по отношению к каждому источнику света в макете.
• Цвет, интенсивность и расположение каждого источника света.
• Возможности физического устройства отображения (дисплея).
Упрощая задачу определения цвета пикселя, мы ускоряем выполнение этой операции, и, соответственно, общую скорость работы механизма визуализации.
Существует несколько способов упрощения этой задачи. Например, можно игнорировать цветовые компоненты света и считать их источниками белого цвета различной интенсивности. Это значительно упрощает расчет цветов и ускоряет производительность без особого вреда для качества. Или возьмем другой пример — мы можем упростить зависимость между свойствами материала и его цветом. Для этого зеркальные и диффузные отражающие свойства материала комбинируются, и по ним определяется индекс в цветовой таблице, содержащей весь диапазон цветов объекта, от чисто зеркального до чисто диффузного. Возможно, эти примеры покажутся излишне упрощенными, однако они показывают, что небольшая доля изобретательности может значительно улучшить производительность работы, хотя и за счет качества изображения. Поскольку чаще всего основной проблемой является именно производительность, с небольшой потерей качества вполне можно примириться.
Приведенные выше примеры показывают, что решить проблему цвета можно несколькими способами. Наверное, разработчикам Direct3D следовало позволить нам, пользователям, самим выбирать методику оптимизации для каждого конкретного случая. Тем не менее они этого не сделали. Нам предлагаются всего два режима: монохромный и RGB. Монохромный режим назван так из-за того, что в нем используются только монохромные (белые) источники света. Мы можем менять интенсивность источника света, но не его цвет. Это заметно упрощает процесс обсчета цветовых оттенков. Кроме того, цвета материалов в монохром-Цветовые модели •Щ 235

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

В RGB-режиме для расчета цвета пикселей используются значительно более сложные вычисления. RGB-режим полностью поддерживает работу с цветными источниками света и свойствами материала и позволяет получить наилучшее возможное качество изображения. Поскольку в этой главе мы будем играть с цветными источниками света, нам придется настроить механизм визуализации на работу в RGB-режиме. Наверняка вы подумали: «Все, о производительности придется забыть!» Не волнуйтесь, дело обстоит не так плохо.

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

• 24-битный дисплей способен отобразить практически любой цвет, сгенерированный механизмом визуализации в виде тройки RGB-компонент. На каждый пиксель необходимо переслать 3 байта данных; в отдельных случаях этот фактор может сказаться на производительности.

• 16-битпь1Й дисплей может отобразить от 32,767 до 65,535 цветов, в зависимости от настройки механизма визуализации. Для каждого пикселя требуется всего 2 байта данных, однако при их генерации приходится выполнить с RGB-компонентами несколько операций сдвига, что также требует некоторых расходов времени.

• 8-битный дисплей (который до сих пор остается наиболее распространенным) отображает всего 256 цветов, причем перед механизмом визуализации встает дополнительная проблема — какой из 256 цветов ему выбрать в каждом конкретном случае? Тем не менее на каждый пиксель в этом режиме приходится пересылать всего 1 байт данных.

Короче говоря, ситуация выглядит следующим образом. При 24-битном дисплее вы получаете потрясающие картинки. При 16-битном дисплее вы получаете хорошее качество с неплохой производительностью, 8-битный дисплей позволяет добиться неплохого качества при хорошей производительности. Основная проблема 8-битных дисплеев заключается в том, что для воспроизведения огромного диапазона цветов RGB-модели при 256-цветной палитре приходится пользоваться методикой смешения цветов (dithering), снижающей общее качество изображения. В монохромном режиме на 8-битном дисплее можно ограничить количество цветов, используемых объектами, и получать все необходимые цвета непосредственно из палитры. При этом вы получаете качественное изображение при хорошей производительности. Чтобы решить, какой режим лучше всего подходит вам, следует немного поэкспериментировать.


Тип цветовой модели задается при создании трехмерного окна. Ниже приведен соответствующий фрагмент кода:

/b> :i^a:i::-' Гпяпя 1П Г!пет u TftHh

int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct)

{

// Создать трехмерное окно

if (!m_wnd3d.Create(this, IDC_3DWND, D3DCOLOR_RGB)) { return -1;

t

Если при вызове функции C3dWnd::Create цветовая модель не указана, по умолчанию принимается значение D3DCOLOR_MONO.

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

Выбор типа освещения

Механизм визуализации DirectSD поддерживает пять разных типов освещения:

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

Тип освещения Положение Направление Точечный источник Предельная дальность Другие параметры
Рассеянный нет нет нет нет нет
Направленный нет да нет нет нет
Параллельно-точечный да нет да нет нет
Точечный да нет да да нет
Зональный да да да да да
Из таблицы видно, что источники света обладают различными свойствами. Для каждого источника света можно задать RGB-значение, которое позволяет управлять как цветом, так и интенсивностью света. Давайте рассмотрим каждый тип освещения и его влияние на макет.

Рассеянный свет

Рассеянный свет обсчитывается проще всего. Он обеспечивает равномерный уровень освещения в макете, при котором все грани объектов освещаются одинаково. На Рисунок 10-1 изображен пример макета, освещенного белым рассеянным светом.

/b>

Выбор типа освещения

Рисунок. 10-1. Макет, освещенный только рассеянным светом

Свет и тень

ПРИМЕЧАНИЕ

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


Вопреки первому впечатлению, эти джентльмены вовсе не собираются грабить банк. «Эффект чулка» вызван сочетанием плоского освещения и смешением цветов, необходимым для отображения объекта на моем 256-цветном дисплее. От одного рассеянного света проку мало, однако в сочетании с другими типами освещения он помогает устранить из макета излишне темные области.

Направленный свет

Направленный свет по простоте обсчета уступает разве что рассеянному. Он освещает макет параллельными лучами из источника, находящегося на бесконечно большом расстоянии от макета. Его ориентация задается с помощью вектора направления. Задавать положение источника направленного света бесполезно. На Рисунок 10-2 изображен макет, освещенный рассеянным светом малой интенсивности в сочетании с направленным светом.

Как видите, макет выглядит значительно лучше, чем с одним лишь рассеянным светом. Освещение на Рисунок 10-2 очень похоже на встречающееся в естественных условиях. Стрелка показывает, откуда падает направленный свет. На Рисунок 10-3 изображен тот же самый макет, но без рассеянного света.

Рисунок 10-3 выглядит слишком контрастным, на нем имеется много темных мест, которые мешают рассмотреть некоторые части объектов.

/b>

Глава 10. Свет и тень

Рисунок. 10-2. Направленный свет используется для получения бликов

Свет и тень

Макет освещен только направленным светом

Свет и тень

Параллельно-точечный свет

Если вы хотите включить в свой макет источник света (например, настольную лампу) и наглядно показать его действие, то наиболее естественным решением оказывается параллельно-точечный источник света. Свет исходит из заданной точки, при перемещении которой изменяется внешний вид макета. Тем не менее, лучи света из такого источника падают параллельно, упрощая все вычисления. Установка параллельно-точечного источника света между двумя головами в нашем макете приводит к результату, изображенному на Рисунок 10-4.

/b>

Выбоо типа освешения

Рисунок. 10-4. Параллельно-точечное освещение

Свет и тень

Как видно из рисунка, источник света (изображенный в виде маленького кубика) освещает головы таким образом, что свет кажется исходящим из точки, в которой находится кубик. По Рисунок 10-4 довольно сложно определить глубину источника света по отношению к головам, но если посмотреть на текст программы (мы сделаем это позднее, на стр. 242), то можно убедиться, что источник света находится немного позади от них.


Точечный свет

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

Я советую немного поэкспериментировать с приложением Lights, чтобы понять отличия этого типа освещения от предыдущего. Впрочем, повышение качества обходится довольно дорого, и по моему мнению, точечное освещение не имеет особых преимуществ перед параллельно-точечным.

Зональный свет

Обсчет зонального освещения оказывается самым сложным, однако оно заметно повышает качество изображения, как можно видеть из Рисунок 10-6 (кроме того, посмотрите на цветную версию рисунка на вкладке).

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

/b>

Глава 10. Свет и тень

Рисунок. 10-5. Точечное освещение

Свет и тень

Зональное освещение

Свет и тень

Выделите немного времени на то, чтобы познакомиться с приложением Lights и посмотреть, как каждый тип освещения влияет на внешний вид макета. В приложении имеются окна диалога, которые позволяют вам регулировать уровень рассеянного света и определять большую часть свойств источников. Кроме того, вы сможете выделять и перемещать источники света в макете, как обычные объекты.

/h2>

Выбор типа освещения

Предельная дальность и затухание

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


Другой параметр, величину которого также можно регулировать, — затухание. Он влияет на убывание интенсивности света с расстоянием. В квадратное уравнение, определяющее интенсивность света, входят три параметра. В документации по DirectSD они названы постоянной, линейной и квадратичной составляющими. По умолчанию им присваиваются значения 1, 0 и 0 соответственно — интенсивность света от такого источника не убывает с расстоянием. Уравнение, определяющее величину затухания, выглядит следующим образом:

а = с + Id + qd2,

где а — величина затухания; с — постоянная составляющая; 1 — линейная составляющая; d — расстояние от источника света, a q — квадратичная составляющая. Для того чтобы изменить затухание источника света, можно воспользоваться функцией C3dlight::SetAttenuation.

Хватит разговоров — давайте программировать

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

Поскольку программная реализация источников света выглядит довольно просто и не сильно отличается для различных типов освещения, я приведу код только для создания параллельно-точечного источника света на Рисунок 10-4.

void CMainFrame::OnEditParlight() {

C3dParPtLight* pLight = new CSdParPtLight;

pLight-»Create (1, 1, 1);

m_pCurLight = pLight;

// Создать фигуру, изображающую источник света C3dShape* psi = new C3dShape;

psl-»CreateCube (0.2) ;

// Присоединить источник света в качестве потомка фигуры, 242 аШУ Глава 10. Свет и тень

// чтобы его можно было выбрать psl-»AddChild(pLight) ;

// Присоединить источник света к макету m_pScene-»AddChild(psl) ;

m pScene-»m_ShapeList. Append (psi) ;

m pScene-»m_ShapeList. Append (pLight) ;

// Поместить источник света на видном месте psl-»SetPosition(0, 1, -2);

psl-»SetName ("Parallel Point Light");

MakeCurrent(psi) ;

}

Большая часть этого фрагмента не имеет никакого отношения к созданию самого источника света, но мы все равно рассмотрим его. В начальных строках функции определяется источник света — в данном случае объект CSdParPtLight. Чтобы показать расположение источника света в макете, я создал фигуру и присоединил к ней источник в качестве потомка, чтобы они перемещались вместе. Классы фигуры и источника света являются производными от CSdFrame. Это позволяет включить их в список фигур макета, чтобы они были удалены во время уничтожения всего макета. Остается лишь задать положение источника света в макете. Для направленного источника света следовало бы задать направление, а для зонального — как направление, так и положение.


Классы C++ для работы с источниками света конкретного типа выглядят очень просто. Они являются производными от класса C3dLight, в котором и создается источник:

BOOL C3dLight::Create(D3DRMLIGHTTYPE type,

double r, double g, double b) {

// Создать фрейм, содержащий источник света if (!C3dFrame::Create(NOLL)) return FALSE;

// Создать объект-источник света ASSERT(m_pILight == NULL);

if (!the3dEngine.CreateLight(type, r, g, b, &m pILight)) {

return FALSE;

} ASSERT(m_pILight);

// Присоединить источник света к фрейму ASSERT (m_J3l Frame) ;

m_hr = m_pIFrame-»AddLight (m_pILight) ;

if (FAILED(m_hr)) return FALSE;

return TRUE;

}

Хватит оазговооов — давайте поогоаммиоовать "lEU 243

Источник света присоединяется к фрейму для того, чтобы мы смогли задать его положение. Источник света, создаваемый механизмом визуализации, не обладает собственным положением или ориентацией, пока он не будет закреплен за каким-нибудь фреймом. Наследование классом C3dLight свойств класса C3dFrame облегчает работу с объектами в макетах.

Цветные источники света

Я довольно долго искал какой-нибудь пример того, как цветное освещение улучшает вид макета, однако после долгих размышлений мне удалось изобрести лишь несколько простейших приложении, не имеющих никакого практического значения. Затем в один прекрасный день я увидел в чьем-то кабинете пару красно-зеленых стереоскопических очков, и это натолкнуло меня на мысль. Предлагаю вашему вниманию программу для просмотра стереоскопических изображении. Приложение находится в каталоге Stereo. Для работы с ним следует надеть стереоскопические очки. На Рисунок 10-7 показано, как будет выглядеть окно приложения, если вы вдруг снимете очки.

Рисунок. 10-7. Нестереоскопическое изображение стереоскопического объекта в зеленых тонах

Свет и тень

Читателей, которым приходилось рассматривать стереокартинки на упаковках с кукурузными хлопьями, может удивить отсутствие на Рисунок 10-7 знакомых красно-зеленых перекрывающихся изображений. Дело в том, что моя программа воспроизводит макет в красном освещении, после чего слегка передвигает камеру и перерисовывает его в зеленом свете. Таким образом, экран может принадлежать либо зеленой, либо красной половинке рабочего цикла. Чтобы добиться полноценного стереоэффекта, запустите приложение Stereo и наденьте стереоскопические очки. При этом желательно выключить свет и остаться в темноте.


94Д •^IStW' Гпап-а 1П Г^ват 1л тош!-

Не знаю, можно ли считать это практическим примером работы с цветным освещением, однако писать программу было довольно интересно. Смысл основной части этого приложения — переместить камеру, задать новое освещение и воспроизвести макет на экране. Давайте выделим несколько минут и посмотрим, как все это делается. Однако перед тем, как заниматься программой, взгляните на Рисунок 10-8, который поясняет принцип ее работы.

Рисунок. 10-8. Получение стереоизображения

Свет и тень

Сначала макет освещается красным светом, а камера переносится в точку А, в которой воспроизводится изображение для левого глаза. Затем макет освещается зеленым светом, а камера переносится в точку В, где воспроизводится изображение для правого глаза. Если просматривать изображение с красным фильтром на левом глазу и зеленым — на правом, то левый глаз будет видеть изображение из точки А, а правый — из точки В. Ниже приведен фрагмент кода пассивного цикла приложения, в котором все это происходит:

BOOL CMainFrame::Update() {

m_bPhase = !m bPhase;

double d = 1.0; // Расхождение

double cz = 10; // Положение камеры по оси z

C3dVector vo;

if (m_pCur3hape) (

I iRRTHhIP UrrrrHJUUIUIA ГРОТЯ "ЧКЙ:-; 9А^

m_pCurShape-»GetPosition (vo) ;

} else {

vo = C3dVector(0, 0, 0) ;

} C3dVector cv;

if (m_pDirLight && m_p3cene) { if "(itiJaPhase) {

m_pDirLight-»SetColor (1, 0, 0) ;

cv = C3dVector(-d, 0, -cz') ;

} else {

m_pDirLight-»SetColor(0, 1, 0);

cv = C3dVector(d, 0, -cz);

} m_pScene-»SetCameraPosition (cv) ;

C3dVector vl = vo — cv;

m_pScene-»SetCameraDirection (vl) ;

}

// Обновить трехмерное окно if (m_wnd3d.Update(1)) { ЬМоге = TRUE;

}

return ЬМоге;

i

Вектор текущего положения объекта (vo) используется для вычисления вектора направления камеры (vl), чтобы векторы для левого и правого видов сходились в центре объекта.

Подобный подход к реализации стереоскопического изображения обладает тремя недостатками:

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


• Стереоскопическое изображение невозможно захватить (сфотографировать);

хотя макет остается прежним, приложение должно постоянно перерисовывать его.

• Вывод стереоскопического макета занимает вдвое больше времени, чем для обычного макета, поскольку приходится сначала воспроизводить его в красном освещении, а затем — в зеленом.

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

/b> ЙУ Глава 10. Свет и тень

Тени

В самом начале книги я упомянул о том, что механизм визуализации Direct3D не поддерживает работу с тенями. Тем не менее в отдельных случаях тень все же можно имитировать, создавая визуальный объект нужной формы и размещая его в нужном месте макета. На Рисунок 10-9 изображен пример из приложения Lights (выполните команды File ¦ New и Edit ¦ Insert Shape, нажмите кнопку ОК на вкладке Sphere и выполните команду Edit ¦ Shadow).

Рисунок. 10-9. Имитация тени

Свет и тень

Для создания тени следует спроектировать объект на плоскость и указать положение источника света в качестве параметра. Чтобы тень появилась в макете, необходимо присоединить ее к некоторому фрейму. Присоединение тени к фрейму затеняющего объекта обеспечивает перемещение тени вместе с объектом. Кроме того, при движении источника света тень также будет перемещаться. Плоскость, на которую отбрасывается тень, задается с помощью точки и вектора нормали. Приведенный ниже фрагмент создает сферу на Рисунок 10-9 и присоединяет к ней тень:

void CMainFrame::OnEditShadow() (

C3dShape* pCast = new C3dShape;

pCast-»CreateSphere(0.3) ;

m_pScene-»AddChild(pCast) ;


m_pScene-»m_ShapeList .Append (pCast) ;

pCast-»SetPosition(0, 1, 0) ;

MakeCurrent(pCast) ;

// Создать тень, слегка приподнятую над плоскостью

Тени

/h2>

CSctVector pt(0, -1.9, 0); // Точка плоскости

C3dVector normal(0, 1, 0); // Нормаль к плоскости

pCast-»Create3hadow(pLight, pt, normal);

i

Функция C3dShape::CreateShadow создает визуальный элемент тени по нескольким аргументам: текущему источнику света, точке плоскости, на которую отбрасывается тень, нормали к этой плоскости:

BOOL CSdShape::CreateShadow(C3dLight* pLight,

D3DVECTOR& vPt,

D3DVECTOR& vN) {

IDirect3DRMVisual* pIVisual = NULL;

m_hr =

the3dEngine.Getlnterface()-»CreateShadow(GetVisual(), pLight-»GetLight () , vPt.x, vPt.y, vPt.z, vN.x, vN.y, vN.z, SpIVisual);

ASSERT(SUCCEEDED(m_hr)) ;

// Присоединить тень к фрейму ASSERT(m_pIFrame) ;

m_hr = m_pIFrame-»AddVisual (pIVisual) ;

return SUCCEEDED(m_hr);

)

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

Обратите внимание — тень может проектироваться только на одну плоскость. Вы не сможете загнуть ее за угол или перенести на смежную грань куба. Кроме того, проекционная плоскость считается бесконечной. В демонстрации, изображенной на Рисунок 10-9, перемещение источника света или фигуры выводит тень за пределы поверхности, на которую она проектируется. Кроме того, можно протащить фигуру сквозь поверхность вниз, а тень при этом останется наверху.

Разумеется, тени повышают реализм изображения, однако пользоваться ими следует осторожно, чтобы не разрушить иллюзию. На Рисунок 10-10 изображено окно приложения Globe, рассмотренного в главе 13, в котором тень применяется для улучшения общего вида макета (кроме того, посмотрите на цветной вариант этого рисунка на вкладке).

9АЯ -flaSi^' ГПОП-Э 1П Г^аат 1Л TQLJL

Рисунок. 10-10. Приложение Globe с тенью

Свет и тень

Итоги

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

Удаленная отладка в Visual C++

Удаленная отладка в Visual C++

Удаленная отладка в Visual C++

Приведенные ниже инструкции предполагают, что вы установили Microsoft Windows 95 на обоих компьютерах — на рабочем (ведущем) и на тестовом (ведомом). Кроме того, вы должны установить Visual C++ на ведущем компьютере, соединить ПК посредством локальной сети и установить стек протокола TCP/IP. Условные имена ведущий и ведомый следует заменить именами ваших компьютеров.
1. На ведомом компьютере создайте каталог TEST и задайте его совместное использование с именем TEST и полным доступом без пароля.
2. Скопируйте перечисленные ниже файлы из соответствующего каталога ведущего компьютера (Msdev\bin или WindowsVSystem) в \\TARGET\TEST (здесь и в дальнейшем TARGET заменяется именем ведомого компьютера):
Msvcmon.exe, Msvcrt40.dll, TInOt.dll, DmnO.dll. Эти файлы необходимы для проведения сеансов отладки на ведомом компьютере.
3. Скопируйте ЕХЕ-файл отлаживаемой программы и все необходимые DLL-библиотеки в \\TARGET\TEST.
4. Запустите на ведущем компьютере Visual C++ и выполните команду Tools ¦ Remote Connection. В окне диалога Remote Connection выберите строку Network (TCP/IP) и нажмите кнопку Settings.
5. В окне диалога Win 32 Network (TCP/IP) Settings укажите имя ведомого компьютера или его IP-адрес (например, 199.99.99.9) и введите пароль (например, DEBUG). Вы обязаны заполнить поле с паролем. Нажмите кнопку ОК во всех окнах диалога.
6. На ведомом компьютере запустите программу Msvcmon.exe, выберите из списка строку Network (TCP/IP) и нажмите кнопку Settings.
7. В окне диалога Win 32 Network (TCP/IP) Settings введите имя ведомого компьютера и пароль, заданный на шаге 5.
8. Нажмите кнопку Connect, чтобы начать сеанс отладки на ведомом компьютере. До установления связи с ведущим компьютером открывается окно диалога Connecting.
9. На ведущем компьютере выполните команду Build ¦ Settings в среде Visual C++. В окне диалога Project Settings выберите вкладку Debug. В полях
•ж^
VnanouLJaa /wn-ani/o D \/ienal f^^-J. 'ЧГЙЙ 40'У
Remote Executable Path и File Name введите путь к исполняемому файлу, скопированному на ведомый компьютер на шаге 3 (например, \TEST\D3DEval.exe). Нажмите кнопку ОК.

10. Нажмите кнопку Go в Visual C++, чтобы начать сеанс отладки. Вероятно, на экране появится окно диалога Find Local Module, в котором следует указать местонахождение локальных DLL-библиотек. Введите путь к каждой из них или снимите флажок, который предлагает вам продолжить поиски.

На ведомом компьютере должно запуститься ваше приложение. Ведущий компьютер будет использоваться для отладки. После завершения работы выйдите из приложения и нажмите кнопку Disconnect в окне диалога Connecting на ведомом компьютере. Не забудьте выбрать в списке Remote Connection на ведущем компьютере строку Local.

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

Отладка с использованием Wdeb386

Если вы хотите получить полный контроль над процессом отладки, вам придется воспользоваться отладчиком Wdeb386. Он позволяет отлаживать любые программы для Windows 95 — приложения, DLL-библиотеки, драйверы устройств, VxD и т. д. Отладчик входит в состав Win32 SDK, который включен в Microsoft Developer Network (II уровень).

Ниже приведены краткие инструкции по запуску Wdeb386. Более подробное описание и перечень команд Wdeb386 можно найти в документации по Win32 SDK.

1. Воспользуйтесь терминалом стандарта RS232 или другим компьютером. Настройте терминал на скорость передачи данных 9600 или 19200 бод. Обычно я задаю для своего терминала 8-битные данные, отсутствие контроля четности и 1 стоп-бит.

2. Подключите терминал к одному из СОМ-портов основного компьютера через нуль-модемный кабель (подробное описание кабельного соединения имеется в сопроводительной документации по установке Wdeb386).

3. Запустите на основном компьютере терминальное приложение (например, HyperTerminal) и убедитесь в правильности пересылки текста на терминал. Наберите на терминале какой-нибудь текст и проверьте, верно ли он передан на основной компьютер. Это позволит быть уверенным в правильности аппаратных настроек.

4. На основном компьютере скопируйте все DLL-библиотеки и символьные файлы из каталога Mstools\debug в Windows\System. Вероятно, для этого вам придется загрузиться в режиме командной строки, поскольку иначе не удастся заместить используемые файлы (такие, как Gdi32.dll). Чтобы загрузить компьютер в режиме командной строки, нажмите клавишу F8 при появлении сообщения Starting Windows во время загрузки и выберите из меню команду Command Prompt Only или выполните команду Shut Down из меню Start Windows 95 и выберите Restart The Computer in MS-DOS Mode.


5. Чтобы запустить сеанс отладки, загрузитесь в режиме командной строки и запустите Wdeb386.ехе. Типичная команда для его загрузки выглядит следующим образом: wdeb386.exe/c:1 /r:9600/s:myfite.sym/s:gdi.sym\windows\win.com.

/b> НУ Приложение А. Отладка

После запуска отладчика загружается Windows, и вы можете тестировать свое приложение. Чтобы перейти в отладчик, нажмите Ctrl+C на терминале или Ctrl+Alt+SysRq на основном компьютере. Если на вашем ПК имеется переключатель NMI (немаскируемого прерывания), он также может использоваться для выхода в отладчик. После этого ваш основной компьютер блокируется до выполнения очередной команды отладчика. Если вы не знаете, что делать дальше, попробуйте команду д — сокращение от go.

Команды для загрузки многочисленных символьных файлов удобно собрать в одном текстовом файле:

/s DDRAW16.SYM /s D3DRAMPF.SYM /s D3DRGBF.SYM /s KRNL386.SYM /S D3DRM8F.SYM /s GDI.SYM /s D3DRM16F.SYM /s D3DRG8F.SYM /s D3DRG16F.SYM /s D3DHALF.SYM

Сохраните этот список в файле, например Debug, inf. Затем можно создать простой пакетный файл для запуска сеанса отладки или ввести следующую командную строку:

wdeb386.exe c:l /r:9600 /f:debug.inf \windows\win.corn /

Подробные инструкции по работе с отладчиком приведены в руководстве пользователя Wdeb386, входящем в Win32 SDK.

Выражение признательности

Выражение признательности
При создании этой книги я получил незначительную помощь от большого количества людей и большую помощь — от незначителвного числа людей. Хочу поблагодарить всех, кто отдал книге свое время и силы (надеюсь, я никого не забуду).
Эрик Стру (Eric Stroo) пригласил меня на демонстрацию Direct3D, проводимую Кент Сикингс (Kate Seekings); именно это событие и вдохновило меня на написание книги.
Дуг Рэбсон (Doug Rabson), Стив Лэйси (Steve Lacey), Джайлс Берджесс (Giles Burgess) и Серван Кеонджиан (Servan Keondjian) из команды программистов Direct3D оказали техническую поддержку по механизму визуализации. Особенно хочется поблагодарить Дуга Рэбсона за ответы, которые он давал на сотни моих вопросов по электронной почте, даже во время выпуска различных бета-версий.
Майкл Виктор (Michael Victor) создал многие трехмерные объекты, использованные в качестве примеров, и превратил наброски автора в полноценные диаграммы.
Мои редакторы Эверилл Карди (Averill Curdy), Джин Росс (Jean Ross) и Виктория Талман (Victoria Thulman), а также все остальные хорошие люди из Microsoft Press помогали мне в работе над книгой.
Дейл Роджерсон (Dale Rogerson) прервал работу над своей книгой, чтобы составить рецензию на мою.
Сердечно благодарю Нэнси Клатс (Nancy Cluts), которая выделила время, чтобы построить и протестировать некоторые примеры, а также написала подробные рецензии по всем главам.
Столь же сердечно благодарю и Ричарда Норена (Richard Noren) за его чрезвычайно подробные рецензии по главам.
Я признателен и многим другим людям, которые помогали мне в написании книги. Среди них: Дон Сперэй (Don Speray), Эрик Берридж (Eric Berridge), Грег Бинкерд (Greg Binkerd), Хан X. Нгуен (Hung H. Nguyen), Джефф В. Стоун (Jeff W. Stone), Джим Блинн (Jim Blinn), Марк Гендрон (Mark Gendron), Майкл Мэлоун (Michael Malone), Пол Дэвид (Paul David), Стив Лэйси (Steve Lacey) и Сью Леду (Sue Ledoux). Спасибо всем!
Благодарю Ричарда Грэншоу (Richard Granshaw) за помощь в особенно сложных ситуациях, которые возникали при работе с моим вариантом Direct3D.
Благодарю фирму Spacetec IMC Corporation за подаренный мне Spaceball Avenger.
Благодарю Денниса Крейна (Dennis Crain), которому во время написания книги я мог пожаловаться на свои проблемы и на жизненные тяготы в целом.
Наконец, хочу поблагодарить свою жену Тамми: за то, что она убедила меня, будто я смогу написать эту книгу в несуществующее свободное время, и помогала мне, отвлекая на себя наших троих детей, Рона, Нелл и Марка; за то, что она периодически просовывала пищу под мою дверь; и за то, что не слишком много жаловалась, хотя в процессе написания книги снова забеременела. Я тебя люблю.



    Бизнес в интернете: Сайты - Софт - Языки - Дизайн