netlib.narod.ru | < Назад | Оглавление | Далее > |
Последний фрагмент кода, добавленный нами в главное обрамленное окно (функция CMainFrame::OpenFile), предназначался для загрузки объекта C3dShape из файла на диске и его включения в текущий макет. Фигуры будут подробно рассмотрены в главе 4, однако я хочу показать вам, что представляют собой объекты C3dShape, и показать, почему я сделал их именно такими.
DirectX 2 SDK позволяет создавать приложения, работающие с механизмом визуализации. Однако SDK не содержит ни отдельных программ, ни простых функций для создания фигур — предполагается, что у вас имеются собственные средства для построения трехмерных объектов, текстур и т.д. Тем не менее я рассмотрел другой сценарий. Я представил себе небольшую компанию, которая желает оценить механизм визуализации перед тем, как вкладывать средства в инструменты, необходимые для работы с графикой в крупном проекте. Но как можно экспериментировать, не имея возможности создать собственную фигуру? На самом деле SDK все же содержит функции для построения фигур: вы составляете список координат вершин и набор списков лицевых вершин, после чего вызываете функцию для создания фигуры. Я решил, что для неопытного пользователя такой уровень работы с фигурами покажется слишком низким, и потому включил в библиотеку 3dPlus функции для создания распространенных геометрических фигур — кубов, сфер, цилиндров и конусов. Кроме того, я добавил код, облегчающий использование растров (bitmaps) Windows в качестве текстур; на момент написания книги такая возможность отсутствовала в SDK. Но перед тем, как реализовывать все это, я нашел функцию для загрузки фигуры из файла с расширением .X. Поэтому моя начальная реализация класса C3dShape состояла буквально из одного конструктора и функции Load. Даже этот минимальный объем кода позволил мне получить трехмерные объекты для отображения в окне.
В документации по DirectX 2 SDK сказано, что к фрейму могут присоединяться визуальные элементы (один и более). Визуальным элементом (visual) называется фигура или текстура, отображаемая на экране. Визуальный элемент не имеет собственного положения; его необходимо присоединить к фрейму таким образом, чтобы при выполнении преобразования он появился в нужном месте окна. Простоты ради я реализовал объекты C3dShape так, что с каждым из них связан ровно один фрейм и один визуальный элемент. Наличие фрейма и визуального элемента позволяет определить положение объекта 3dShape и его геометрическую форму, благодаря чему он становится больше похож на реальный объект. Недостаток такой схемы заключается в том, что если в макет входят 23 совершенно одинаковых дерева, то для создания леса понадобится 23 фрейма и 23 визуальных элемента, а это не очень эффективно. Гораздо лучше было бы создать всего одну фигуру (визуальный элемент) и воспроизвести ее в 23 различных местах. Другими словами, мы бы присоединили один визуальный элемент к 23 разным фреймам и добились существенной экономии памяти за счет данных, необходимых для определения 22 оставшихся фигур.
На самом деле вы все же можете присоединить один и тот же визуальный элемент к нескольким фреймам, как мы увидим в главе 4 при рассмотрении клонирования фигур. Однако описанная выше простая схема, невзирая на некоторую неэффективность, в нашем случае работает вполне нормально.
Давайте посмотрим, как устроена функция C3dShape::Load. При этом я познакомлю вас с некоторыми подробностями реализации и на примере продемонстрирую работу с СОМ-интерфейсами механизма визуализации.
const char* C3dShape::Load(const char* pszFileName) { static CString strFile; if (!pszFileName ¦¦ !strlen(pszFileName)) { // Вывести окно диалога File Open CFileDialog dlg(TRUE, NULL, NULL, OFN_HIDEREADONLY, _3DOBJ_LOADFILTER, NULL); if (dlg.DoModal() != IDOK) return NULL; // Получить путь к файлу strFile = dlg.m_ofn.lpstrFile; } 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);
Если имя файла не указано, появляется окно диалога, в котором строка-фильтр равна *.х, так что по умолчанию в окне диалога отображаются только те файлы, которые может открыть данная функция. После получения имени открываемого файла вызывается локальная функция 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 NULL; }
Сначала мы проверяем, не равно ли NULL значение указателя m_pIMeshBld. Подобные директивы ASSERT довольно часто встречаются в коде библиотеки 3dPlus. Затем мы вызываем функцию IRLMeshBuilder::Load, которая загружает файл и создает на его основе сетку (mesh). СОМ-интерфейс IRLMeshBuilder предназначен для создания и модификации сеток. Сеткой называется набор вершин и граней, определяющих форму объекта (на самом деле в сетку входит еще кое-что, но на данном этапе такого определения будет вполне достаточно). Данная функция, как и большинство других СОМ-функций, возвращает значение типа HRESULT, в котором передаются сведения о том, успешно ли была вызвана функция. Для проверки значения HRESULT и определения того, успешно ли завершилась данная функция, служат два макроса — SUCCEEDED и FAILED. Эти макросы определяются среди функций OLE и не являются специфичными для Direct3D. Я сделал своим правилом присваивать результаты всех обращений к СОМ-интерфейсам, производимых в библиотеке 3dPlus, переменной m_hr, которая присутствует в любом классе семейства C3d. Если при этом вызов завершается неудачно и функция класса возвращает FALSE, можно проанализировать переменную класса m_hr и выяснить причину ошибки. Подобная уловка не претендует на гениальность, но сильно помогает при отладке.
Переменная m_pIMeshBld инициализируется при конструировании объекта C3dShape:
C3dShape::C3dShape() { m_pIVisual = NULL; C3dFrame::Create(NULL); ASSERT(m_pIFrame); m_pIFrame->SetAppData((ULONG)this); m strName = "3D Shape"; m_pIMeshBld = NULL; the3dEngine.CreateMeshBuilder(&m_pIMeshBld); ASSERT(m_pIMeshBld); AttachVisual(m_pIMeshBld); }
Глобальный объект the3dEngine пользуется некоторыми глобальными функциями Direct3D для создания различных интерфейсов трехмерной графики. Чтобы вы не подумали, будто я от вас что-то скрываю, покажу, откуда возникает интерфейс IRLMeshBuilder:
BOOL C3dEngine::CreateMeshBuilder(IDirect3DRMMeshBuilder** pIBld) { ASSERT(m_pIWRL); ASSERT(pIBld); m_hr = m_pIWRL->CreateMeshBuilder(pIBld); if (FAILED(m_hr)) return FALSE; ASSERT(*pIBld); return TRUE; }
Пока я не стану объяснять, откуда берется значение m_pIWRL, но вы наверняка уловили общий принцип: обращения к СОМ-интерфейсам мало чем отличаются от вызовов функций объектов в C++. Сходство настолько велико, что я использую префикс рI для СОМ-интерфейсов. Чтобы понять отличия между ними, давайте посмотрим, что происходит с указателями на СОМ-интерфейсы при уничтожении объекта C3dShape:
C3dShape::~C3dShape() { if (m_pIVisual) m_pIVisual->Release(); if (m_pIMeshBld) m_pIMeshBld->Release(); m_ImgList.DeieteAll(); }
Как видите, наши действия сильно отличаются от обычного удаления объектов по указателям. Завершая работу с СОМ-интерфейсом, вы обязаны вызвать его функцию Release, чтобы уменьшить значение его счетчика обращений. Если не сделать этого, то СОМ-объект будет жить в памяти вечно.
Напоследок я бы хотел сделать одно замечание, относящееся к вызову функций из конструкторов объектов C++. Как нетрудно догадаться, попытка создать интерфейс внутри конструктора может кончиться неудачей — чаще всего это происходит из-за нехватки памяти. В своей программе я даже не пытаюсь обнаружить такую ситуацию. Проблемы с памятью вызывают исключение, которое, как я надеюсь, будет перехвачено в вашей программе! Конечно, с моей стороны нехорошо перекладывать свою работу на других, однако создание мощной функциональной программы существенно увеличит ее объем, а я стараюсь по возможности упростить свой код, чтобы вам было проще разобраться с ним. Я уже упоминал во вступлении о том, что моя библиотека — не коммерческий продукт, а всего лишь набор примеров. Разработку коммерческой версии я оставляю вам. Если вы хотите научиться создавать мощные классы, которые должным образом обрабатывают исключения, я сильно рекомендую обратиться к книге Скотта Мейерса (Scott Meyers) «More Effective C++: Thirty-Five More Ways to Improve Your Programs and Design» (Addison-Wesley, 1996).
netlib.narod.ru | < Назад | Оглавление | Далее > |