2021SC@SDUSC
目录
Parser
1.IModelParser
2.EModelParserFlags
3.AssimpParser
3.1LoadModel
3.2ProcessMaterials
3.3ProcessNode
3.4ProcessMesh
本章我们继续来讲OvRendering中的函数。首先需要简单了解两种类型的函数,这将会对以后的章节有所帮助。
Parser
1.IModelParser
/**
* Interface for any model parser
*/
class IModelParser
{
public:
/**
* Load meshes from a file
* Return true on success
* @param p_filename
* @param p_meshes
* @param p_parserFlags
*/
virtual bool LoadModel
(
const std::string& p_fileName,
std::vector<Mesh*>& p_meshes,
std::vector<std::string>& p_materials,
EModelParserFlags p_parserFlags
) = 0;
};
我们可以看到这是一个用于解析模型的接口类,其中只有一个纯虚函数,通过注释我们了解到,它的作用是从文件中加载网格信息。函数的参数包括了文件名、网格容器的引用、材质贴图的引用,以及一个EModelParserFlags类型的解析器枚举值。
LoadModel将会返回当前文件的网格是否加载成功,它的具体操作将会在派生的子类中实现。
2.EModelParserFlags
下列的枚举值与内联函数是对EModelParserFlags的补充,主要用于对模型中的区分与识别不同的模型节点,并对其进行算术运算。
/**
* Some flags that can be used for model parsing
*/
enum class EModelParserFlags : uint32_t
{
NONE = 0x0,
CALC_TANGENT_SPACE = 0x1,
JOIN_IDENTICAL_VERTICES = 0x2,
MAKE_LEFT_HANDED = 0x4,
TRIANGULATE = 0x8,
REMOVE_COMPONENT = 0x10,
GEN_NORMALS = 0x20,
GEN_SMOOTH_NORMALS = 0x40,
SPLIT_LARGE_MESHES = 0x80,
PRE_TRANSFORM_VERTICES = 0x100,
LIMIT_BONE_WEIGHTS = 0x200,
VALIDATE_DATA_STRUCTURE = 0x400,
IMPROVE_CACHE_LOCALITY = 0x800,
REMOVE_REDUNDANT_MATERIALS = 0x1000,
FIX_INFACING_NORMALS = 0x2000,
SORT_BY_PTYPE = 0x8000,
FIND_DEGENERATES = 0x10000,
FIND_INVALID_DATA = 0x20000,
GEN_UV_COORDS = 0x40000,
TRANSFORM_UV_COORDS = 0x80000,
FIND_INSTANCES = 0x100000,
OPTIMIZE_MESHES = 0x200000,
OPTIMIZE_GRAPH = 0x400000,
FLIP_UVS = 0x800000,
FLIP_WINDING_ORDER = 0x1000000,
SPLIT_BY_BONE_COUNT = 0x2000000,
DEBONE = 0x4000000,
GLOBAL_SCALE = 0x8000000,
EMBED_TEXTURES = 0x10000000,
FORCE_GEN_NORMALS = 0x20000000,
DROP_NORMALS = 0x40000000,
GEN_BOUNDING_BOXES = 0x80000000
};
inline EModelParserFlags operator~ (EModelParserFlags a) { return (EModelParserFlags)~(int)a; }
inline EModelParserFlags operator| (EModelParserFlags a, EModelParserFlags b) { return (EModelParserFlags)((int)a | (int)b); }
inline EModelParserFlags operator& (EModelParserFlags a, EModelParserFlags b) { return (EModelParserFlags)((int)a & (int)b); }
inline EModelParserFlags operator^ (EModelParserFlags a, EModelParserFlags b) { return (EModelParserFlags)((int)a ^ (int)b); }
inline EModelParserFlags& operator|= (EModelParserFlags& a, EModelParserFlags b) { return (EModelParserFlags&)((int&)a |= (int)b); }
inline EModelParserFlags& operator&= (EModelParserFlags& a, EModelParserFlags b) { return (EModelParserFlags&)((int&)a &= (int)b); }
inline EModelParserFlags& operator^= (EModelParserFlags& a, EModelParserFlags b) { return (EModelParserFlags&)((int&)a ^= (int)b); }
内联函数多是基于EModelParserFlags类型的运算符重载,这里就不详细说明。
3.AssimpParser
/**
* A simple class to load assimp model data (Vertices only)
*/
class AssimpParser : public IModelParser
{
public:
/**
* Simply load meshes from a file using assimp
* Return true on success
* @param p_filename
* @param p_meshes
* @param p_parserFlags
*/
bool LoadModel
(
const std::string& p_fileName,
std::vector<Mesh*>& p_meshes,
std::vector<std::string>& p_materials,
EModelParserFlags p_parserFlags
) override;
private:
void ProcessMaterials(const struct aiScene* p_scene, std::vector<std::string>& p_materials);;
void ProcessNode(void* p_transform, struct aiNode* p_node, const struct aiScene* p_scene, std::vector<Mesh*>& p_meshes);
void ProcessMesh(void* p_transform, struct aiMesh* p_mesh, const struct aiScene* p_scene, std::vector<Geometry::Vertex>& p_outVertices, std::vector<uint32_t>& p_outIndices);
};
接下来就是ModelParser的子类AssimpParser,在介绍它的函数前,我们先来了解一个关键的模型加载库——Assimp。
在日常的图形程序中,通常都会使用非常复杂且好玩的模型,它们比静态的箱子要好看多了。然而,和箱子对象不同,我们不太能够对像是房子、汽车或者人形角色这样的复杂形状手工定义所有的顶点、法线和纹理坐标。我们想要的是将这些模型(Model)导入(Import)到程序当中。模型通常都由3D艺术家在Blender、3DS Max或者Maya这样的工具中精心制作。
这些所谓的3D建模工具(3D Modeling Tool)可以让艺术家创建复杂的形状,并使用一种叫做UV映射(uv-mapping)的手段来应用贴图。这些工具将会在导出到模型文件的时候自动生成所有的顶点坐标、顶点法线以及纹理坐标。这样子艺术家们即使不了解图形技术细节的情况下,也能拥有一套强大的工具来构建高品质的模型了。所有的技术细节都隐藏在了导出的模型文件中。但是,作为图形开发者,我们就必须要了解这些技术细节了。
所以,我们的工作就是解析这些导出的模型文件以及提取所有有用的信息,将它们储存为OpenGL能够理解的格式。一个很常见的问题是,模型的文件格式有很多种,每一种都会以它们自己的方式来导出模型数据。像是Wavefront的.obj这样的模型格式,只包含了模型数据以及材质信息,像是模型颜色和漫反射/镜面光贴图。而以XML为基础的Collada文件格式则非常的丰富,包含模型、光照、多种材质、动画数据、摄像机、完整的场景信息等等。Wavefront的.obj格式通常被认为是一个易于解析的模型格式。建议至少去Wavefront的wiki页面上看看文件格式的信息是如何封装的。这应该能让你认识到模型文件的基本结构。
总而言之,不同种类的文件格式有很多,它们之间通常并没有一个通用的结构。所以如果我们想从这些文件格式中导入模型的话,我们必须要去自己对每一种需要导入的文件格式写一个导入器。很幸运的是,正好有一个库专门处理这个问题。
一个非常流行的模型导入库是Assimp,它是Open Asset Import Library(开放的资产导入库)的缩写。Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。当Assimp加载完模型之后,我们就能够从Assimp的数据结构中提取我们所需的所有数据了。由于Assimp的数据结构保持不变,不论导入的是什么种类的文件格式,它都能够将我们从这些不同的文件格式中抽象出来,用同一种方式访问我们需要的数据。
当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:
- 和材质和网格(Mesh)一样,所有的场景/模型数据都包含在Scene对象中。Scene对象也包含了场景根节点的引用。
- 场景的Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。
- 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
- 一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的。
- 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。
所以,我们需要做的第一件事是将一个物体加载到Scene对象中,遍历节点,获取对应的Mesh对象(我们需要递归搜索每个节点的子节点),并处理每个Mesh对象来获取顶点数据、索引以及它的材质属性。最终的结果是一系列的网格数据,我们会将它们包含在一个Model 对象中。
了解了以上内容,我们来看AssimpParser类的具体函数。
3.1LoadModel
在函数的开头我们首先要引入Assimp,在这里利用的是“Assimp::Importer import”语句,之后利用Assimp内置的函数ReadFile将模型数据从文件中导入,所需的参数就是先前传入的参数。
bool OvRendering::Resources::Parsers::AssimpParser::LoadModel(const std::string & p_fileName, std::vector<Mesh*>& p_meshes, std::vector<std::string>& p_materials, EModelParserFlags p_parserFlags)
{
Assimp::Importer import;
const aiScene* scene = import.ReadFile(p_fileName, static_cast<unsigned int>(p_parserFlags));
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
return false;
ProcessMaterials(scene, p_materials);
aiMatrix4x4 identity;
ProcessNode(&identity, scene->mRootNode, scene, p_meshes);
return true;
}
当我们用scene指针存储返回ReadFile返回的结果后,可以利用它判断文件模型是否正确导入,是则进行接下来的操作;反之返回错误。
3.2ProcessMaterials
void OvRendering::Resources::Parsers::AssimpParser::ProcessMaterials(const aiScene * p_scene, std::vector<std::string>& p_materials)
{
for (uint32_t i = 0; i < p_scene->mNumMaterials; ++i)
{
aiMaterial* material = p_scene->mMaterials[i];
if (material)
{
aiString name;
aiGetMaterialString(material, AI_MATKEY_NAME, &name);
p_materials.push_back(name.C_Str());
}
}
}
该函数的作用在于处理材质信息,在该函数中,我们传入先前使用ReadFile函数获得的数据,并逐次遍历模型的材质数据的类一个节点,若当前节点不为空,则使用Assimp的API获取对应材质的关键字名称,并将其转换为字符串存入向量p_materials中。
3.3ProcessNode
void OvRendering::Resources::Parsers::AssimpParser::ProcessNode(void* p_transform, aiNode * p_node, const aiScene * p_scene, std::vector<Mesh*>& p_meshes)
{
aiMatrix4x4 nodeTransformation = *reinterpret_cast<aiMatrix4x4*>(p_transform) * p_node->mTransformation;
// Process all the node's meshes (if any)
for (uint32_t i = 0; i < p_node->mNumMeshes; ++i)
{
std::vector<Geometry::Vertex> vertices;
std::vector<uint32_t> indices;
aiMesh* mesh = p_scene->mMeshes[p_node->mMeshes[i]];
ProcessMesh(&nodeTransformation, mesh, p_scene, vertices, indices);
p_meshes.push_back(new Mesh(vertices, indices, mesh->mMaterialIndex)); // The model will handle mesh destruction
}
// Then do the same for each of its children
for (uint32_t i = 0; i < p_node->mNumChildren; ++i)
{
ProcessNode(&nodeTransformation, p_node->mChildren[i], p_scene, p_meshes);
}
}
该函数的作用在于处理模型节点(即单位网格)的信息,我们首先获取当前节点的变换矩阵,然后对所有的节点进行遍历,利用node的mMeshes信息作用索引,访问scene的mMeshes信息,并将开头获取的节点的变换矩阵与网格信息传入网格处理函数ProcessMesh中,最后利用修改后的网格信息构造新的Mesh实例并存入向量p_meshes。
由于当前节点可能拥有子节点,所以还需要遍历其子节点完成相同的操作。
3.4ProcessMesh
void OvRendering::Resources::Parsers::AssimpParser::ProcessMesh(void* p_transform, aiMesh* p_mesh, const aiScene* p_scene, std::vector<Geometry::Vertex>& p_outVertices, std::vector<uint32_t>& p_outIndices)
{
aiMatrix4x4 meshTransformation = *reinterpret_cast<aiMatrix4x4*>(p_transform);
for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i)
{
aiVector3D position = meshTransformation * p_mesh->mVertices[i];
aiVector3D normal = meshTransformation * (p_mesh->mNormals ? p_mesh->mNormals[i] : aiVector3D(0.0f, 0.0f, 0.0f));
aiVector3D texCoords = p_mesh->mTextureCoords[0] ? p_mesh->mTextureCoords[0][i] : aiVector3D(0.0f, 0.0f, 0.0f);
aiVector3D tangent = p_mesh->mTangents ? meshTransformation * p_mesh->mTangents[i] : aiVector3D(0.0f, 0.0f, 0.0f);
aiVector3D bitangent = p_mesh->mBitangents ? meshTransformation * p_mesh->mBitangents[i] : aiVector3D(0.0f, 0.0f, 0.0f);
p_outVertices.push_back
(
{
position.x,
position.y,
position.z,
texCoords.x,
texCoords.y,
normal.x,
normal.y,
normal.z,
tangent.x,
tangent.y,
tangent.z,
bitangent.x,
bitangent.y,
bitangent.z
}
);
}
for (uint32_t faceID = 0; faceID < p_mesh->mNumFaces; ++faceID)
{
auto& face = p_mesh->mFaces[faceID];
for (size_t indexID = 0; indexID < 3; ++indexID)
p_outIndices.push_back(face.mIndices[indexID]);
}
}
最后来看关键的网格处理函数,我们首先同样获取网格的变换信息,并将对应的变换作用于网格的每一个顶点,包括处理顶点位置坐标(position)、纹理坐标(texCoords)、法向量(normal)、切向量(tangent)与侧切向量(bitangent),然后将所有信息按序存入向量p_outVertices中。
我们可以看到除了position外,其他的数据可能出现指针为空的现象,为了做出应对,我们可以使用三目运算符判断指针的状态,当指针为空时,我们将对应的变量赋予一个默认值。
最后我们还需要对网格的每个面做处理,因为我们的网格是四边形结构,所以需要将四个顶点的索引存入p_outIndices中。
(博客内容参考LearnOpenGL)
|