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< Назад | Оглавление | Далее >

Сайт управляется системой uCoz