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

       

Расставляем декорации


Глава 2 Расставляем декорации

Структура приложения


Основные принципы архитектуры, выбираемые в начале проекта, нередко оказывают значительное влияние на его развитие. Неудачная структура может привести к тому, что ваш проект станет «обрастать бородавками» быстрее, чем вы будете их удалять. Например, при написании книги «Animation Techniques for Win32» я еще не обладал достаточным опытом программирования на C++ и работы с Microsoft Visual C++ и библиотеками MFC. В начале работы над примерами я совершил то, что сейчас считаю своей грубой ошибкой: воспользовался Visual C++ для построения однодокументного (SDI) приложения и решил, что мне удастся как-нибудь приспособить его для своих целей. Я сделал это лишь потому, что на тот момент приходилось выбирать между однодокументным и многодокументным (MDI) типами приложения, а MDI-приложение явно не подходило для воспроизведения игровой анимации. Сейчас я ясно понимаю, что мог бы существенно упростить все свои примеры, если бы отказался от принятой в Visual C++ метафоры «документ/вид» и воспользовался простым окном с меню.

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

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

Структура приложения 'Чр!' 33

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

Я не предлагаю вам создавать реальные программы на основе нашего примера. Тем не менее он может пригодиться для экспериментов с новыми идеями. Библиотека 3dPlus проектировалась для обучения, а не для создания коммерческих продуктов. Если вы захотите применить ее при реальной разработке, придется добавить немалый объем кода для обработки ошибок, исключений и т. д.


Для построения базовой структуры приложения в Visual C++ следует выполнить следующие действия:

1. Воспользуйтесь Visual C++ MFC AppWizard для создания однодокументного приложения, в котором отключена поддержка баз данных, поддержка OLE и вывода на печать. В результате получается самое простое оконное приложение, создаваемое с помощью AppWizard. Я назвал свой проект Stage. Вы можете выбрать как статическую компоновку библиотек MFC, так и динамическую, с использованием DLL. Во всех своих примерах я пользовался DLL, чтобы сократить размер ЕХЕ-файла.

2. Исключите из проекта файлы классов документа и вида. В моем случае это были файлы StageDoc.h, StageDoc.cpp, StageView.h и StageView.cpp. Удалите эти файлы из каталога проекта. У вас остаются три программных файла на C++: Stage.cpp, MainFrm.cpp и StdAfx.cpp.

3. Отредактируйте исходные файлы и исключите из них любые ссылки на заголовочные 41айлы классов документа или вида.



4. Вставьте в файл StdAfx.h директивы для включения файлов •^mmsystem.t^ и <d3drmwin.h>. Первый из них используется функциями для работы с джойстиком, которые понадобятся нам позднее, а во втором определяются все функции Direct3D.

5. Включите файл <3dplus.h> в StdAfx.h или Stage.h. Я включил его в Stage.h, чтобы при внесении изменений в библиотеку мне не приходилось бы заново строить предварительно компилированный заголовочный файл в приложении, с которым я работаю.

6. В окне диалога Project Settings (команда Build ¦ Settings) поместите библиотеки Direct3D и 3dPlus в список Link. Во все мои примеры включались файлы SdPlusd.lib, d3drm40f.lib, ddraw.lib и winmm.lib. Обратите внимание: проект библиотеки 3dPlus позволяет работать как с отладочной OdPlusd.lib), так и с окончательной версией библиотеки OdPlusd.lib). В своих примерах я пользовался отладочной версией 3dPlus, чтобы вы могли просмотреть все символы, входящие в библиотеку, а при желании — трассировать ее модули. Библиотека d3drm содержит все трехмерные функции, вызываемые в примере. Библиотека ddraw обеспечивает работу DirectDraw, a winmm — ряд мультимедиа-функций для воспроизведения звука.



Подготовка завершена. Нам еще предстоит добавить в наше приложение довольно много программного кода перед тем, как его можно будет откомпилировать и построить, однако делать это придется уже без помощи AppWizard. На Рисунок 2-1 показана структура приложения Stage.

34 1У Глава 2. Расставляем декорации

Рисунок. 2-1. Структура приложения



Блок с пометкой Механизм визуализации Direct3D чем-то напоминает Рим — все дороги ведут к нему. Мы рассмотрим каждый из этих блоков, когда будем описывать процесс взаимодействия классов семейства C3d с механизмом визуализации Direct3D.

Отображение главного окна

Далее необходимо изменить инициализирующий код приложения, чтобы обеспечить создание главного окна. Для этого следует отредактировать функцию CStage::lnitlnstance в файле Stage.cpp. Когда AppWizard строит базовое приложение SDI, он включает в функцию Initlnstance код для создания пустого документа, который, в свою очередь, создает главное окно. Поскольку мы удалили код, относящийся к документу, придется строить главное окно самостоятельно. Новая версия функции Initlnstance выглядит следующим образом:

BOOL CStageApp::Initlnstance()

{

// Стандартная инициализация

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

#ifdef _AFXDLL

Enable3dControls(); // Вызывается при использовании MFC //в виде совместной DLL-библиотеки

#else

Enable3dControlsStatic(); // Вызывается при статической

// компоновке MFC ttendif

LoadStdProfileSettings();// Загрузить стандартные параметры

// из INI-файла (включая MRU) // Загрузить главное обрамленное окно CMainFrame* pFrame = new CMainFrame;

if (!pFrame->LoadFrame(IDR_MAINFRAME,

35

Отображение главного окна

WSJ3VERLAPPEDWINDOW WS VISIBLE)) {

return FALSE;

}

// Сохранить указатель на главное окно

m pMainWnd = pFrame;

return TRUE;

}

Обратите внимание на два важных действия: вызов функции LoadFrame для загрузки и отображения обрамленного окна и сохранение указателя на него в переменной m_pMainWnd. Указатель сохраняется для того, чтобы классы MFC могли передавать сообщения главному окну приложения, тем самым обеспечивая его правильную работу. Кроме того, необходимо отредактировать файл MainFrm.h и объявить конструктор CMainFrame открытым (public) — по умолчанию он является защищенным (protected). Заодно включите в перечень открытых членов CMainFrame объявление переменной C3dWnd m_wnd3d.



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

В файл Stage.cpp осталось добавить две важные функции: Onldle и OpenDocumentFile. Первая из них обновляет изображение в окне во время пассивной работы приложения, а вторая необходима для правильного открытия файлов. Воспользуйтесь ClassWizard и включите в класс CStageApp функцию Onldle, затем отредактируйте ее и приведите к следующему виду:

BOOL CStageApp::Onldle(LONG ICount) {

BOOL ЬМоге = CWinApp::Onldle(ICount);

// Получить указатель на главное обрамленное окно CMainFrame* pFrame = (CMainFrame*) m_pMainWnd;

if (pFrame) (

// Обновить изображение в трехмерном окне

if (pFrame->m_wnd3d.Update(1)) { ЬМоге = TRUE;

} ! return ЬМоге;

}

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

36 iii^1' Глава 2. Расставляем декорации

ствующее о необходимости выделения дополнительных квантов пассивного времени. Функция C3dWnd::Update, вызываемая из Onldle, возвращает значение TRUE в том случае, если отображаемые объекты существуют, и FALSE, если их нет. При таком подходе наше приложение не станет зря требовать дополнительные кванты в том случае, когда ему нечего рисовать.

Вторая функция, которую необходимо включить в Stage.cpp, OpenDocumentFile, предназначена для работы со списком последних открывавшихся 41айлов в меню. Если добавить в программу код, приведенный в следующем разделе («Модификация главного окна»), но не включить эту функцию, то все будет нормально до тех пор, пока вы не щелкнете на каком-либо имени файла в нижней части меню. В этот момент MFC выдает ASSERT, а ваше приложение останавливается. Жаль, конечно, что работа MFC так тесно привязана к архитектуре «документ/вид», но здесь уж ничего не поделаешь, и нам придется решать те проблемы, которые возникли в тот момент, когда мы удалили из своего проекта 4'>айлы Doc и View и тем самым вмешались в работу AppWizard. К счастью, сделать это несложно. Все, что необходимо, — переопределить функцию CWinApp::OpenDocumentFile (включите эту функцию в класс CStageApp с помощью ClassWizard):



CDocument* CStageApp::OpenDocumentFile(LPCTSTR

IpszFileName)

{

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

CMainFrame* pFrame = (CMainFrame*) m_pMainWnd;

if (pFrame) {

return (CDocument*) pFrame->OpenFile(IpszFileName);

} else {

return NULL;

} }

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

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

Модификация главного окна

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

Модификация главного окна '^ИЯ 37

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

Создание трехмерного окна

AppWizard включает в функцию OnCreate класса CMainFrame довольно много кода, предназначенного для создания самого окна, панели инструментов и строки состояния. Нам придется добавить специальный фрагмент, в котором трехмерное окно будет создаваться как потомок главного обрамленного окна. Заодно мы создадим исходный макет с трехмерным объектом — по крайней мере, мы сможем проверить, работает ли наша программа. Одно их худших разочарований в жизни программиста — ввести несколько сотен строк программы, откомпилировать и запустить ее лишь для того, чтобы увидеть большое черное окно (вряд ли кто-нибудь при этом станет прыгать от радости). Я привожу текст 41ункции OnCreate за исключением фрагментов, сгенерированных AppWizard, чтобы вы могли увидеть, что же именно мы добавили в нее (полный текст функции находится в проекте Stage на прилагаемом к книге CD-ROM):



int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct) (

// Создать трехмерное окно if (!m_wnd3d.Create(this, IDC_3DWND)) ( return -1;

}

NewScene () ;

ASSERT(m_pScene) ;

// Создать трехмерный объект C3dShape shi;

shi.CreateCube(2) ;

m_pScene->AddChild(&shl) ;

shl.SetRotation(l, 1, 1, 0.02);

return 0;

}

Этот фрагмент во многом напоминает тот, который использовался в программе Basic для отображения исходного макета. Обратите внимание на то, что константа IDC_3DWND включается в проект командой View] Resource Symbols в меню Visual C++. Класс CMainFrame пополнился двумя членами: m_wnd3d и m_pScene. Переменная m_wnd3d была включена нами в MainFrm.h ранее, после редактирования функции CStageApp::lnitlnstance (стр. 35). Переменная m_pScene добавляется следующим образом:

38 ¦¦У Глава 2. Расставляем декорации

class CMainFrame : public CFrameWnd

public:

C3dWnd m_wnd3d;

CSdScene* m_pScene;

};

ПРИМЕЧАНИЕ

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

Теперь в нашем главном окне содержится трехмерное окно и указатель на текущий макет. Возвращаясь к функции OnCreate на предыдущей странице, проследим за последовательностью действий: сначала мы создаем трехмерное окно, затем при помощи функции NewScene строим трехмерный макет (работа этой функции будет рассмотрена ниже), после чего мы создаем куб, присоединяем его к макету и начинаем вращать. Если взглянуть на текст функции C3dWnd::Create в библиотеке 3dPlus, нетрудно увидеть, что она создает трехмерное окно в качестве окна-потомка, а передаваемый при ее вызове указатель this (см. предыдущую страницу) используется для определения окна-родителя. Конечно, достижением программистской мысли это не назовешь, и все же данный факт достаточно важен для понимания основ.



Настройка размеров окна

На моем компьютере установлено разрешение экрана 1280х 1024. Microsoft Windows обладает одной скверной привычкой — по умолчанию она создает громадное окно лишь потому, что у меня установлено большое разрешение экрана. При работе с приложениями, для которых такое большое окно не требуется, я обычно устанавливаю исходный размер окна, включая пару лишних строк в CMainFrame::PreCreateWindow. В приведенном ниже фрагменте задается начальный размер окна 300х350 пикселей:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) {

// Задать исходный размер окна

cs.cx = 300;

cs.су = 350;

return CFrameWnd::PreCreateWindow(cs);

ЧЁЙЙ-Й*

Модификация главного окна "тЩЦ: 39

Функция NewScene

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

BOOL CMainFraitie : : NewScene ()

{

// Удалить макет, если он существует if (m pScene) {

m_wnd3d.SetScene(NULL) ;

delete m_pScene;

m_pScene = NULL;

}

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

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

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

dl.Create(0.8, 0.8, 0.8);

m_pScene->AddChild(&dl) ;

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

dl.SetDirectionfl, -1, 1);

m_pScene->SetAmbientLight(0.4, 0.4, 0.4);

m wnd3d.SetScene(m pScene);

return TRUE;

t

Мы включим функцию NewScene в файл MamFrm.cpp. Она удаляет существующий макет и создает новый, со стандартным расположением источников света. Затем макет присоединяется к сцене, которая является частью трехмерного окна. Экспериментируя с трехмерными объектами, я хочу быть уверенным в том, что они уничтожаются и создаются без всяких проблем. Данная функция позволяет удалить все созданные ранее объекты и заново начать работу со макетом (она принесла большую пользу, когда мой малолетний сын схватил джойстик и загнал все объекты куда-то за пределы экрана).

Пересчет размеров трехмерного окна

Так как AppWizard создал панель инструментов и строку состояния, занимающие место в клиентной области окна, нам необходимо иметь возможность заново вычислить размеры трехмерного окна в том случае, если пользователь перемещает или убирает панель инструментов или же скрывает строку состояния. Воспользуйтесь ClassWizard и создайте функцию CMainFrame::RecalcLayout, которая переопределяет функцию CFrameWnd::RecalcLayout:



void CMainFrame::RecalcLayout( BOOL bNotify) 40 iiy Глава 2. Расставляем декорации

\

/I Заново разместить служебные области

//и поместить трехмерное окно в центр.

// Размещение служебных областей выполняется

// обрамленным окном.

CFrameWnd::RecalcLayout(bNotify) ;

// Определить размеры свободного места //в клиентной области // для размещения трехмерного окна CRect re;

RepositionBars(О,

OxFFFF,

IDC_3DWND,

CWnd::reposQuery,

&rc) ;

if (IsWindow(m_wnd3d.GetSafeHwnd())) f m_wnd3d.MoveWindow(&rc, FALSE);

}

В сущности, данный фрагмент определяет размеры свободного места в клиент-ной области и использует его для размещения трехмерного окна. Более подробные объяснения можно найти в документации по MFC.

Уничтожение окна

Все хорошее когда-нибудь приходит к концу — в том числе и окна. Включая в программу функцию OnDestroy, мы сможем освободить память, занятую нашими объектами (для этого следует вызвать ClassWizard и указать обрабатываемое сообщение WM_DESTROY):

void CMainFrame::OnDestroy() {

CFrameWnd::OnDestroy() ;

// Уничтожить текущий макет m_wnd3d.SetScene(NULL) ;

if (m_pScene) { delete m_pScene;

}

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

Модификация главного окна '''^ 41

Подготовка к отображению трехмерного окна

Одна из самых интересных особенностей механизма визуализации DirectSD заключается в том, что он работает непосредственно с видеопамятью и не пользуется интерфейсом графических устройств Windows (GDI). Следовательно, крайне важно, чтобы механизм визуализации точно знал экранное положение того окна, с которым он работает. Если это положение будет указано неверно, то он либо не станет рисовать вообще, либо, что еще хуже, примется рисовать поверх других окон. Кроме того, механизм визуализации должен знать, активно приложение или нет и получило ли оно какие-либо сообщения, связанные с палитрой. Если приложение переходит в фоновый режим, механизм визуализации должен освободить палитру, чтобы ей могли воспользоваться другие приложения. Все эти требования выполняются функциями, предназначенными для обработки сообщений WM_ACIVATEAPP, WM_PALETECHANGED и WM_MOVE:



void CMainFrame::OnActivateApp(BOOL bActive, HTASK hTask) {

CFrameWnd::OnActivateApp(bActive, hTask) ;

// Сообщить трехмерному окну об изменении состояния m_wnd3d.SendMessage(WM_ACTIVATEAPP, (WPARAM)bActive, (LPARAM)hTask) ;

}

void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd) {

// Сообщить трехмерному окну об изменении палитры m_wnd3d.SendMessage(WM_PALETTECHANGED, pFocusWnd ?

(WPARAM)pFocusWnd->GetSafeHwnd() : 0);

\

void CMainFrame::OnMove(int x, int y) {

CFrameWnd::OnMove(x, y) ;

// Сообщить трехмерному окну о перемещении обрамленного

окна

m_wnd3d.SendMessage(WM_MOVE,

О,

MAKELPARAM(0, 0)) ;

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

42 'ЭДГ Глава 2. Расставляем декорации

Меню File

Остается лишь предусмотреть обработку команд меню File ¦ New и File ¦ Open. Ранее мы уже построили функцию для удаления текущего и создания нового макета, поэтому команда File ¦ New реализуется тривиально (воспользуйтесь ClassWizard для добавления идентификатора объекта ID_FILE_NEW):

void CMainFrame::OnFileNew()

{

NewScene ();

}

Команда File ¦ Open обрабатывается двумя следующими функциями:

BOOL CMainFrame::OpenFile(const char* pszPath) {

// Попытаться открыть файл с фигурой

C3dShape sh;

const char* pszFile = sh.Load(pszPath);

if (!pszFile) return FALSE;

// Создать новый макет NewScene ();

ASSERT(m pScene);

// Присоединить новую фигуру к макету m pScene->AddChild(&sh);

sh.SetRotation(l, 1, 1, 0.02);

// Включить имя в список последних открывавшихся файлов AfxGetApp()->AddToRecentFileList(pszFile) ;

return TRUE;

}

void CMainFrame::OnFileOpen() f

OpenFile(NULL) ;

}

Теперь давайте посмотрим, как работает функция OpenFile. Сначала мы создаем новый объект C3dShape и вызываем его функцию Load. Эта функция либо пытается открыть файл, либо, при отсутствии заданного имени файла, выводит окно диалога, в котором пользователю предлагается выбрать файл. В том случае, если файл имеет правильный формат, код класса C3dShape открывает его и создает трехмерный объект на основании данных из файла. Понятно, правда? Далее мы присоединяем новый объект к макету и приводим его во вращение, чтобы увидеть макет во всей красе. Имя файла заносится в список последних открывавшихся файлов, что облегчает его повторное открытие в будущем (вспомните, что функция OpenFile также вызывается в функции



Модификация главного окна 'vS 43

OpenDocumentFile в Stage.cpp, при выборе пользователем одного из файлов в меню).

Осталось добавить несколько завершающих штрихов, после которых проект Stage будет нормально компилироваться и работать. Прежде всего необходимо инициализировать переменную m_pScene в конструкторе CMainFrame, для этого следует включить в конструктор строку m_pScene = NULL. Кроме того, поскольку функции NewScene и OpenFile не были созданы с помощью ClassWizard, придется вручную добавить их объявления в конструктор CMainFrame в файле MainFrm.h.

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

Окна, ракурсы и устройства

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

Давайте начнем с самого начала. Окно — объект Microsoft Windows, который обрабатывает сообщения и отображается в приложениях. Ракурсом называется математическое описание того, как набор объектов в трехмерном пространстве отображается в окне. Устройством (device) называется программа, связанная с реальным устройством, отвечающим за работу видеосистемы на вашем компьютере. Чтобы создать трехмерный макет в приложении, необходимо иметь окно, ракурс и устройство. На самом деле с одним устройством может быть связано несколько ракурсов и несколько окон, однако мы построим систему с одним окном, одним ракурсом и одним устройством. Вы управляете работой окна; управление ракурсом и устройством осуществляет механизм визуализации.



GDI и DirectDraw

Давайте посмотрим, что же на самом деле происходит, когда мы открываем окно на рабочем столе Windows. На Рисунок 2-2 изображено окно, помещенное в произвольном месте рабочего стола.

Теперь спустимся на аппаратный уровень и посмотрим на карту памяти видеоадаптера (Рисунок 2-3).

44 lly Глава 2. Расставляем декорации

Окно

Рисунок. 2-3. Карта памяти видеоадаптера



Окна, ракурсы и устройства

Графический вывод в Windows



Например, для того чтобы нарисовать в окне прямоугольник, следует вызвать функцию Rectangle. GDI спрашивает драйвер видеоустройства, умеет ли тот рисовать прямоугольники; если не умеет, GDI «договаривается» с драйвером о каком-нибудь другом способе рисования прямоугольника (построение множества линий или чего-нибудь в этом роде). Затем драйвер устройства обращается к содержимому видеопамяти или, если нам повезло, пользуется аппаратными особенностями видеокарты для непосредственно рисования прямоугольника. Как вы думаете, быстро проходит этот процесс или медленно? Правильный ответ — не очень медленно, но и быстрым его никак не назовешь. Все дело в универсальности GDI, за которую приходится расплачиваться.

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

46 ^ЦУ Глава 2. Расставляем декорации

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



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

Графический вывод с использованием DirectDraw



DirectDraw обладает еще одной важнейшей особенностью. Посмотрите внимательнее на Рисунок 2-3 на стр. 45. Для хранения изображения на экране используется менее половины имеющейся видеопамяти, а весь остаток пропадает даром. Нельзя ли распорядиться им более разумно? Если видеокарта на Рисунок 2-3 обладает аппаратным блиттером (специальная система, предназначенная для выполнения блито-вых операций), то в свободной памяти можно хранить вспомогательные спрайты, текстуры и т. д. Аппаратный блиттер позволит напрямую переносить изображения из внеэкранной видеопамяти в активную. Вам уже не приходится тратить время на пересылку видеоданных по компьютерной шине данных, благодаря чему возрастает скорость графического вывода. DirectDraw управляет свободной видеопамятью и позволяет создавать в ней внеэкранные поверхности или использовать ее любым другим способом. Фрагмент свободной памяти можно даже выделить под вторичный буфер, размеры которого совпадают с буфером главного окна, и построить в нем следующий кадр анимации, после чего воспользоваться исключительно быстрой блитовой операцией для обновления содержимого активной видеопамяти и смены изображения.

Пересылка видеоданных

Чтобы понять, почему данные в пределах видеопамяти копируются значительно быстрее, чем из основной памяти, необходимо понимать, как работает аппаратная часть компьютера. На Рисунок 2-6 изображена упрощенная модель работы основных компонентов видеосистемы компьютера.

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



Окна, ракурсы и устройства ^1^' 47

Рисунок. 2-6. Примерная архитектура видеосистемы



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

На самом деле пересылку данных в видеопамяти можно ускорить и дальше. Если ваше приложение будет работать в полноэкранном исключительном режиме (full-screen exclusive mode), видеокарта сможет переключаться между двумя страницами видеопамяти, благодаря чему анимация достигает производительности, присущей разве что играм для DOS.

Так какое же отношение все сказанное имеет к механизму визуализации? Он должен очень быстро производить графический вывод (прямо в видеопамять или, еще предпочтительнее, с использованием аппаратных средств видеокарты для поддержки работы с трехмерной графикой). К тому времени, когда вы будете читать эту книгу, на рынке уже появятся видеокарты с аппаратным ускорением трехмерной графики, стоимость которых не будет превышать $200. Как же происходит пересылка данных в этих условиях? Механизм визуализации обращается к функциям промежуточного программного уровня (в данном случае — непосредственного режима Direct3D), который сообщает видеокарте о необходимости выполнить ряд примитивных операций по обсчету трехмерной графики. Если видеокарта не может справиться с подобной задачей, необходимые функции эмулируются с привлечением программных драйверов Direct3D. При наличии таких программных компонентов мы получаем новую модель (Рисунок 2-7), при которой любое приложение (не только механизм визуализации) сможет вызвать набор трехмерных функций, которые будут реализованы с максимальной производительностью.



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

48

Глава 2. Расставляем декорации

Рисунок. 2-7. Работа с трехмерной графикой с участием промежуточного уровня Direct3D



Создание устройства и ракурса в приложении Stage

Давайте рассмотрим, какое же место в этой схеме занимает устройство и ракурс. Устройством называется программный компонент механизма визуализации, работающий с промежуточным уровнем Direct3D (см. Рисунок 2-7), а ракурс определяет, каким образом устройство используется при выводе в область видеопамяти, соответствующую окну. Следовательно, единственное назначение создаваемого окна заключается в том, чтобы указать устройству область видеопамяти, с которой оно будет работать. При создании трехмерного окна мы одновременно создаем устройство и указываем, к какому участку видеопамяти оно будет обращаться. Сделать это можно несколькими способами, но мы рассмотрим лишь два из них.

Проще всего вызвать функцию, которая непосредственно создает устройство по логическому номеру (handle) окна. Это очень удобно, поскольку вам совершенно не приходится думать о том, как работает промежуточный уровень Direct3D — вы указываете логический номер окна, а механизм визуализации делает все остальное. Кроме того, можно воспользоваться функциями DirectDraw для выделения памяти под видеобуферы, а также функциями Direct3D для создания Z-буфера (Z-буфером называется специальный видеобуфер, содержащий информацию о «глубине» каждого пикселя изображения). Затем вся эта информация передается механизму визуализации, который и создает устройство.

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



Окна, ракурсы и устройства

49

BOOL C3dWnd::CreateStage()

// Инициализировать объект DirectDraw if (!m_pDD) {

m pDD = new CDirectDraw;

} if (!m_pDD->Create()) return FALSE;

// Установить экранный режим для окна if ( !m_pDD->SetWindowedMode (GetSafeHwnd () ,

m_iWidth,

m_iHeight)) { return FALSE;

}

// Создать объект Direct3D if (!m_pD3D) {

m_pD3D = new CDirect3D;

} if (!m_pD3D->Create(m_pDD)) return FALSE;

// Задать цветовую модель if (!m_pD3D->SetMode(m_ColorModel)) return FALSE;

// Создать сцену if (!m_pStage) {

m_pStage = new C3dStage;

} if (!m_pStage->Create(m_pD3D)) return FALSE;

// Присоединить текущий макет m pStage->SetScene(m_pScene);

return TRUE;

}

Первая половина функции C3dWnd::CreateStage посвящена созданию объектов DirectDraw и Direct3D, предоставляющих основу для рисования трехмерных объектов в окне. Затем мы выбираем оконный режим для объекта DirectDraw (в отличие от полноэкранного режима) и задаем монохромную цветовую модель MONO для объекта Direct3D (цветовые модели рассматриваются в главе 10). Несколько последних строк создают объект C3dStage по объекту DirectDraw и присоединяют текущий макет к сцене. В свою очередь, объект-сцена C3dStage содержит объекты C3dDevice (устройство) и C3dViewport (ракурс), которые отвечают за взаимодействие с компонентами DirectDraw и Direct3D. Кроме того, сцена содержит объект C3dCamera; мы рассмотрим его ниже. Функция, в которой происходит фактическое создание сцены по объекту Direct3D, выглядит следующим образом:

50 ВД!8' Глава 2. Расставляем декорации

BOOL C3dStage::Create(CDirect3D* pD3D)

{

// Удалить существующий макет SetScene(NULL) ;

// Создать новое устройство по поверхностям Direct3D if (!m_Device.Create(pD3D)) return FALSE;

// Задать качество m Device.SetQuality(m_Quality);

// Создать ракурс if (!m_Viewport.Create(&m_Device,

&m Camera,

0,~0,

m_Device.GetWidth(),

m_Device.GetHeight())) ( return FALSE;

}

return TRUE;

Как видите, приведенная выше функция сводится к построению объектов C3dDevice и CSdViewport. Чтобы создать устройство, мы вызываем соответствующую функцию Direct3D и передаем ей указатель на используемые компоненты DirectDraw:



BOOL C3dDevice::Create(CDirect3D* pD3D)

{

if (m_pIDevice) {

m_pIDevice->Release();

m_pI Device = NULL;

}

m_hr = the3dEngine.Get!nterface()->CreateDeviceFromD3D(

pD3D->GetD3DEngine(),

pD3D->GetD3DDevice (),

&m pIDevice) ;

if (FAILED(m_hr)) { return FALSE;

} ASSERT(m_plDevice) ;

return TRUE;

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

Окна,.ракурсы и устройства "^ 51

Раз уж у меня имелась рабочая программа, в которой использовались поверхности, а не логический номер, я решил оставить все без изменений, в качестве примера. Если когда-нибудь вам захочется самостоятельно поэкспериментировать с механизмом визуализации вместо того, чтобы пользоваться 3dPlus, можно подумать о создании устройства по логическому номеру окна. Разумеется, при работе с классами библиотеки 3dPlus можно совершенно не интересоваться их внутренней реализацией и считать их чем-то вроде «черных ящиков». Тем не менее знакомство с основами работы библиотеки 3dPlus поможет при дальнейшем расширении ее функций.

Проекционная система

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

Положение передней и задней отсекающих плоскостей задается функциями IRLViewport::SetFront и IRLViewport::SetBack. СОМ-интерфейс IRLViewport применяется для управления ракурсом. Величину угла камеры можно изменить функцией IRLViewport::SetField. При создании объекта C3dStage некоторым параметрам присваиваются начальные значения, а другие остаются как есть. Если вы посмотрите на реализацию класса C3dStage, то увидите, что в нем отсутствуют специальные функции для изменения параметров видимой области, поскольку стандартные значения хорошо подходят для наших примеров (хотя при желании



Рисунок. 2-8. Видимая область



52

Глава 2. Расставляем декорации

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

Чтобы точно определить положение трехмерного объекта на экране, необходимо применить к его вершинам преобразование, которое отображает трехмерные пространственные координаты на двумерные координаты окна. Преобразование координат осуществляется с помощью матрицы размеров 4х4, которая является суперпозицией отдельных преобразовании перспективы, масштабирования и переноса. В сущности, для получения двумерных координат вершины следует умножить вектор трехмерных координат на матрицу преобразования. Если вам захочется поближе познакомиться с теорией, я бы порекомендовал книгу «Computer Graphics Principles and Practice» (см. библиографию на стр. 333) или какой-нибудь другой учебник по компьютерной графике.

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

ПРИМЕЧАНИЕ

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

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



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

Проекционная система ''^Ц: 53

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

При создании фрейма сцены камера находится перед сценой и направляется на ее центр. Другими словами, камера располагается в отрицательной области оси Z. На Рисунок 2-9 показано взаимное расположение фреймов камеры и сцены.

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

Рисунок. 2-9. Фрейм камеры и фрейм сцены



Создание фигур

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

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

54

Глава 2. Расставляем декорации

средства для построения трехмерных объектов, текстур и т. д. Тем не менее я рассмотрел другой сценарии. Я представил себе небольшую компанию, которая желает оценить механизм визуализации перед тем, как вкладывать средства в инструменты, необходимые для работы с графикой в крупном проекте. Но как можно экспериментировать, не имея возможности создать собственную фигуру? На самом деле SDK все же содержит функции для построения фигур: вы составляете список координат вершин и набор списков лицевых вершин, после чего вызываете функцию для создания фигуры. Я решил, что для неопытного пользователя такой уровень работы с фигурами покажется слишком низким, и потому включил в библиотеку 3dPlus функции для создания распространенных геометрических фигур — кубов, сфер, цилиндров и конусов. Кроме того, я добавил код, облегчающий использование растров (bitmaps) Windows в качестве текстур; на момент написания книги такая возможность отсутствовала в SDK. Но перед тем, как реализовывать все это, я нашел функцию для загрузки фигуры из файла с расширением .X. Поэтому моя начальная реализация класса CSdShape состояла буквально из одного конструктора и функции Load. Даже этот минимальный объем кода позволил мне получить трехмерные объекты для отображения в окне.



В документации по DirectX 2 SDK сказано, что к 4) рейму могут присоединяться визуальные элементы (один и более). Визуальным элементом (visual) называется фигура или текстура, отображаемая на экране. Визуальный элемент не имеет собственного положения; его необходимо присоединить к фрейму таким образом, чтобы при выполнении преобразования он появился в нужном месте окна. Простоты ради я реализовал объекты CSdShape так, что с каждым из них связан ровно один фрейм и один визуальный элемент. Наличие фрейма и визуального элемента позволяет определить положение объекта 3dShape и его геометрическую форму, благодаря чему он становится больше похож на реальный объект. Недостаток такой схемы заключается в том, что если в макет входят 23 совершенно одинаковых дерева, то для создания леса понадобится 23 фрейма и 23 визуальных элемента, а это не очень эффективно. Гораздо лучше было бы создать всего одну фигуру (визуальный элемент) и воспроизвести ее в 23 различных местах. Другими словами, мы бы присоединили один визуальный элемент к 23 разным фреймам и добились существенной экономии памяти за счет данных, необходимых для определения 22 оставшихся фигур.

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

Давайте посмотрим, как устроена функция C3dShape::Load. При этом я познакомлю вас с некоторыми подробностями реализации и на примере продемонстрирую работу с СОМ-интерфейсами механизма визуализации.

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

static CString strFile;

if (!pszFileName ¦¦ !strlen(pszFileName)) {

// Вывести окно диалога File Open CFileDialog dig(TRUE,

Создание фигур '^1 55

NULL, NULL,

OFN_HIDEREADONLY, _3DOBJ_LOADFILTER, NULL) ;

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

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



} else (

strFile = pszFileName;

}

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

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

m hr = m_pIMeshBld->Load((void*)(const char*)strFile,

NULL,

D3DRMLOAD_FROMFILE ¦ D3DRMLOAD_FIRST,

C3dLoadTextureCallback,

this) ;

if (FAILED(m_hr)) ( return NULL;

}

AttachVisual(m_pIMeshBld) ;

m strName = "File object: ";

m strName += pszFileName;

return strFile;

}

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

C3dShape shape;

shape.Load("egg.x") ;

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

C3dShape shape;

shape.Load(NULL);

56 lly Глава 2. Расставляем декорации

Если имя файла нс указано, появляется окно диалога, в котором строка-фильтр равна *.х, так что по умолчанию в окне диалога отображаются только те файлы, которые может открыть данная функция. После получения имени открываемого файла вызывается локальная функция New, удаляющая из объекта-фигуры любые существующие визуальные элементы. Поскольку я всегда стараюсь создавать объекты, подходящие для повторного использования, вы можете вызывать функцию Load для объекта 3dShape произвольное количество раз. Мне кажется, что это гораздо удобнее, чем создавать новый объект C++ каждый раз, когда мне захочется поиграть с очередной фигурой.

Истинное волшебство происходит в следующем фрагменте, и его следует рассмотреть поподробнее:

ASSERT(m_pIMeshBld) ;

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

D3DRMLOAD_FROMFILE ¦ D3DRMLOAD_FIRST, C3dLoadTextureCallback, this) ;

if (FAILED(m_hr)) { return NOLL;

}

Сначала мы проверяем, не равно ли NULL значение указателя m_plMeshBld. Подобные директивы ASSERT довольно часто встречаются в коде библиотеки 3dPlus. Затем мы вызываем 4'>ункцию IRLMeshBuilder::Load, которая загружает файл и создает на его основе сетку (mesh). СОМ-интерфейс IRLMeshBuilder предназначен для создания и модификации сеток. Сеткой называется набор вершин и граней, определяющих форму объекта (на самом деле в сетку входит еще кое-что, но на данном этапе такого определения будет вполне достаточно). Данная функция, как и большинство других СОМ-функций, возвращает значение типа HRESULT, в котором передаются сведения о том, успешно ли была вызвана функция. Для проверки значения HRESULT и определения того, успешно ли завершилась данная функция, служат два макроса — SUCCEEDED и FAILED. Эти макросы определяются среди функций OLE и не являются специфичными для Direct3D. Я сделал своим правилом присваивать результаты всех обращений к СОМ-интерфейсам, производимых в библиотеке 3dPlus, переменной m_hr, которая присутствует в любом классе семейства C3d. Если при этом вызов завершается неудачно и функция класса возвращает FALSE, можно проанализировать переменную класса m_hr и выяснить причину ошибки. Подобная уловка не претендует на гениальность, но сильно помогает при отладке.



Переменная m_plMeshBld инициализируется при конструировании объекта C3dShape:

C3dShape::C3dShape() (

m_pIVisual = NULL;

C3dFrame::Create(NULL) ;

ASSERT(m_pIFrame) ;

m_pIFrame->SetAppData((OLONG)this) ;

Создание фигур '"^l 57

m strName = «3D Shape»;

m^pIMeshBId = NULL;

the3dEngine.CreateMeshBuilder(&m_pIMeshBld);

ASSERT(m_pIMeshBld) ;

AttachVisual(m_pIMeshBld) ;

}

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

BOOL C3dEngine::CreateMeshBuilder(IDirect3DRMMeshBuilder**

pIBId)

(

ASSERT(m_pIWRL) ;

ASSER'. 'oIBId) ;

m_hr = m_pIWRL->CreateMeshBuilder(pIBld) ;

if (FAILED(m_hr)) return FALSE;

ASSERT(*pIBld);

return TRUE;

Пока я не стану объяснять, откуда берется значение m_plWRL, но вы наверняка уловили общий принцип: обращения к СОМ-интерфейсам мало чем отличаются от вызовов функций объектов в C++. Сходство настолько велико, что я использую префикс р1 для СОМ-интерфейсов. Чтобы понять отличия между ними, давайте посмотрим, что происходит с указателями на СОМ-интерфейсы при уничтожении объекта C3dShape:

C3dShape : : --C3dShape () {

if (m_pIVisual) m_pIVisual->Release() ;

if (m_pIMeshBld) m_pIMeshBld->Release();

m ImgList.DeieteAll () ;

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

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



58 1ЦУ Глава 2. Расставляем декорации

нехорошо перекладывать свою работу на других, однако создание мощной функциональной программы существенно увеличит ее объем, а я стараюсь по возможности упростить свой код, чтобы вам было проще разобраться с ним. Я уже упоминал во вступлении о том, что моя библиотека — не коммерческий продукт, а всего лишь набор примеров. Разработку коммерческой версии я оставляю вам. Если вы хотите научиться создавать мощные классы, которые должным образом обрабатывают исключения, я сильно рекомендую обратиться к книге Скотта Мей-ерса (Scott Meyers) «More Effective C+++: Thirty-Five More Ways to Improve Your Programs and Design» (Addison-Wesley, 1996).

Что же мы узнали в этой главе?

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

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

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

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

«Папа, а зачем там вертится чайник?» Я не знал, что ответить. Тем не менее у меня появился повод подумать о том, как много предстоит сделать, если я хочу создать что-нибудь мало-мальски впечатляющее.

Не беспокойтесь, дальше все пойдет легче.


Содержание раздела