Цвет и текстуры
Глава 8 Цвет и текстуры
Цвет
Целый раздел, посвященный цвету, — не слишком ли расточительно? Впрочем, надо же с чего-то начинать, а работа с цветом относится к числу необходимейших навыков. Как было сказано в главе 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
На Рисунок 13-1 компоненты DirectDraw и Direct3D объединены в одной рамке, поскольку все приложения, работающие с Direct3D, всегда так или иначе связаны и с DirectDraw (если поместить их в отдельных рамках, то на диаграмме лишь появятся дополнительные стрелки).
Рисунок. 13-1. Архитектура DirectDraw
/h2>
Непостедственный оежим Direct3D
Поскольку DirectDraw и Direct3D все же обладают разными функциями, давайте взглянем на диаграмму, на которой показаны услуги, предоставляемые каждым из этих механизмов. На Рисунок 13-2 перечислены некоторые из возможностей, использованных нами при разработке приложений для непосредственного режима Direct3D.
/b>
Глава 13, Direct3D
Конвейер визуализации
Наиболее интересной частью всего непосредственного режима является конвейер визуализации, который получает описание объектов макета, данные об источниках света, несколько матриц 4х4 и создает по ним итоговую картину, которую мы видим на экране. На Рисунок 13-3 изображена упрощенная схема конвейера визуализации.
/b>
мый буфер выполнения (execute buffer). Затем буфер выполнения пропускается через конвейер. Один и тот же буфер можно многократно пропустить через конвейер; вскоре мы убедимся, что это очень полезно.
Если вы захотите выполнять свои собственные преобразования мировых координат, то можете подключиться к конвейеру на этапе освещения. Если потребуется самостоятельно выполнять преобразования и обсчитывать источники света, подключайтесь к конвейеру на этапе генерации растров (должен напомнить о том, что для самостоятельного выполнения преобразований, обсчета освещения и генерации растров вам следует обратиться к главе 12).
Буфер выполнения
Конвейер визуализации получает исходные данные в буферах выполнения. Обычно такой буфер содержит набор вершин и последовательность команд, указывающих, что нужно сделать с этими вершинами. На Рисунок 13-4 изображен упрощенный вид буфера выполнения.
Рисунок. 13-4. Буфер выполнения
В буфере, изображенном на Рисунок 13-4, содержится список вершин, описывающих одну или несколько фигур в системе координат модели. За списком следует операция матричного умножения, которая может использоваться, например, для поворота макета на несколько градусов. Далее идет материал, а за ним — команда, применяемая к списку вершин (обычно сводящаяся к преобразованию и освещению вершин). Оставшаяся часть списка заполняется командами для рисования отдельных треугольников. Каждый треугольник описывается тремя индексными значениями. По индексам выбирается элемент списка вершин, с которого начина-
/h2>
Глава 13. Direct3D
ется буфер. Нетрудно догадаться, что буфер выполнения большей частью заполнен данными о вершинах и фигурах.
Поскольку в набор команд буфера выполнения также включены команды для рисования отдельных точек и линий, вы можете легко воспользоваться тем же самым набором исходных данных для рисования «проволочной модели» объектов — для этого следует заменить команды рисования треугольников командами рисования линий. Если вы захотите самостоятельно выполнять преобразования, с освещением или без него, вид буфера выполнения почти не изменится. Отличия заключаются лишь в том, что для вершин в списке указывается другой тип, а в буфере отсутствуют матричные операции (или данные о материалах, если вы самостоятельно обсчитываете освещение). На Рисунок 13-5 изображена структура команды буфера выполнения.
Рисунок. 13-S. Структура команды в буфере выполнения
Команда состоит из кода операции, определяющего ее тип, поля размера, задающего общий размер блока данных, и счетчика, который указывает количество аналогичных команд, следующих за данной. Иногда счетчик оказывается чрезвычайно полезным — например, при задании набора треугольников.
Я весьма упрощенно показал, как действует буфер выполнения. Тем не менее вскоре мы увидим его в действии (стр. 315), и вы получите представление о том, как пользоваться им на практике. Если вам хочется получить более подробную информацию, обращайтесь к документации DirectX 2 SDK.
BviteD выполнения
/h2>
Практическое использование непосредственного режима
Из документации DirectX 2 SDK выясняется, что для многих объектов непосредственного режима (например, источников света) определяется не только специальная структура данных, но и логический номер, по которому можно обращаться к объекту, а также СОМ-интерфейс для операций с ним. Это кажется излишеством, но если принять во внимание реализацию объектов в Direct3D, такой подход обретает смысл.
Одна из главных задач Direct3D — обеспечение аппаратного ускорения как можно большего количества операций. В этом благородном деле способны помочь логические номера. Предположим, нам потребовалось задать материал объекта. Мы заполняем структуру данных, определяющую материал, и обращаемся к механизму визуализации с требованием создать соответствующий объект. Механизм визуализации обнаруживает, что наши аппаратные средства обладают специальными возможностями по работе с материалами, и поэтому после создания объекта-материала аппаратный драйвер возвращает логический номер для нового материала. Он понадобится нам для манипуляций с созданным объектом, поскольку последний, вероятно, находится в памяти видеоадаптера и мы не сможем обращаться к нему напрямую. Поскольку все начиналось со структуры данных, расположенной в основном адресном пространстве, желательно пользоваться той же самой структурой и для работы с материалом на аппаратном уровне. СОМ-интерфейс объекта позволяет получить информацию от структуры данных и применить ее к объекту. Кроме того, именно логический номер заносится в буфер выполнения при манипуляциях с материалом, потому что работа с ним может происходить на аппаратном уровне. На Рисунок 13-6 показано, как связаны между собой различные компоненты.
Рисунок. 13-6. Различные компоненты материала 308 Hill1 Глава 13. Direct3D
Необходимость хранения структур данных, интерфейсов и логических номеров для каждого объекта приводит к чрезмерному усложнению программ, поэтому я скрыл все это в классах 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, то вы вполне готовы к самостоятельному изучению. Я уже не должен вам помогать, потому что вы знаете ничуть не меньше меня.
Дамы и господа, вечер продолжается!
Глоссарий
Глоссарий
Абстрактный режим
Режим Direct3D, позволяющий программисту работать с объектами, источниками света и трехмерными координатами.
Альфа-буфер
Массив значений, определяющих способ объединения пикселей изображения-источника с изображением-приемником. Альфа-буфер может использоваться для реализации прозрачности, размывки границ и создания тумана.
Бит-блит(ЬНЬН)
Гра4)ическая операция, при которой прямоугольная область пикселей копируется между различными участками памяти.
Блиттер
Аппаратное средство копирования фрагментов изображения между различными участками буфера. Иными словами, аппаратный бит-блит.
Вектор направления
Вектор, задающий направление в трехмерном пространстве.
Вторичный буфер
Видеобуфер для подготовки следующего кадра анимационнои последовательности. Готовый вторичный буфер заменяет первичный и таким образом выводится на экран.
Двойная буферизация
Технология, при которой два (или более) буфера используются для создания мультипликационного изображения без эффекта «мерцания». Новые данные записываются в буфер, который не отображается на экране. Затем буфера переключаются, чтобы мгновенно вывести новое изображение на экран.
Декал
В контексте DirectSD соответствует более распространенному термину «спрайт».
Диффузное отражение
Световой поток, рассеиваемый объектом. Цвет потока в основном совпадает с естественным цветом объекта.
Единицы модели
Произвольные единицы, описывающие положение вершин объектов в создаваемой трехмерной модели. В качестве единиц модели можно выбрать мили, метры, дюймы и т. д., поскольку модель неограниченно масштабируется. Единичная матрица
Матрица, которая при умножении на другую матрицу оставляет ее без изменений. На диагонали — единицы, остальное — нули.
/b> ^Р1 Глоссарий
Единичный вектор
Вектор единичной длины. Удовлетворяет условию
х2 + у2 + z2 = 1
Зеркальное отражение
Световой поток, отражаемый блестящим объектом. Цвет потока обычно совпадает с цветом источника света, а не с цветом самого объекта.
Идентичная палитра
Логическая палитра Windows, которая точно соответствует текущему состоянию физической палитры видеоадаптера.
Кватернион
Математическое представление поворота вокруг заданной оси. Используется для описания поворотов объекта в анимационных сценах.
Конвейер визуализации
Аппаратное и/или программное средство для последовательной обработки графических команд, создающее изображение (обычно в видеобуфере).
Масштабный коэффициент
Множитель, с помощью которого изображение увеличивается или уменьшается.
Мертвая зона
В теории управления «мертвой зоной» называется диапазон входных параметров, при которых выходные значения не изменяются — другими словами, при попадании входных параметров в «мертвую зону» система кажется застывшей. Наличие «мертвой зоны» позволяет предотвратить влияние случайных отклонений или явление дрейфа в серво-механизмах.
Непосредственный режим
Режим Direct3D, который позволяет программисту более тесно работать с аппаратными и программными средствами визуализации, чем в абстрактном режиме.
Ортографическая проекция
Тип проекции, при которой объект «расплющивается» на проекционной плоскости. Ортографическая проекция характерна для конструкторских чертежей, используемых в производстве и строительстве.
Первичный буфер
Видеобуфер, содержащий текущее изображение на экране. В приложениях, работающих в оконном режиме, первичный буфер используется совместно с другими Windows-приложениями.
Перспективная проекция
Тип проекции, создающий иллюзию глубины изображения. Грани объекта, находящиеся в отдалении от наблюдателя, кажутся меньше, чем те, что расположены вблизи. Перспективная проекция помогает создать правдоподобное изображение трехмерного тела на плоскости.
Глоссарий '^lil 331
Интерфейс DirectDraw
Глава 12 Интерфейс 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
Рисование в 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
бы при исчерпании всей свободной видеопамяти вам не пришлось вносить исправления в программу. Разумеется, рисование на поверхностях в основной памяти происходит медленнее, чем обращение к поверхности в видеопамяти, поэтому за расположением поверхностей необходимо следить.
К одним поверхностям могут присоединяться другие. Взгляните на Рисунок 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. Работа с буферами в полноэкранном режиме
Первичный и вторичный буфера имеют одинаковый размер. В полноэкранном режиме можно переключать буфера, то есть выводить на экран содержимое вторичного буфера вместо первичного. Таким образом реализуется переключение страниц видеопамяти с минимальными накладными расходами. Разумеется, при желании можно воспользоваться блит-функциями для переноса содержимого вторичного буфера в первичный.
Второй режим больше соответствует облику стандартных приложений Windows. В оконном режиме приходится работать с привычным окном приложения, расположенным на рабочем столе, как показано на Рисунок 12-4.
Оконный режим связан с определенными сложностями, поскольку поверхность вторичного буфера используется совместно с GDI. При непосредственной записи в видеопамять необходимо соблюдать осторожность и не выйти за пределы области вашего окна. Чтобы помочь вам в этом, DirectDraw предоставляет объект-ограничитель DirectDrawClipper, который присоединяется к первичному буферу. Ограничитель следит за окном, созданным на рабочем столе, и определяет границы памяти первичного буфера. В оконном режиме вы уже не можете переключать страницы, как это делается в полноэкранном режиме, и размер вторичного буфера обычно равен лишь размеру окна. Некоторые видеоадаптеры выполняют сложные перекрытия, которые позволяют в оконном режиме переключать страницы так, как это делается в полноэкранном; при соответствующей аппаратной поддержке DirectDraw позволяет вызвать функцию переключения Flip для поверхности в оконном режиме.
Компоненты DirectDraw
/h2>
Рисунок. 12-4. Оконный режим
Ограничитель не только следит за тем, чтобы вы не пытались что-нибудь записать за пределы прямоугольной области окна. Если другое окно частично перекроет ваше, то вы также не сможете рисовать поверх перекрывающего окна.
Поверхности и 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
числом цветов в системе, на которой выполняется программа. Чтобы изменить цветовой режим, необходимо обратиться к свойствам экрана в Панели управления'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_pIMeshBld) ;
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