DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用二进制文件提升读取效率
前言
一个模型通常是由三个部分组成:网格、纹理、材质。在一开始的时候,我们是通过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() != '\n')
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() != '\n')
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() != '\n')
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,以及有什么问题也可以在这里汇报。
DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用二进制文件提升读取效率的更多相关文章
- cesium模型加载-加载fbx格式模型
整体思路: fbx格式→dae格式→gltf格式→cesium加载gltf格式模型 具体方法: 1. fbx格式→dae格式 工具:3dsMax, 3dsMax插件:OpenCOLLADA, 下载地址 ...
- 如何让openvpn在windows启动时自动加载
在非常需要vpn的人群中,他们几乎一整天都连接着vpn,但是每次开机都要连接vpn的过程有时候比较繁琐对于新手而言.这篇文章主要是说明如何在windows启动时自动加载openvpn自动连接,该教程适 ...
- OpenGL OBJ模型加载.
在我们前面绘制一个屋,我们可以看到,需要每个立方体一个一个的自己来推并且还要处理位置信息.代码量大并且要时间.现在我们通过加载模型文件的方法来生成模型文件,比较流行的3D模型文件有OBJ,FBX,da ...
- 6_1 持久化模型与再次加载_探讨(1)_三种持久化模型加载方式以及import_meta_graph方式加载持久化模型会存在的变量管理命名混淆的问题
笔者提交到gitHub上的问题描述地址是:https://github.com/tensorflow/tensorflow/issues/20140 三种持久化模型加载方式的一个小结论 加载持久化模型 ...
- Wish3D用户必看!模型加载失败原因汇总
上传到Wish3D的模型加载不出来,作品显示页面漆黑一片,是什么原因? 很有可能是操作过程中的小失误,不妨从以下几点检查.还是不行的请加QQ群(Wish3D交流群3):635725654,@Wish3 ...
- 从零开始openGL——三、模型加载及鼠标交互实现
前言 在上篇文章中,介绍了基本图形的绘制.这篇博客中将介绍模型的加载.绘制以及鼠标交互的实现. 模型加载 模型存储 要实现模型的读取.绘制,我们首先需要知道模型是如何存储在文件中的. 通常模型是由网格 ...
- Flutter学习笔记(19)--加载本地图片
如需转载,请注明出处:Flutter学习笔记(19)--加载本地图片 上一篇博客正好用到了本地的图片,记录一下用法: 首先新建一个文件夹,这个文件夹要跟目录下 然后在pubspec.yaml里面声明出 ...
- 安装Windows 2008 操作系统时加载ServeRAID-MR10系列阵列卡驱动
安装Windows 2008 操作系统时加载ServeRAID-MR10系列阵列卡驱动 适用机型: 所有System x3200 M2; 所有System x3250 M2; 所有System x33 ...
- 粒子系统与雨的效果 (DirectX11 with Windows SDK)
前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...
随机推荐
- docker容器日志收集方案汇总评价总结
docker日志收集方案有太多,下面截图罗列docker官方给的日志收集方案(详细请转docker官方文档).很多方案都不适合我们下面的系列文章没有说. 经过以下5篇博客的叙述简单说下docker容器 ...
- Python开发【第四篇】函数
函数的作用 函数可以让编程逻辑结构化以及模块化 无论是C.C++,Java还是Python,函数是必不可少的知识点,也是很重要的知识点,函数是完成一个功能的代码块,使用函数可以使逻辑结构变得更加清晰以 ...
- USING KERBEROS
https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/managing_smart_cards/u ...
- [LeetCode] 12,13 整数和罗马数互转
12. 整数转罗马数字 题目链接:https://leetcode-cn.com/problems/integer-to-roman/ 题目描述: 罗马数字包含以下七种字符: I, V, X, L,C ...
- matlab读取cvs文件的几种方法
matlab读取CVS文件的几种方法: 1,实用csvread()函数 csvread()函数有三种使用方法: 1.M = csvread('filename')2.M = csvread('fi ...
- ARC089E GraphXY 构造
传送门 在Luogu上评了"NOI"之后评级变成了"普及+/提高"--我觉得我可能要退群了 考虑构造一个这样的图: 其中上半部分是从\(S\)开始的一条长\(1 ...
- JS 面向对象 ~ 继承的7种方式
前言: 继承 是 OO 语言中的一个最为人津津乐道的概念.许多 OO 语言都支持两种继承方式:接口继承 和 实现继承.接口继承只继承方法签名,而实现继承则继承实际的方法.如前所述,由于函数没有签名,在 ...
- BugPhobia开发篇章:Beta阶段第VII次Scrum Meeting
0x01 :Scrum Meeting基本摘要 Beta阶段第七次Scrum Meeting 敏捷开发起始时间 2015/12/19 00:00 A.M. 敏捷开发终止时间 2015/12/21 23 ...
- 查看crontab运行状态
cron服务是linux的内置服务,但它不会开机自动启动.可以用以下命令启动和停止服务: /sbin/service crond start/sbin/service crond stop/sbin/ ...
- 前后端不分离的springboot项目问题:页面框架问题
前言:最近自己想搞一个以springboot开发的web项目,由于页面布局问题,在前期开发的时候没有太注意,每天写一点现在开发到一半出现了一个大问题. 1.先说说整个网站框架搭建问题:(整个项目前后端 ...