Генерация нормалей вершин
11.2.4. Генерация нормалей вершин
Может получиться так, что в X-файле отсутствуют данные о нормалях вершин. В этом случае нам необходимо вручную вычислить нормали вершин, поскольку они необходимы для расчета освещения. Мы уже немного говорили о том, что делать в таком случае в главе 5. Однако теперь, когда мы знаем об интерфейсе ID3DXMesh и его родителе ID3DXBaseMesh, для генерации нормалей вершин произвольной сетки можно воспользоваться следующей функцией:
HRESULT D3DXComputeNormals( LPD3DXBASEMESH pMesh, // Сетка, для которой вычисляются нормали const DWORD *pAdjacency // Информация о смежности граней );
Эта функция генерирует нормали вершин используя усреднение нормалей. Если предоставлена информация о смежности граней, то дублирующиеся вершины будут игнорироваться. Если же информация о смежности не предоставлена, то дублирующиеся вершины будут получать нормали вычисленные путем усреднения нормалей тех граней, к которым они относятся. При реализации необходимо учесть, что настраиваемый формат вершин той сетки, которую мы передаем в параметре pMesh, должен содержать флаг D3DFVF_NORMAL.
Обратите внимание, что если X-файл не содержит данных нормалей вершин, в формате вершин объекта ID3DXMesh, создаваемого функцией D3DXLoadMeshFromX, флага D3DFVF_NORMAL не будет. Следовательно, перед тем как вызвать функцию D3DXComputeNormals, мы должны клонировать сетку, указав для клона формат вершин с установленным флагом D3DFVF_NORMAL. Эту особенность демонстрирует приведенный ниже фрагмент кода:
// Флаг D3DFVF_NORMAL указан в формате вершин сетки? if (!(pMesh->GetFVF() & D3DFVF_NORMAL)) { // Нет, клонируем сетку и добавляем флаг D3DFVF_NORMAL // к ее формату вершин: ID3DXMesh* pTempMesh = 0; pMesh->CloneMeshFVF( D3DXMESH_MANAGED, pMesh->GetFVF() | D3DFVF_NORMAL, // добавляем флаг Device, &pTempMesh);
// Вычисляем нормали: D3DXComputeNormals(pTempMesh, 0);
pMesh->Release(); // удаляем старую сетку pMesh = pTempMesh; // сохраняем новую сетку с нормалями }
Сетки: часть II
В этой главе мы продолжим изучение связанных с сетками интерфейсов, структур и функций, предоставляемых библиотекой D3DX. Основываясь на заложенном в предыдущей главе фундаменте мы перейдем к более интересным техникам, таким как загрузка и визуализация сложных трехмерных моделей, хранящихся в файле на диске, а также управление уровнем детализации сетки через интерфейс прогрессивных сеток.
Цели | |
Узнать как загрузить данные из X-файла в объект ID3DXMesh. Изучить какую пользу приносит применение прогрессивных сеток и как использовать интерфейс прогрессивных сеток ID3DXPMesh. Познакомиться с ограничивающими объемами, выяснить чем они полезны и как их можно создать с помощью функций библиотеки D3DX. |
IDXBuffer
В предыдущей главе мы упомянули интерфейс ID3DXBuffer, но подробно его не обсуждали. При работе с библиотекой D3DX мы будем часто сталкиваться с этим интерфейсом, так что имеет смысл познакомиться с ним поближе.
Интерфейс ID3DXBuffer представляет собой структуру данных общего назначения, которую библиотека D3DX использует для хранения данных в непрерывном блоке памяти. У интерфейса всего два метода:
LPVOID GetBufferPointer() — Возвращает указатель на начало области с данными.
DWORD GetBufferSize() — Возвращает размер буфера в байтах.
Чтобы структуру можно было применять для любых данных, используются указатели типа void. Это означает, что при получении хранящихся в буфере данных необходимо выполнять приведение типа. Например, функция D3DXLoadMeshFromX использует ID3DXBuffer чтобы возвратить информацию о смежности граней сетки. Поскольку данные о смежности граней хранятся в массиве значений типа DWORD, то, когда мы хотим использовать хранящуюся в буфере информацию о смежности граней, нам надо выполнить приведение типа буфера к массиву DWORD.
Вот пара примеров:
DWORD* info =(DWORD*)adjacencyInfo->GetBufferPointer(); D3DXMATERIAL* mtrls = (D3DXMATERIAL*)mtrlBuffer->GetBufferPointer();
Поскольку ID3DXBuffer это COM-объект, после завершения работы с ним его следует освободить, чтобы не было утечек памяти:
adjacencyInfo->Release(); mtrlBuffer->Release();
Мы можем создать пустой буфер ID3DXBuffer с помощью следующей функции:
HRESULT D3DXCreateBuffer( DWORD NumBytes, // Размер буфера в байтах LPD3DXBUFFER *ppBuffer // Возвращает указатель на буфер );
Приведенный ниже фрагмент кода создает буфер для хранения четырех целых чисел:
ID3DXBuffer* buffer = 0; D3DXCreateBuffer(4 * sizeof(int), &buffer);
с помощью программ редактирования трехмерных
Можно создать сложную сетку с помощью программ редактирования трехмерных моделей и затем либо экспортировать либо конвертировать ее в X-файл. Затем, с помощью функции D3DXLoadMeshFromX можно загрузить данные сетки из X-файла в объект ID3DXMesh, который можно использовать в приложении.
Прогрессивные сетки, представленные интерфейсом ID3DXPMesh, могут применяться для управления уровнем детализации сетки; то есть мы можем динамически изменять детализацию сетки. Эта возможность очень полезна, поскольку часто требуется управление детализацией сетки в зависимости от ее местоположения в сцене. Например, расположенные близко к зрителю сетки должны отображаться с большим количеством деталей, чем сетки, находящиеся вдалеке.
Мы можем вычислить ограничивающую сферу и ограничивающий параллелепипед с помощью функций D3DXComputeBoundingSphere и D3DXComputeBoundingBox соответственно. Ограничивающие объемы полезны, потому что помогают приблизительно оценить занимаемое сеткой пространство, что значительно ускоряет некоторые вычисления.
Изображение сетки с тремя различными уровнями детализации
Рисунок 11.2. Изображение сетки с тремя различными уровнями детализации
В основе прогрессивных сеток лежит таже самая идея, что и у детализируемых текстур. Работая с текстурами мы отметили, что нерационально использовать текстуры с большим разрешением для мелких удаленных объектов на которых дополнительные детали все равно будут невидимы. Тоже самое верно и для сеток; небольшим, удаленным сеткам не требуется такое же большое число граней, что и крупным, близким к зрителю сеткам, потому что дополнительные детали на мелких сетках невидимы. Таким образом мы можем прекратить тратить время на визуализацию моделей с большим количеством граней в тех случаях, когда достаточно более простой модели.
Один из способов использования прогрессивных сеток — настройка уровня детализации сетки в зависимости от расстояния между ней и камерой. При уменьшении дистанции мы добавляем детали (треугольные грани) к сетке, а когда дистанция увеличивается — мы можем убирать детали.
Обратите внимание, что мы обсуждаем не способы реализации прогрессивных сеток, а варианты использования интерфейса ID3DXPMesh. Тех читателей, кого интересуют детали реализации, мы отсылаем к посвященной прогрессивным сеткам статье на сайте Хьюджеса Хоппа http://research.microsoft.com/~hoppe/.
Материалы в X-файле
11.2.2. Материалы в X-файле
Седьмой аргумент функции D3DXLoadMeshFromX возвращает количество используемых в сетке материалов, а пятый аргумент возвращает массив структур D3DXMATERIAL, содержащих данные этих материалов. Определение структуры D3DXMATERIAL выглядит так:
typedef struct D3DXMATERIAL { D3DMATERIAL9 MatD3D; LPSTR pTextureFilename; } D3DXMATERIAL;
Это очень простая структура; она содержит базовую структуру D3DMATERIAL9 и указатель на завершающуюся нулем строку, которая является именем файла связанной с материалом текстуры. X-файлы не содержат в себе данных текстур; вместо этого они содержат имена файлов, которые используются для обращения к графическим файлам, содержащим реальные данные текстур. Следовательно, после загрузки X-файла с помощью функции D3DXLoadMeshFromX мы должны загрузить текстуры, используя указанные имена файлов. Мы покажем как это сделать в следующем разделе.
Особенно ценно, что функция D3DXLoadMeshFromX загружает данные из X-файла таким образом, что i-ый элемент в возвращаемом ею массиве D3DXMATERIAL соответствует i-ой подгруппе сетки. Соответственно подгруппы нумеруются в порядке 0, 1, 2, ..., n– 1, где n — это количество подгрупп и материалов. Это позволяет визуализировать сетку с помощью простого цикла, перебирающего все подгруппы и визуализирующего их.
Методы IDXPMesh
11.3.3. Методы ID3DXPMesh
Интерфейс ID3DXPMesh является наследником интерфейса ID3DXBaseMesh. Поэтому он наследует всю функциональность изученного нами ранее интерфейса ID3DXMesh, а также предоставляет следующие дополнительные методы (учтите, что это не полный список):
DWORD GetMaxFaces(VOID)— Возвращает максимальное количество граней, которое может быть в прогрессивной сетке.
DWORD GetMaxVertices(VOID) — Возвращает максимальное количество вершин, которое может быть в прогрессивной сетке.
DWORD GetMinFaces(VOID) — Возвращает минимальное количество граней, которое может быть в прогрессивной сетке.
DWORD GetMinVertices(VOID) — Возвращает минимальное количество вершин, которое может быть в прогрессивной сетке.
HRESULT SetNumFaces(DWORD Faces) — Данный метод позволяет задать количество граней до которого мы хотим упростить или усложнить сетку. Предположим, сетка состоит из 50 граней и мы хотим упростить ее до 30 граней; тогда нам следует написать:
pmesh->SetNumFaces(30);
Обратите внимание, что после изменения реальное количество граней сетки может отличаться от запрошенного. Если параметр Faces меньше, чем GetMinFaces(), он будет увеличен до GetMinFaces(). Аналогично, если Faces больше чем GetMaxFaces(), он будет уменьшен до GetMaxFaces().
HRESULT SetNumVertices(DWORD Vertices) — Метод позволяет задать количество вершин до которого мы хотим упростить или усложнить сетку. Предположим, сетка состоит из 20 вершин и мы хотим повысить уровень детализации, чтобы она содержала 40 вершин; тогда нам следует написать:
pmesh->SetNumVertices(40);
Обратите внимание, что после изменения реальное количество вершин сетки может отличаться от запрошенного. Если параметр Vertices меньше, чем GetMinVertices(), он будет увеличен до GetMinVertices(). Аналогично, если Vertices больше чем GetMaxVertices(), он будет уменьшен до GetMaxVertices().
HRESULT TrimByFaces( DWORD NewFacesMin, DWORD NewFacesMax, DWORD *rgiFaceRemap, // Данные о перемещении граней DWORD *rgiVertRemap // Данные о перемещении вершин );
Метод позволяет изменить минимальное и максимальное количество граней сетки, указав соответственно значения NewFacesMin и NewFacesMax. Обратите внимание, что новые значения должны попадать в существующий интервал от минимального до максимального значения, то есть находиться в пределах [GetMinFaces(), GetMaxFaces()]. Помимо этого функция возвращает информацию о перемещении вершин и граней. Данные о перемещении вершин и граней обсуждались в разделе 10.4.HRESULT TrimByVertices( DWORD NewVerticesMin, DWORD NewVerticesMax, DWORD *rgiFaceRemap, // Данные о перемещении граней DWORD *rgiVertRemap // Данные о перемещении вершин );
Метод позволяет изменить минимальное и максимальное количество вершин сетки, указав соответственно значения NewVerticesMin и NewVerticesMax. Обратите внимание, что новые значения должны попадать в существующий интервал от минимального до максимального значения, то есть находиться в пределах [GetMinVertices(), GetMaxVertices()]. Помимо этого функция возвращает информацию о перемещении вершин и граней. Данные о перемещении вершин и граней обсуждались в разделе 10.4.
ПРИМЕЧАНИЕ
Новые константы
11.4.1. Новые константы
Давайте добавим две константы, которые будут использоваться в оставшейся части книги. Добавляются они к пространству имен d3d:
namespace d3d {
...
const float INFINITY = FLT_MAX; const float EPSILON = 0.001f;
Константа INFINITY используется просто для представления наибольшего числа, которое может храниться в переменной типа float. Поскольку у нас не может быть значения типа float большего чем FLT_MAX, данное число будет служить для нас концепцией бесконечности, а использование именованной константы делает код более читаемым, явно указывая на те места, где подразумевается бесконечно большое значение. Константа EPSILON— это малое число, задав которое мы будем считать, что все числа меньше его равны нулю. Потребность в такой константе вызвана погрешностями огругления при вычислениях с плавающей точкой. Из-за них результат вычислений, который должен быть равен нулю, может немного отличаться от нуля. И, следовательно, сравнение его с нулем закончится неудачно. Поэтому для чисел с плавающей точкой мы заменяем операцию сравнения с нулем на проверку, что значение меньше, чем EPSILON. Приведенный ниже фрагмент кода показывает, как константа EPSILON используется при проверке равенства двух чисел с плавающей точкой:
bool Equals(float lhs, float rhs) { // если lhs == rhs их разность должна быть равна нулю return fabs(lhs - rhs) < EPSILON ? true : false; }
Ограничивающие объемы
Иногда требуется вычислить ограничивающий объем для сетки. Чаще всего в качестве ограничивающих объемов используются сферы и параллелепипеды. Иногда применяются цилиндры, элипсоиды, ромбы и капсулы. На Рисунок 11.4 изображена сетка с ограничивающей сферой и та же сетка с ограничивающим параллелепипедом. В данном разделе мы будем работать только с ограничивающими сферами и ограничивающими параллелепипедами.
Окно программы Bounding Volumes
Рисунок 11.5. Окно программы Bounding Volumes. Обратите внимание, чтобы ограничивающая сфера была прозрачной используется альфа-смешивание
Код приложения достаточно прост, и мы не будем его обсуждать. Интерес представляет только реализация двух функций, которые формируют ограничивающую сферу и ограничивающий параллелепипед для указанной сетки:
bool ComputeBoundingSphere( ID3DXMesh* mesh, // сетка, для которой вычисляется ограничивающая сфера d3d::BoundingSphere* sphere) // возвращает ограничивающую сферу { HRESULT hr = 0;
BYTE* v = 0; mesh->LockVertexBuffer(0, (void**)&v);
hr = D3DXComputeBoundingSphere( (D3DXVECTOR3*)v, mesh->GetNumVertices(), D3DXGetFVFVertexSize(mesh->GetFVF()), &sphere->_center, &sphere->_radius);
mesh->UnlockVertexBuffer();
if( FAILED(hr) ) return false;
return true; }
bool ComputeBoundingBox( ID3DXMesh* mesh, // сетка, для которой вычисляется ограничивающий параллелепипед d3d::BoundingBox* box) // возвращает ограничивающий параллелепипед { HRESULT hr = 0;
BYTE* v = 0; mesh->LockVertexBuffer(0, (void**)&v);
hr = D3DXComputeBoundingBox( (D3DXVECTOR3*)v, mesh->GetNumVertices(), D3DXGetFVFVertexSize(mesh->GetFVF()), &box->_min, &box->_max);
mesh->UnlockVertexBuffer();
if( FAILED(hr) ) return false;
return true; }
Обратите внимание, что приведение типа (D3DXVECTOR3*)v подразумевает, что в используемой структуре данных вершины информация о координатах вершины хранится в самом начале. Также обратите внимание на использование функции D3DXGetFVFVertexSize для получения размера структуры данных вершины, соответствующей указанному описанию формата вершин.
Окно программы XFile
Рисунок 11.1. Окно программы XFile
В данном примере используются следующие глобальные переменные:
ID3DXMesh* Mesh = 0; std::vector<D3DMATERIAL9> Mtrls(0); std::vector<IDirect3DTexture9*> Textures(0);
Здесь мы объявляем объект ID3DXMesh, который будет использоваться для хранения данных сетки, загружаемых из X-файла. Мы также объявляем векторы материалов и текстур, которые будут хранить используемые в сетке материалы и текстуры.
Начнем с реализации нашей стандартной функции Setup. Сначала мы загружаем X-файл:
bool Setup() { HRESULT hr = 0;
// // Загрузка данных из X-файла // ID3DXBuffer* adjBuffer = 0; ID3DXBuffer* mtrlBuffer = 0; DWORD numMtrls = 0;
hr = D3DXLoadMeshFromX( "bigship1.x", D3DXMESH_MANAGED, Device, &adjBuffer, &mtrlBuffer, 0, &numMtrls, &Mesh);
if(FAILED(hr)) { ::MessageBox(0, "D3DXLoadMeshFromX() - FAILED", 0, 0); return false; }
После того, как данные из X-файла загружены мы должны перебрать все элементы массива D3DXMATERIAL и загрузить текстуры, на которые ссылается сетка:
// // Извлечение материалов и загрузка текстур //
if(mtrlBuffer != 0 && numMtrls != 0) { D3DXMATERIAL* mtrls=(D3DXMATERIAL*)mtrlBuffer-> GetBufferPointer();
for(int i = 0; i < numMtrls; i++) { // При загрузке в свойстве MatD3D не устанавливается // значение для фонового света, поэтому установим его сейчас mtrls[i].MatD3D.Ambient = mtrls[i].MatD3D.Diffuse;
// Сохраняем i-ый материал Mtrls.push_back(mtrls[i].MatD3D);
// Проверяем, связана ли с i-ым материалом текстура if( mtrls[i].pTextureFilename != 0 ) { // Да, загружаем текстуру для i-ой подгруппы IDirect3DTexture9* tex = 0; D3DXCreateTextureFromFile( Device, mtrls[i].pTextureFilename, &tex);
// Сохраняем загруженную текстуру Textures.push_back(tex); } else { // Нет текстуры для i-ой подгруппы Textures.push_back(0); } } } d3d::Release<ID3DXBuffer*>(mtrlBuffer); // закончили работу с буфером . . // Пропущен код, не относящийся к теме данной главы . // (т.е.
установка освещения, матриц вида и проекции и т.д.) . return true; } // конец функции Setup()
В функции Display мы в каждом кадре слегка разворачиваем сетку, чтобы она вращалась. Сетка визуализируется с помощью простого цикла, поскольку ее подгруппам присвоены номера, идущие в порядке 0, 1, 2, ..., n– 1, где n — это количество подгрупп:
bool Display(float timeDelta) { if(Device) { // // Обновление: поворот сетки //
static float y = 0.0f; D3DXMATRIX yRot; D3DXMatrixRotationY(&yRot, y); y += timeDelta;
if( y >= 6.28f ) y = 0.0f;
D3DXMATRIX World = yRot;
Device->SetTransform(D3DTS_WORLD, &World);
// // Визуализация //
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0);
Device->BeginScene();
for(int i = 0; i < Mtrls.size(); i++) { Device->SetMaterial(&Mtrls[i]); Device->SetTexture(0, Textures[i]); Mesh->DrawSubset(i); }
Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; }
Пример приложения: ограничивающие объемы
11.4.3. Пример приложения: ограничивающие объемы
Приложение Bounding Volumes находящееся в папке примеров к данной главе, расположенной на диске с сопроводительными файлами, демонстрирует использование функций D3DXComputeBoundingSphere и D3DXComputeBoundingBox. Программа загружает сетку из X-файла и вычисляет для нее ограничивающую сферу и ограничивающий параллелепипед. Затем программа создает два объекта ID3DXMesh— один для моделирования ограничивающей сферы и другой для моделирования ограничивающего параллелепипеда. После этого визуализируется загруженная из X-файла сетка и вместе с ней либо сетка ограничивающей сферы, либо сетка ограничивающего параллелепипеда (Рисунок 11.5). Пользователь может выбирать, что именно (ограничивающая сфера или ограничивающий параллелепипед) отображается, нажимая на клавишу Space.
Пример приложения: прогрессивная сетка
11.3.4. Пример приложения: прогрессивная сетка
Приложение Progressive Mesh очень похоже на пример XFile, за исключением того, что в нем мы создаем и визуализируем прогрессивную сетку, которая, соответственно, будет представлена интерфейсом ID3DXPMesh. Пользователь может изменять уровень детализации сетки с помощью клавиатуры. Чтобы увеличить количество граней сетки надо нажать клавишу A, а чтобы удалить грани из сетки — нажать клавишу S.
Используемые в данном примере глобальные переменные похожи на глобальные переменные приложения XFile, за исключением того, что мы добавили переменную для хранения прогрессивной сетки:
ID3DXMesh* SourceMesh = 0; ID3DXPMesh* PMesh = 0; // прогрессивная сетка std::vector<D3DMATERIAL9> Mtrls(0); std::vector<IDirect3DTexture9*> Textures(0);
Вспомните, что для создания прогрессивной сетки нам надо указать исходную обычную сетку на основании данных которой будет сгенерирована прогрессивная сетка. Поэтому сперва мы загружаем данные из X-файла в объект ID3DXMesh с именем SourceMesh, и лишь потом создаем прогрессивную сетку:
bool Setup() { HRESULT hr = 0;
// ...Код загрузки данных из X-файла в SourceMesh пропущен // // ...Извлечение материалов и текстур пропущено
Поскольку данный код аналогичен коду приложения XFile мы пропустили его. Теперь, когда у нас есть исходная сетка, мы можем создать из нее прогрессивную сетку с помощью следующего кода:
// // Создание прогрессивной сетки //
hr = D3DXGeneratePMesh( SourceMesh, (DWORD*)adjBuffer->GetBufferPointer(), // смежность граней 0, // использовать веса атрибутов вершин по умолчанию 0, // использовать веса вершин по умолчанию 1, // упрощать насколько возможно D3DXMESHSIMP_FACE, // упрощать по числу граней &PMesh);
d3d::Release<ID3DXMesh*>(SourceMesh); // закончили работу с исходной сеткой d3d::Release<ID3DXBuffer*>(adjBuffer); // закончили работу с буфером
if(FAILED(hr)) { ::MessageBox(0, "D3DXGeneratePMesh() - FAILED", 0, 0); return false; }
Обратите внимание, что хотя мы и указали возможность упрощения сетки до одной грани, обычно такого упрощения не происходит из-за весов вершин и атрибутов; указание 1 приводит к упрощению сетки до минимально возможного разрешения.
Теперь прогрессивная сетка создана, но если мы ее визуализируем прямо сейчас, то она будет изображена с минимально возможным разрешением. Поскольку мы хотим визуализировать сетку с максимальным разрешением, необходимо установить его:
// установить максимальную детализацию DWORD maxFaces = PMesh->GetMaxFaces(); PMesh->SetNumFaces(maxFaces);
В функции Display мы проверяем нажатие клавиш A и S и обрабатываем их:
bool Display(float timeDelta) { if( Device ) { // // Обновление: Смена разрешения сетки //
// Получаем текущее количество граней в сетке pmesh int numFaces = PMesh->GetNumFaces();
// Добавляем грань. Обратите внимание, что SetNumFaces() // автоматически корректирует значение, если оно // выходит за допустимые границы. if(::GetAsyncKeyState('A') & 0x8000f) { // Иногда из-за деталей внутренней реализации // интерфейса ID3DXPMesh, для того, чтобы инвертировать // преобразование слияния граней необходимо добавить // более одной грани. Другими словами, иногда // в результате добавления одной грани количество граней // сетки может остаться неизменным. В таком случае // для увеличения счетчика количества граней // необходимо добавить сразу две грани. PMesh->SetNumFaces(numFaces + 1); if(PMesh->GetNumFaces() == numFaces) PMesh->SetNumFaces(numFaces + 2); }
// Удаляем грань. Обратите внимание, что SetNumFaces() // автоматически корректирует значение, если оно // выходит за допустимые границы. if(::GetAsyncKeyState('S') & 0x8000f) PMesh->SetNumFaces(numFaces - 1);
Код достаточно прямолинеен, следует только обратить внимание, что иногда для инверсии преобразования слияния граней необходимо добавить к сетке не одну грань, а две.
В завершение мы визуализируем объект ID3DXPMesh точно так же, как визуализировали объект ID3DXMesh.
Кроме того, мы желтыми линиями рисуем треугольные ячейки сетки, для чего рисуем сетку в каркасном режиме установив материал желтого цвета. Мы делаем это для того, чтобы можно было видеть добавление и удаление отдельных треугольников при увеличении и уменьшении уровня детализации прогрессивной сетки.
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0); Device->BeginScene();
for(int i = 0; i < Mtrls.size(); i++) { Device->SetMaterial(&Mtrls[i]); Device->SetTexture(0, Textures[i]); PMesh->DrawSubset(i);
// рисуем каркас сетки Device->SetMaterial(&d3d::YELLOW_MTRL); Device->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); PMesh->DrawSubset(i); Device->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); }
Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; } // конец функции Display
Пример приложения: загрузка X-файла
11.2.3. Пример приложения: загрузка X-файла
Теперь мы взглянем на относящийся к рассматриваемой теме код из первого примера к данной главе, который называется XFile. Этот пример загружает X-файл с именем bigship1.x, который находится в папке с DirectX SDK. Полный исходный код примера расположен в сопроводительных файлах. Окно рассматриваемой программы показано на Рисунок 11.1.
Прогрессивные сетки
Прогрессивные сетки, представленные интерфейсом ID3DXPMesh, позволяют упрощать сетку путем последовательности преобразований слияния граней (edge collapse transformations, ECT). Каждое такое преобразование удаляет из сетки одну вершину и одну или две грани. Поскольку преобразование является обратимым (обратное преобразование называется расщеплением вершин (vertex split)), мы можем обратить процесс упрощения в обратную сторону и восстановить первоначальное состояние сетки. Конечно же мы не можем сделать сетку более детализированной чем оригинал; можно только упростить сетку и вернуть ее в исходное состояние. На Рисунок 11.2 показана сетка с тремя различными уровнями детализации (levels of detail, LOD): высоким средним и низким.
Сетка визуализированная с ограничивающей
Рисунок 11.4. Сетка визуализированная с ограничивающей сферой и ограничивающим параллелепипедом. Сфера определяется путем задания центральной точки и радиуса. Параллелепипед определяется путем задания двух углов
Ограничивающие параллелепипеды и сферы, помимо прочего, часто используются для быстрой проверки видимости объектов и для обнаружения столкновений. Например, если не видна ограничивающая сфера или ограничивающий параллелепипед сетки, значит не видна и сама сетка. Проверка видимости сферы или параллелепипеда выполняется гораздо быстрее, чем проверка видимости каждого треугольника сетки. Что касается обнаружения столкновений, предположим, что в сцене выпущена ракета и нам необходимо определить, столкнулась ли она с каким-нибудь объектом сцены. Поскольку объекты состоят из треугольных граней, нам надо перебрать каждую грань каждого объекта и проверить столкнулась ли с ней ракета (которая математически моделируется с помощью луча). Этот подход требует огромного количества проверок пересечения луча с треугольником — по одной проверке для каждой грани каждого объекта сцены. Более эффективный подход заключается в вычислении для каждой сетки ограничивающей сферы или ограничивающего параллелепипеда и последующем выполнении для каждой сетки одной проверки на пересечение луча со сферой (или параллелепипедом). Тогда мы можем сказать, что объект поражен, если луч пересекается с его ограничивающим объемом. Это достаточно хорошая аппроксимация; если требуется большая точность, мы можем использовать проверку пересечения луча со сферой или параллелепипедом для того, чтобы отбросить те объекты, которые явно не задеты, а затем выполнить дополнительные более точные проверки для тех объектов сцены, чьи ограничивающие объемы пересекает луч.
Библиотека D3DX предоставляет функции для вычисления ограничивающей сферы и ограничивающего параллелепипеда сетки. В качестве входных данных функции получают массив вершин сетки для которой вычисляется ограничивающая сфера или ограничивающий параллелепипед.
Функции достаточно гибкие и могут работать с различными форматами вершин.
HRESULT D3DXComputeBoundingSphere( LPD3DXVECTOR3 pFirstPosition, DWORD NumVertices, DWORD dwStride, D3DXVECTOR3* pCenter, FLOAT* pRadius );
pFirstPosition — Указатель на описывающий местоположение вектор в структуре данных первой вершины из массива вершин.
NumVertices — Количество вершин в массиве вершин.
dwStride — Размер данных каждой вершины в байтах. Эти сведения необходимы потому что в структуре данных вершины может храниться дополнительная информация, такая как вектор нормали или координаты текстуры, которая не требуется для вычисления ограничивающей сферы, и функция должна знать, сколько байт следует пропустить, чтобы перейти к данным местоположения следующей вершины.
pCenter — Возвращает координаты центра ограничивающей сферы.
pRadius — Возвращает радиус ограничивающей сферы.
HRESULT D3DXComputeBoundingBox( LPD3DXVECTOR3 pFirstPosition, DWORD NumVertices, DWORD dwStride, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax );
Первые три параметра те же самые, что и первые три параметра в функции D3DXComputeBoundingSphere. Последние два параметра используются для возврата координат двух углов ограничивающего параллелепипеда.
Создание прогессивной сетки
11.3.1. Создание прогессивной сетки
Мы можем создать объект ID3DXPMesh с помощью следующей функции:
HRESULT D3DXGeneratePMesh( LPD3DXMESH pMesh, CONST DWORD *pAdjacency, CONST LPD3DXATTRIBUTEWEIGHTS pVertexAttributeWeights, CONST FLOAT *pVertexWeights, DWORD MinValue, DWORD Options, LPD3DXPMESH *ppPMesh );
pMesh— Исходная сетка на основании данных которой будет создаваться прогрессивная сетка.
pAdjacency — Указатель на массив значений типа DWORD, содержащий информацию о смежности граней сетки pMesh.
pVertexAttributeWeights — Указатель на массив элементов D3DXATTRIBUTEWEIGHTS размера pMesh->GetNumVertices(), в котором i-ый элемент соответствует i-ой вершине сетки pMesh и задает веса ее атрибутов. Веса атрибутов (attribute weight) используются при определении того какая именно вершина будет удалена при упрощении сетки. Вы можете передать в этом параметре ноль, и тогда для каждой вершины будут использованы веса атрибутов по умолчанию. Более подробно веса атрибутов и структура D3DXATTRIBUTEWEIGHTS обсуждаются в разделе 11.3.2.
pVertexWeights — Указатель на массив чисел с плавающей запятой размера pMesh->GetNumVertices(), в котором i-ый элемент соответствует i-ой вершине сетки pMesh и задает вес вершины. Чем больше вес вершины, тем меньше у нее шансов, что она будет удалена в процессе упрощения сетки. Вы можете передать в этом параметре ноль и тогда вес каждой вершины будет равен 1.0 (значение по умолчанию).
MinValue — Минимально возможное количество вершин или граней в сетке (что будет учитываться — вершины или грани — определяет следующий параметр Options) до которого может производиться упрощение. Обратите внимание, что это только желаемое значение и, в зависимости от весов вершин/атрибутов, параметры полученной в результате сетки могут не соответствовать этому значению.
Options — Один из членов перечисления D3DXMESHSIMP:
D3DXMESHSIMP_VERTEX — Указывает, что предыдущий параметр MinValue задает количество вершин.
D3DXMESHSIMP_FACE — Указывает, что предыдущий параметр MinValue задает количество граней.
ppPMesh — Возвращает созданную прогрессивную сетку.
Типы ограничивающих объемов
11.4.2. Типы ограничивающих объемов
Чтобы упростить работу с ограничивающими сферами и ограничивающими параллелепипедами мы реализуем представляющие их классы в пространстве имен d3d:
struct BoundingBox { BoundingBox();
bool isPointInside(D3DXVECTOR3& p);
D3DXVECTOR3 _min; D3DXVECTOR3 _max; };
struct BoundingSphere { BoundingSphere();
D3DXVECTOR3 _center; float _radius; };
d3d::BoundingBox::BoundingBox() { // ограничивающий параллелепипед // бесконечно малого размера _min.x = d3d::INFINITY; _min.y = d3d::INFINITY; _min.z = d3d::INFINITY;
_max.x = -d3d::INFINITY; _max.y = -d3d::INFINITY; _max.z = -d3d::INFINITY; }
bool d3d::BoundingBox::isPointInside(D3DXVECTOR3& p) { // точка внутри ограничивающего параллелепипеда? if(p.x >= _min.x && p.y >= _min.y && p.z >= _min.z && p.x <= _max.x && p.y <= _max.y && p.z <= _max.z) { return true; } else { return false; } }
d3d::BoundingSphere::BoundingSphere() { _radius = 0.0f; }
Веса атрибутов вершин
11.3.2. Веса атрибутов вершин
typedef struct _D3DXATTRIBUTEWEIGHTS { FLOAT Position; FLOAT Boundary; FLOAT Normal; FLOAT Diffuse; FLOAT Specular; FLOAT Texcoord[8]; FLOAT Tangent; FLOAT Binormal; } D3DXATTRIBUTEWEIGHTS;
Структура данных с весами атрибутов вершины позволяет нам указать вес для каждого возможного компонента вершины. Значение 0.0 указывает, что данный компонент не обладает весом. Чем больше вес компонентов вершины, тем меньше вероятность, что данная вершина будет удалена при упрощении сетки. По умолчанию используются следующие значения:
D3DXATTRIBUTEWEIGHTS AttributeWeights; AttributeWeights.Position = 1.0; AttributeWeights.Boundary = 1.0; AttributeWeights.Normal = 1.0; AttributeWeights.Diffuse = 0.0; AttributeWeights.Specular = 0.0; AttributeWeights.Tex[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
Рекомендуется всегда использовать значения по умолчанию, только если у приложения нет веских причин отказаться от них.
X-файлы
До сих пор мы работали с простыми геометрическими объектами, такими как сферы, цилиндры, кубы, и использовали функции D3DXCreate*. Если вы попытаетесь сконструировать собственный трехмерный объект, указывая координаты его вершин, то несомненно обнаружите, что это очень нудное занятие. Для того, чтобы облегчить эту скучную работу по созданию данных трехмерных объектов были созданы специальные приложения, называемые редакторами трехмерных моделей (3D modelers). Эти редакторы позволяют пользователю создавать сложные и реалистично выглядящие сетки в визуальной интерактивной среде с помощью богатого набора инструментов, что делает процесс моделирования гораздо проще. Наиболее популярными редакторами моделей в отрасли программирования игр являются 3DSMax (www.discreet.com), LightWave 3D (www.newtek.com) и Maya (www.aliaswavefront.com).
Естественно, эти редакторы могут экспортировать данные созданной сетки (геометрию, материалы, анимацию и другие полезные данные) в файл. Следовательно, нам остается написать процедуру чтения файла, которая будет извлекать все данные сетки, после чего мы сможем использовать их в нашем приложении. Это очень хорошее решение, но есть и более удобный вариант. Существует формат файла с данными сетки, называемый X-файл (с расширением .X). Большинство популярных редакторов моделей могут выполнять экспорт данных в этот формат и, кроме того, существует множество программ-конвертеров, преобразующих распространенные форматы файлов сеток в файлы формата .X. Главным удобством X-файлов является то, что они являются «родным» форматом DirectX и, следовательно, библиотека D3DX без дополнительных усилий с вашей стороны поддерживает работу с X-файлами. Это значит, что библиотека D3DX предоставляет функции для чтения и записи X-файлов, а значит, если мы используем этот формат, нам не надо писать собственные процедуры чтения и записи.
ПРИМЕЧАНИЕ
Загрузка X-файлов
11.2.1. Загрузка X-файлов
Для загрузки содержащихся в X-файле данных сетки мы будем использовать показанную ниже функцию. Обратите внимание, что этот метод создает объект ID3DXMesh и загружает в него данные геометрии из X-файла.
HRESULT D3DXLoadMeshFromX( LPCSTR pFilename, DWORD Options, LPDIRECT3DDEVICE9 pDevice, LPD3DXBUFFER *ppAdjacency, LPD3DXBUFFER *ppMaterials, LPD3DXBUFFER* ppEffectInstances, PDWORD pNumMaterials, LPD3DXMESH *ppMesh );
pFilename — Имя загружаемого X-файла.
Options — Один или несколько флагов, определяющих параметры создаваемой сетки. Полный список флагов приведен в описании перечисления D3DXMESH в документации SDK. Наиболее часто используются следующие флаги:
D3DXMESH_32BIT — Сетка будет использовать 32-разрядные индексы.
D3DXMESH_MANAGED — Сетка будет размещена в управляемом пуле памяти.
D3DXMESH_WRITEONLY — Данные сетки будут только записываться и не будут читаться.
D3DXMESH_DYNAMIC — Буферы сетки будут динамическими.
pDevice — Связанное с сеткой устройство.
ppAdjacency — Возвращает буфер ID3DXBuffer, содержащий массив значений типа DWORD, хранящий информацию о смежности граней сетки.
ppMaterials — Возвращает буфер ID3DXBuffer, содержащий массив структур D3DXMATERIAL, хранящий данные о материалах сетки. Мы подробнее поговорим о материалах сетки в следующем разделе.
ppEffectInstances — Возвращает буфер ID3DXBuffer, содержащий массив структур D3DXEFFECTINSTANCE. Мы игнорируем этот параметр и всегда будем передавать в нем 0.
pNumMaterials — Возвращает количество используемых в сетке материалов (то есть количество элементов D3DXMATERIAL в массиве, возвращаемом через указатель ppMaterials).
ppMesh — Возвращает созданный объект ID3DXMesh, заполненный данными о геометрии из X-файла.