前言
一个模型通常是由三个部分组成:网格、纹理、材质。在一开始的时候,我们是通过Geometry类来生成简单几何体的网格。但现在我们需要寻找合适的方式去表述一个复杂的网格,而且包含网格的文件类型多种多样,对应的描述方式也存在着差异。这一章我们主要研究obj格式文件的读取。
因为精力问题无法对obj做完整支持,如果需要读取obj格式的模型文件,推荐各位使用ASSIMP库
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
.obj格式
.obj格式是Alias|Wavefront公司推出的一种模型文件格式,通常以文本形式进行描述,因此你可以按记事本来打开查看里面的内容。通过市面上的一些常见的建模软件如3dsMax,Maya等都可以导出.obj文件。一些游戏引擎如Unity3d也支持导入.obj格式的模型。该文件可以直接描述多边形、法向量、纹理坐标等等信息。
.obj文件结构简述
.obj文件内部的每一行具体含义取决于开头以空格、制表符分隔的关键字是什么。这里只根据当前项目需要的部分来描述关键字
关键字 | 含义 |
---|---|
# | 这一行是一条注释 |
顶点数据:
关键字 | 含义 |
---|---|
v | 这是一个3D顶点坐标 |
vt | 这是一个纹理坐标 |
vn | 这是一个3D法向量 |
元素:
关键字 | 含义 |
---|---|
f | 这是一个面,这里我们只支持三角形构成的面 |
组合:
关键字 | 含义 |
---|---|
g | 这是一个组,后面接着的内容是组的名称 |
o | 这是一个对象,后面接着的内容是对象的名称 |
材质:
关键字 | 含义 |
---|---|
mtllib | 需要加载.mtl材质文件,后面接着的内容是文件名 |
usemtl | 使用加载的.mtl材质文件中的某一材质,后面接着的内容是材质名 |
.mtl文件结构简述
.mtl文件内部描述方式和.obj文件一样,但里面使用的关键字有所不同
关键字 | 含义 |
---|---|
# | 这一行是一条注释 |
newmtl | 这是一个新的材质,后面接着的内容是材质名称 |
材质描述:
关键字 | 含义 |
---|---|
Ka | 环境光反射颜色 |
Kd | 漫射光反射颜色 |
Ks | 镜面反射光反射颜色 |
d | 不透明度,即Alpha值 |
Tr | 透明度,即1.0 - Alpha值 |
map_Ka | 环境光反射指定的纹理文件 |
map_Kd | 漫射光反射指定的纹理文件 |
简单示例
现在要通过.obj文件来描述一个平面正方形草丛。ground.obj
文件如下:
mtllib ground.mtl
v -10.0 -1.0 -10.0
v -10.0 -1.0 10.0
v 10.0 -1.0 10.0
v 10.0 -1.0 -10.0
vn 0.0 0.0 -1.0
vt 0.0 0.0
vt 0.0 5.0
vt 5.0 5.0
vt 5.0 0.0
g Square
usemtl TestMat
f 1/1/1 2/2/1 3/3/1
f 3/3/1 4/4/1 1/1/1
# 2 faces
其中根据v的先后出现顺序,对应的索引为1到4。若索引值为3,则对应第3行v对应的顶点
注意: 索引的初始值在.obj中为1,而不是0!
而诸如1/1/1
这样的三索引对应的含义为:顶点坐标索引/纹理坐标索引/法向量索引
若写成1//1
,则表明不使用纹理坐标,但现在在我们的项目中不允许缺少上面任何一种索引
这样在一个f
里面出现顶点坐标索引/纹理坐标索引/法向量索引
的次数说明了该面的顶点数目,目前我们也仅考虑支持三角形面
一个模型最少需要包含一个组或一个对象
注意:
(1).obj纹理坐标是基于笛卡尔坐标系的,即(0.3, 0.7)对应的是实际的纹理坐标(0.3, 0.3),即需要做(x, 1.0 - y)的变换
(2).obj的顶点坐标和法向量坐标是基于右手坐标系的,并且在右手坐标系下顶点实际上是按逆时针排布的,需要进行从右手坐标系到左手坐标系的变换,就得取z的负值,并将顶点顺序反过来读(理论上z值不变化且顶点顺序正常来读也是可以正常显示的,只不过读出来的模型变成了xOy屏幕的镜面反射效果)
而.mtl文件的描述如下
newmtl TestMat
d 1.0000
Ns 10.0000
Ka 0.8000 0.8000 0.8000
Kd 0.3000 0.3000 0.3000
Ks 0.0000 0.0000 0.0000
map_Ka grass.dds
map_Kd grass.dds
漫反射和环境光反射都将使用同一种纹理。
使用自定义二进制数据格式提升读取效率
使用文本类型的.obj格式文件进行读取的话必然要面临一个比较严重的问题:模型网格的面数较多会导致文本量极大,直接读取.obj的效率会非常低下。通常推荐在第一次读取.obj文件导入到程序后,再将读取好的顶点等信息以二进制文件的形式合理安排内容布局并保存,然后下次运行的时候读取该二进制文件来获取模型信息,可以大幅度加快读取速度,并且节省了一定的内存空间。
现在来说明下当前项目下自定义二进制格式.mbo的字节布局:
// [Part数目] 4字节
// [AABB盒顶点vMax] 12字节
// [AABB盒顶点vMin] 12字节
// [Part
// [漫射光材质文件名]520字节
// [材质]64字节
// [顶点数]4字节
// [索引数]4字节
// [顶点]32*顶点数 字节
// [索引]2(或4)*索引数 字节,取决于顶点数是否不超过65535
// ]
// ...
这里将.obj中的一个组或一个对象定义为.mbo格式中的一个模型部分,然后顶点使用的是VertexPosNormalTex
类型,大小为32字节。索引使用WORD
或DWORD
类型,若当前Part不同的顶点数超过65535,则必须使用DWORD
类型来存储索引。
环境光/漫射光材质文件名使用的是wchar_t[MAX_PATH]
的数组,大小为2*260字节。
但要注意一开始从.obj导出的顶点数组是没有经过处理(包含重复顶点),需要通过一定的操作分离出顶点数组和索引数组才能传递给.mbo格式。
ObjReader--读取.obj/.mbo格式模型
ObjReader.h中包含了ObjReader
类和MtlReader
类:
#ifndef OBJREADER_H
#define OBJREADER_H
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <unordered_map>
#include <map>
#include <algorithm>
#include <locale>
#include <filesystem>
#include "Vertex.h"
#include "LightHelper.h"
class MtlReader;
class ObjReader
{
public:
struct ObjPart
{
Material material; // 材质
std::vector<VertexPosNormalTex> vertices; // 顶点集合
std::vector<WORD> indices16; // 顶点数不超过65535时使用
std::vector<DWORD> indices32; // 顶点数超过65535时使用
std::wstring texStrDiffuse; // 漫射光纹理文件名,需为相对路径,在mbo必须占260字节
};
// 指定.mbo文件的情况下,若.mbo文件存在,优先读取该文件
// 否则会读取.obj文件
// 若.obj文件被读取,且提供了.mbo文件的路径,则会根据已经读取的数据创建.mbo文件
bool Read(const wchar_t* mboFileName, const wchar_t* objFileName);
bool ReadObj(const wchar_t* objFileName);
bool ReadMbo(const wchar_t* mboFileName);
bool WriteMbo(const wchar_t* mboFileName);
public:
std::vector<ObjPart> objParts;
DirectX::XMFLOAT3 vMin, vMax; // AABB盒双顶点
private:
void AddVertex(const VertexPosNormalTex& vertex, DWORD vpi, DWORD vti, DWORD vni);
// 缓存有v/vt/vn字符串信息
std::unordered_map<std::wstring, DWORD> vertexCache;
};
class MtlReader
{
public:
bool ReadMtl(const wchar_t* mtlFileName);
public:
std::map<std::wstring, Material> materials;
std::map<std::wstring, std::wstring> mapKaStrs;
std::map<std::wstring, std::wstring> mapKdStrs;
};
#endif
ObjReader.cpp定义如下:
#include "ObjReader.h"
using namespace DirectX;
bool ObjReader::Read(const wchar_t * mboFileName, const wchar_t * objFileName)
{
if (mboFileName && ReadMbo(mboFileName))
{
return true;
}
else if (objFileName)
{
bool status = ReadObj(objFileName);
if (status && mboFileName)
return WriteMbo(mboFileName);
return status;
}
return false;
}
bool ObjReader::ReadObj(const wchar_t * objFileName)
{
objParts.clear();
vertexCache.clear();
MtlReader mtlReader;
std::vector<XMFLOAT3> positions;
std::vector<XMFLOAT3> normals;
std::vector<XMFLOAT2> texCoords;
XMVECTOR vecMin = g_XMInfinity, vecMax = g_XMNegInfinity;
std::wifstream wfin(objFileName);
if (!wfin.is_open())
return false;
// 切换中文
std::locale china("chs");
china = wfin.imbue(china);
for (;;)
{
std::wstring wstr;
if (!(wfin >> wstr))
break;
if (wstr[0] == '#')
{
//
// 忽略注释所在行
//
while (!wfin.eof() && wfin.get() != '
')
continue;
}
else if (wstr == L"o" || wstr == L"g")
{
//
// 对象名(组名)
//
objParts.emplace_back(ObjPart());
// 提供默认材质
objParts.back().material.ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
objParts.back().material.diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
objParts.back().material.specular = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
vertexCache.clear();
}
else if (wstr == L"v")
{
//
// 顶点位置
//
// 注意obj使用的是右手坐标系,而不是左手坐标系
// 需要将z值反转
XMFLOAT3 pos;
wfin >> pos.x >> pos.y >> pos.z;
pos.z = -pos.z;
positions.push_back(pos);
XMVECTOR vecPos = XMLoadFloat3(&pos);
vecMax = XMVectorMax(vecMax, vecPos);
vecMin = XMVectorMin(vecMin, vecPos);
}
else if (wstr == L"vt")
{
//
// 顶点纹理坐标
//
// 注意obj使用的是笛卡尔坐标系,而不是纹理坐标系
float u, v;
wfin >> u >> v;
v = 1.0f - v;
texCoords.emplace_back(XMFLOAT2(u, v));
}
else if (wstr == L"vn")
{
//
// 顶点法向量
//
// 注意obj使用的是右手坐标系,而不是左手坐标系
// 需要将z值反转
float x, y, z;
wfin >> x >> y >> z;
z = -z;
normals.emplace_back(XMFLOAT3(x, y, z));
}
else if (wstr == L"mtllib")
{
//
// 指定某一文件的材质
//
std::wstring mtlFile;
wfin >> mtlFile;
// 去掉前后空格
size_t beg = 0, ed = mtlFile.size();
while (iswspace(mtlFile[beg]))
beg++;
while (ed > beg && iswspace(mtlFile[ed - 1]))
ed--;
mtlFile = mtlFile.substr(beg, ed - beg);
// 获取路径
std::wstring dir = objFileName;
size_t pos;
if ((pos = dir.find_last_of('/')) == std::wstring::npos &&
(pos = dir.find_last_of('\')) == std::wstring::npos)
{
pos = 0;
}
else
{
pos += 1;
}
mtlReader.ReadMtl((dir.erase(pos) + mtlFile).c_str());
}
else if (wstr == L"usemtl")
{
//
// 使用之前指定文件内部的某一材质
//
std::wstring mtlName;
std::getline(wfin, mtlName);
// 去掉前后空格
size_t beg = 0, ed = mtlName.size();
while (iswspace(mtlName[beg]))
beg++;
while (ed > beg && iswspace(mtlName[ed - 1]))
ed--;
mtlName = mtlName.substr(beg, ed - beg);
objParts.back().material = mtlReader.materials[mtlName];
objParts.back().texStrDiffuse = mtlReader.mapKdStrs[mtlName];
}
else if (wstr == L"f")
{
//
// 几何面
//
VertexPosNormalTex vertex;
DWORD vpi[3], vni[3], vti[3];
wchar_t ignore;
// 顶点位置索引/纹理坐标索引/法向量索引
// 原来右手坐标系下顶点顺序是逆时针排布
// 现在需要转变为左手坐标系就需要将三角形顶点反过来输入
for (int i = 2; i >= 0; --i)
{
wfin >> vpi[i] >> ignore >> vti[i] >> ignore >> vni[i];
}
for (int i = 0; i < 3; ++i)
{
vertex.pos = positions[vpi[i] - 1];
vertex.normal = normals[vni[i] - 1];
vertex.tex = texCoords[vti[i] - 1];
AddVertex(vertex, vpi[i], vti[i], vni[i]);
}
while (iswblank(wfin.peek()))
wfin.get();
// 几何面顶点数可能超过了3,不支持该格式
if (wfin.peek() != '
')
return false;
}
}
// 顶点数不超过WORD的最大值的话就使用16位WORD存储
for (auto& part : objParts)
{
if (part.vertices.size() < 65535)
{
for (auto& i : part.indices32)
{
part.indices16.push_back((WORD)i);
}
part.indices32.clear();
}
}
XMStoreFloat3(&vMax, vecMax);
XMStoreFloat3(&vMin, vecMin);
return true;
}
bool ObjReader::ReadMbo(const wchar_t * mboFileName)
{
// [Part数目] 4字节
// [AABB盒顶点vMax] 12字节
// [AABB盒顶点vMin] 12字节
// [Part
// [漫射光材质文件名]520字节
// [材质]64字节
// [顶点数]4字节
// [索引数]4字节
// [顶点]32*顶点数 字节
// [索引]2(或4)*索引数 字节,取决于顶点数是否不超过65535
// ]
// ...
std::ifstream fin(mboFileName, std::ios::in | std::ios::binary);
if (!fin.is_open())
return false;
UINT parts = (UINT)objParts.size();
// [Part数目] 4字节
fin.read(reinterpret_cast<char*>(&parts), sizeof(UINT));
objParts.resize(parts);
// [AABB盒顶点vMax] 12字节
fin.read(reinterpret_cast<char*>(&vMax), sizeof(XMFLOAT3));
// [AABB盒顶点vMin] 12字节
fin.read(reinterpret_cast<char*>(&vMin), sizeof(XMFLOAT3));
for (UINT i = 0; i < parts; ++i)
{
wchar_t filePath[MAX_PATH];
// [漫射光材质文件名]520字节
fin.read(reinterpret_cast<char*>(filePath), MAX_PATH * sizeof(wchar_t));
objParts[i].texStrDiffuse = filePath;
// [材质]64字节
fin.read(reinterpret_cast<char*>(&objParts[i].material), sizeof(Material));
UINT vertexCount, indexCount;
// [顶点数]4字节
fin.read(reinterpret_cast<char*>(&vertexCount), sizeof(UINT));
// [索引数]4字节
fin.read(reinterpret_cast<char*>(&indexCount), sizeof(UINT));
// [顶点]32*顶点数 字节
objParts[i].vertices.resize(vertexCount);
fin.read(reinterpret_cast<char*>(objParts[i].vertices.data()), vertexCount * sizeof(VertexPosNormalTex));
if (vertexCount > 65535)
{
// [索引]4*索引数 字节
objParts[i].indices32.resize(indexCount);
fin.read(reinterpret_cast<char*>(objParts[i].indices32.data()), indexCount * sizeof(DWORD));
}
else
{
// [索引]2*索引数 字节
objParts[i].indices16.resize(indexCount);
fin.read(reinterpret_cast<char*>(objParts[i].indices16.data()), indexCount * sizeof(WORD));
}
}
fin.close();
return true;
}
bool ObjReader::WriteMbo(const wchar_t * mboFileName)
{
// [Part数目] 4字节
// [AABB盒顶点vMax] 12字节
// [AABB盒顶点vMin] 12字节
// [Part
// [环境光材质文件名]520字节
// [漫射光材质文件名]520字节
// [材质]64字节
// [顶点数]4字节
// [索引数]4字节
// [顶点]32*顶点数 字节
// [索引]2(或4)*索引数 字节,取决于顶点数是否不超过65535
// ]
// ...
std::ofstream fout(mboFileName, std::ios::out | std::ios::binary);
UINT parts = (UINT)objParts.size();
// [Part数目] 4字节
fout.write(reinterpret_cast<const char*>(&parts), sizeof(UINT));
// [AABB盒顶点vMax] 12字节
fout.write(reinterpret_cast<const char*>(&vMax), sizeof(XMFLOAT3));
// [AABB盒顶点vMin] 12字节
fout.write(reinterpret_cast<const char*>(&vMin), sizeof(XMFLOAT3));
// [Part
for (UINT i = 0; i < parts; ++i)
{
wchar_t filePath[MAX_PATH];
wcscpy_s(filePath, objParts[i].texStrDiffuse.c_str());
// [漫射光材质文件名]520字节
fout.write(reinterpret_cast<const char*>(filePath), MAX_PATH * sizeof(wchar_t));
// [材质]64字节
fout.write(reinterpret_cast<const char*>(&objParts[i].material), sizeof(Material));
UINT vertexCount = (UINT)objParts[i].vertices.size();
// [顶点数]4字节
fout.write(reinterpret_cast<const char*>(&vertexCount), sizeof(UINT));
UINT indexCount;
if (vertexCount > 65535)
{
indexCount = (UINT)objParts[i].indices32.size();
// [索引数]4字节
fout.write(reinterpret_cast<const char*>(&indexCount), sizeof(UINT));
// [顶点]32*顶点数 字节
fout.write(reinterpret_cast<const char*>(objParts[i].vertices.data()), vertexCount * sizeof(VertexPosNormalTex));
// [索引]4*索引数 字节
fout.write(reinterpret_cast<const char*>(objParts[i].indices32.data()), indexCount * sizeof(DWORD));
}
else
{
indexCount = (UINT)objParts[i].indices16.size();
// [索引数]4字节
fout.write(reinterpret_cast<const char*>(&indexCount), sizeof(UINT));
// [顶点]32*顶点数 字节
fout.write(reinterpret_cast<const char*>(objParts[i].vertices.data()), vertexCount * sizeof(VertexPosNormalTex));
// [索引]2*索引数 字节
fout.write(reinterpret_cast<const char*>(objParts[i].indices16.data()), indexCount * sizeof(WORD));
}
}
// ]
fout.close();
return true;
}
void ObjReader::AddVertex(const VertexPosNormalTex& vertex, DWORD vpi, DWORD vti, DWORD vni)
{
std::wstring idxStr = std::to_wstring(vpi) + L"/" + std::to_wstring(vti) + L"/" + std::to_wstring(vni);
// 寻找是否有重复顶点
auto it = vertexCache.find(idxStr);
if (it != vertexCache.end())
{
objParts.back().indices32.push_back(it->second);
}
else
{
objParts.back().vertices.push_back(vertex);
DWORD pos = (DWORD)objParts.back().vertices.size() - 1;
vertexCache[idxStr] = pos;
objParts.back().indices32.push_back(pos);
}
}
bool MtlReader::ReadMtl(const wchar_t * mtlFileName)
{
materials.clear();
mapKdStrs.clear();
std::wifstream wfin(mtlFileName);
std::locale china("chs");
china = wfin.imbue(china);
if (!wfin.is_open())
return false;
std::wstring wstr;
std::wstring currMtl;
for (;;)
{
if (!(wfin >> wstr))
break;
if (wstr[0] == '#')
{
//
// 忽略注释所在行
//
while (wfin.get() != '
')
continue;
}
else if (wstr == L"newmtl")
{
//
// 新材质
//
std::getline(wfin, currMtl);
// 去掉前后空格
size_t beg = 0, ed = currMtl.size();
while (iswspace(currMtl[beg]))
beg++;
while (ed > beg && iswspace(currMtl[ed - 1]))
ed--;
currMtl = currMtl.substr(beg, ed - beg);
}
else if (wstr == L"Ka")
{
//
// 环境光反射颜色
//
XMFLOAT4& ambient = materials[currMtl].ambient;
wfin >> ambient.x >> ambient.y >> ambient.z;
if (ambient.w == 0.0f)
ambient.w = 1.0f;
}
else if (wstr == L"Kd")
{
//
// 漫射光反射颜色
//
XMFLOAT4& diffuse = materials[currMtl].diffuse;
wfin >> diffuse.x >> diffuse.y >> diffuse.z;
if (diffuse.w == 0.0f)
diffuse.w = 1.0f;
}
else if (wstr == L"Ks")
{
//
// 镜面光反射颜色
//
XMFLOAT4& specular = materials[currMtl].specular;
wfin >> specular.x >> specular.y >> specular.z;
}
else if (wstr == L"Ns")
{
//
// 镜面系数
//
wfin >> materials[currMtl].specular.w;
}
else if (wstr == L"d" || wstr == L"Tr")
{
//
// d为不透明度 Tr为透明度
//
float alpha;
wfin >> alpha;
if (wstr == L"Tr")
alpha = 1.0f - alpha;
materials[currMtl].ambient.w = alpha;
materials[currMtl].diffuse.w = alpha;
}
else if (wstr == L"map_Kd")
{
//
// map_Kd为漫反射使用的纹理
//
std::wstring fileName;
std::getline(wfin, fileName);
// 去掉前后空格
size_t beg = 0, ed = fileName.size();
while (iswspace(fileName[beg]))
beg++;
while (ed > beg && iswspace(fileName[ed - 1]))
ed--;
fileName = fileName.substr(beg, ed - beg);
// 追加路径
std::wstring dir = mtlFileName;
size_t pos;
if ((pos = dir.find_last_of('/')) == std::wstring::npos &&
(pos = dir.find_last_of('\')) == std::wstring::npos)
pos = 0;
else
pos += 1;
mapKdStrs[currMtl] = dir.erase(pos) + fileName;
}
}
return true;
}
其中AddVertex
方法用于去除重复的顶点,并构建索引数组。
在改为读取.mbo文件后,原本读取.obj需要耗时3s,现在可以降到2ms以内,大幅提升了读取效率。其关键点就在于要构造连续性的二进制数据以减少读取次数,并剔除掉原本读取.obj时的各种词法分析部分(在该部分也浪费了大量的时间)。
由于ObjReader
类对.obj格式的文件要求比较严格,如果出现不能正确加载的现象,请检查是否出现下面这些情况,否则需要自行修改.obj/.mtl文件,或者给ObjReader
实现更多的功能:
- 使用了/将下一行的内容连接在一起表示一行
- 存在索引为负数
- 使用了类似1//2这样的顶点(即不包含纹理坐标的顶点)
- 使用了绝对路径的文件引用
- 相对路径使用了.和..两种路径格式
- 若.mtl材质文件不存在,则内部会使用默认材质值
- 若.mtl内部没有指定纹理文件引用,需要另外自行加载纹理
- f的顶点数不为3(网格只能以三角形构造,即一个f的顶点数只能为3)
Model类
现在使用一个模型类来管理从ObjReader
读取到的信息,并创建出各种GPU资源:
struct ModelPart
{
// 使用模板别名(C++11)简化类型名
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
ModelPart() : material(), texDiffuse(), vertexBuffer(), indexBuffer(),
vertexCount(), indexCount(), indexFormat() {}
ModelPart(const ModelPart&) = default;
ModelPart& operator=(const ModelPart&) = default;
ModelPart(ModelPart&&) = default;
ModelPart& operator=(ModelPart&&) = default;
Material material;
ComPtr<ID3D11ShaderResourceView> texDiffuse;
ComPtr<ID3D11Buffer> vertexBuffer;
ComPtr<ID3D11Buffer> indexBuffer;
UINT vertexCount;
UINT indexCount;
DXGI_FORMAT indexFormat;
};
struct Model
{
// 使用模板别名(C++11)简化类型名
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
Model();
Model(ID3D11Device * device, const ObjReader& model);
// 设置缓冲区
template<class VertexType, class IndexType>
Model(ID3D11Device * device, const Geometry::MeshData<VertexType, IndexType>& meshData);
template<class VertexType, class IndexType>
Model(ID3D11Device * device, const std::vector<VertexType> & vertices, const std::vector<IndexType>& indices);
Model(ID3D11Device * device, const void* vertices, UINT vertexSize, UINT vertexCount,
const void * indices, UINT indexCount, DXGI_FORMAT indexFormat);
//
// 设置模型
//
void SetModel(ID3D11Device * device, const ObjReader& model);
//
// 设置网格
//
template<class VertexType, class IndexType>
void SetMesh(ID3D11Device * device, const Geometry::MeshData<VertexType, IndexType>& meshData);
template<class VertexType, class IndexType>
void SetMesh(ID3D11Device * device, const std::vector<VertexType> & vertices, const std::vector<IndexType>& indices);
void SetMesh(ID3D11Device * device, const void* vertices, UINT vertexSize, UINT vertexCount,
const void * indices, UINT indexCount, DXGI_FORMAT indexFormat);
//
// 调试
//
// 设置调试对象名
// 若模型被重新设置,调试对象名也需要被重新设置
void SetDebugObjectName(const std::string& name);
std::vector<ModelPart> modelParts;
DirectX::BoundingBox boundingBox;
UINT vertexStride;
};
GameObject类
因为下一章还会讲到硬件实例化,所以GameObject
类在后期还会有所改动:
class GameObject
{
public:
// 使用模板别名(C++11)简化类型名
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
GameObject() = default;
~GameObject() = default;
GameObject(const GameObject&) = default;
GameObject& operator=(const GameObject&) = default;
GameObject(GameObject&&) = default;
GameObject& operator=(GameObject&&) = default;
// 获取物体变换
Transform& GetTransform();
// 获取物体变换
const Transform& GetTransform() const;
//
// 获取包围盒
//
DirectX::BoundingBox GetLocalBoundingBox() const;
DirectX::BoundingBox GetBoundingBox() const;
DirectX::BoundingOrientedBox GetBoundingOrientedBox() const;
//
// 设置模型
//
void SetModel(Model&& model);
void SetModel(const Model& model);
//
// 绘制
//
// 绘制对象
void Draw(ID3D11DeviceContext * deviceContext, BasicEffect& effect);
//
// 调试
//
// 设置调试对象名
// 若模型被重新设置,调试对象名也需要被重新设置
void SetDebugObjectName(const std::string& name);
private:
Model m_Model = {}; // 模型
Transform m_Transform = {}; // 物体变换
};
GameObject::Draw方法
该方法根据已有的模型数据绘制出来:
void GameObject::Draw(ID3D11DeviceContext* deviceContext, BasicEffect& effect)
{
UINT strides = m_Model.vertexStride;
UINT offsets = 0;
for (auto& part : m_Model.modelParts)
{
// 设置顶点/索引缓冲区
deviceContext->IASetVertexBuffers(0, 1, part.vertexBuffer.GetAddressOf(), &strides, &offsets);
deviceContext->IASetIndexBuffer(part.indexBuffer.Get(), part.indexFormat, 0);
// 更新数据并应用
effect.SetWorldMatrix(m_Transform.GetLocalToWorldMatrixXM());
effect.SetTextureDiffuse(part.texDiffuse.Get());
effect.SetMaterial(part.material);
effect.Apply(deviceContext);
deviceContext->DrawIndexed(part.indexCount, 0, 0);
}
}
剩余一些不是很重大的变动就不放出来了,比如BasicEffect
类和对应的hlsl的变动,可以查看源码(文首文末都有)。
模型加载演示
这里我选用了之前合作项目时设计师完成的房屋模型,经过ObjReader
加载后实装到GameObject
以进行绘制。效果如下:
Obj文件完整说明
今天在修改的时候查到一份十分详细的obj和mtl文件说明,有兴趣的读者可以点击下面的链接:
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。