深度了解OpenGL:利用Assimp导入模型
背景
到现在为止我们都在使用手动生成的模型。正如你所想的,指明每个顶点的位置和其他属性有点时候并不是十分方便。对于一个箱子、锥体和简单平面还好,但是像人们的脸怎么办?现实的商业应用和游戏中,程序中使用模型一般都是由美术人员通过如 Blender, Maya 或 3ds Max 等建模软件来解决这个问题。这些软件提供了高级的工具帮助他们创造很复杂的模型。模型完成后可以以不同格式的文件保存。文件中包含了这个模型的所有几何解释。这些文件可以被加载到一个游戏引擎(提供支持特定格式的引擎)里面,文件中的内容可用来填充到渲染所需要的顶点缓存和索引缓存中。使用这些专业的模型对于场景效果的提升是十分关键的。
自己开发解析器将耗费你很长的时间。如果你想从不同格式的模型文件中加载模型,你需要学习每一种格式然后为其开发一个特定的解释器。有些模型的格式简单,但是一些也很复杂,你最终可能花费很多时间在这些并不是 3D 设计的核心内容上。因此,这章介绍的内容就是使用外加的库负责从文件中解释和加载模型。
Open Asset Import Library,也称 Assimp,是一个可以处理许多 3D 格式的开源库,包括最受欢迎的二进制反码格式。它是跨平台的,可用于 Linux 和 Windows,非常容易使用和嵌入到以 C/C++ 程序中。
这课没有太多的理论。让我们直接去看如何使用 Assimp 库中提供的函数来导入 3D 模型。在开始之前,请先确认你已经从上面的链接安装了 Assimp。
(mesh.h:50)
class Mesh
{
public:
Mesh();
~Mesh();
bool LoadMesh(const std::string& Filename);
void Render();
private:
bool InitFromScene(const aiScene* pScene, const std::string& Filename);
void InitMesh(unsigned int Index, const aiMesh* paiMesh);
bool InitMaterials(const aiScene* pScene, const std::string& Filename);
void Clear();
struct MeshEntry {
MeshEntry();
~MeshEntry();
bool Init(const std::vector& Vertices,
const std::vector& Indices);
GLuint VB;
GLuint IB;
unsigned int NumIndices;
unsigned int MaterialIndex;
};
std::vector<MeshEntry> m_Entries;
std::vector<Texture*> m_Textures;
};
这个 Mesh 类是 Assimp 和 OpenGL 程序之间的接口。这个类使用一个文件名参数作为 LoadMesh() 函数的参数,借助于 Assimp 加载模型,然后我们对加载进的模型数据进行解析,并将这些模型数据填充到顶点缓存、索引缓存以及纹理对象。为了渲染 mesh,我们定义了 Render() 函数。Mesh 类的内部结构与 Assimp 加载模型的方法相匹配。Assimp 用一个
aiScene 对象来代表被加载的 mesh,
aiScene 对象中封装了包含模型各部分的 mesh 结构体。
aiScene 对象中必须至少含有一个 mesh 结构,复杂的模型可以包含多个 mesh 结构。 Mesh 类的成员 m_Entries 是 MeshEntry 结构体向量,其中的每个结构体和
aiScene 对象中的一个 mesh 结构相对应。这个结构体包含顶点缓存、索引缓存和材质的索引。目前一个材质只是一个纹理,又因为 mesh 实体可以共享材质,所以我们给每个材质( m_Textures )单独设一个向量。MeshEntry::MaterialIndex 指向 m_Textures 里面的一个纹理。
(mesh.cpp:77)
bool Mesh::LoadMesh(const std::string& Filename)
{
// Release the previously loaded mesh (if it exists)
Clear();
bool Ret = false;
Assimp::Importer Importer;
const aiScene* pScene = Importer.ReadFile(Filename.c_str(), aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs);
if (pScene) {
Ret = InitFromScene(pScene, Filename);
}
else {
printf("Error parsing '%s': '%s'\n", Filename.c_str(), Importer.GetErrorString());
}
return Ret;
}
加载mesh从这个函数开始。我们在栈上创建了一个 Assimp::Importer 类的实例,并且调用它的读文件(ReadFile)方法。这个函数需要两个参数:模型文件的完整路径和一些处理的选项。 Assimp 能对加载的模型执行很多实用的优化。例如,为没有法线的模型生成法线,优化模型的结构来提高性能等,我们可以根据需要来选择合适的操作。在这里我们使用了其提供的三个操作:第一个是
aiProcess_Triangulate ,它将不是由三角组成的模型转换为基于三角形的网格模型。例如:一个四边形 mesh 可以通过从其中的每个四边形生成两个三角形而被变换为三角形 mesh; 第二个操作是
aiProcess_GenSmoothNormals ,为那些原来不含顶点法线的模型生成顶点法线。记住这些加工方式是非重叠的位掩码,因此你可以使用"或"运算来对这些操作进行组合;第三个操作是
aiProcess_FlipUVsv ,沿着 y 轴翻转纹理坐标。你需要根据导入的模型数据来选择合适的操作。如果 mesh 成功加载,我们获得一个指向
aiScene 对象的指针。这个对象包含整个模型的内容,模型的不同结构都保持在一个
aiMesh 结构中。接下来我们调用 InitFromScene() 函数来初始化 Mesh 对象。
(mesh.cpp:97)
bool Mesh::InitFromScene(const aiScene* pScene, const std::string& Filename)
{
m_Entries.resize(pScene->mNumMeshes);
m_Textures.resize(pScene->mNumMaterials);
// Initialize the meshes in the scene one by one
for (unsigned int i = 0 ; i < m_Entries.size() ; i++) {
const aiMesh* paiMesh = pScene->mMeshes[i];
InitMesh(i, paiMesh);
}
return InitMaterials(pScene, Filename);
}
首先我们根据需要用到的 m_Entries 和 m_Textures 数量来为其分配存储空间,其数目可经由
aiScene 对象中的成员 mNumMeshes 和 mNumMaterials 得到。接下来我们遍历
aiScene 对象中的 mMeshes 数组,并挨个儿初始化 m_Entries 实例。最后初始化材质。
(mesh.cpp:111)
void Mesh::InitMesh(unsigned int Index, const aiMesh* paiMesh)
{
m_Entries[Index].MaterialIndex = paiMesh->mMaterialIndex;
std::vector Vertices;
std::vector Indices;
...
首先我们记录下当前 mesh 的材质索引,在渲染过程中将通过它来找到 mesh 对应的正确材质。接下来,我们创建两个 STL 容器来储存顶点和索引缓冲器的内容。STL 容器有一个
很好的特性:能够在连续的缓冲区中储存数据,这使得将数据加载到 OpenGL 缓存中变得很容易(使用 glBufferData() 函数)。
(mesh.cpp:118)
const aiVector3D Zero3D(0.0f, 0.0f, 0.0f);
for (unsigned int i = 0 ; i < paiMesh->mNumVertices ; i++) {
const aiVector3D* pPos = &(paiMesh->mVertices[i]);
const aiVector3D* pNormal = &(paiMesh->mNormals[i]) : &Zero3D;
const aiVector3D* pTexCoord = paiMesh->HasTextureCoords(0) ? &(paiMesh->mTextureCoords[0][i]) : &Zero3D;
Vertex v(Vector3f(pPos->x, pPos->y, pPos->z),
Vector2f(pTexCoord->x, pTexCoord->y),
Vector3f(pNormal->x, pNormal->y, pNormal->z));
Vertices.push_back(v);
}
...
这里我们通过对模型数据的解析将顶点属性数据依次存放到我们的 Vertices 容器中。我们使用到
aiMesh 类中下面的一些方法:
- mNumVertices - 顶点数量
- mVertices - 包含位置属性的数组
- mNormals - 包含顶点法线属性的数组
- mTextureCoords - 包含纹理坐标数组,这是一个二维数组,因为每个顶点可以拥有多个纹理坐标。
因此,总的来说我们有三个相互独立的数组,它们囊括了所有我们需要的顶点信息,我们可以通过这些信息来构建我们最终的顶点结构体。注意一些模型没有纹理坐标,所以在访问mTextureCoords数组之前(可能会引发错误),我们应该通过调用
HasTextureCoords() 来检查纹理是否存在。除此之外,一个 mesh 的每个顶点可以包含多个纹理坐标。在这章,我们只是简单地使用其第一个纹理坐标。因此 mTextureCoords 数组(二维的)始终只有第一行的值被访问。如果纹理坐标不存在,我们将这个顶点的纹理坐标初始化为 0 向量。
推荐阅读
-
深度了解OpenGL:使用Assimp载入模型并绑定VAO
-
深度了解OpenGL:使用Assimp载入模型并绑定VAO
-
使用assimp库在OpenGL中导入模型
-
深度了解OpenGL:利用Assimp导入模型
-
搞定!OpenGL_05教程:使用Assimp导入网格和模型的功能设计
-
[姿势估计] 实践记录:使用 Dlib 和 mediapipe 进行人脸姿势估计 - 本文重点介绍方法 2):方法 1:基于深度学习的方法:。 基于深度学习的方法:基于深度学习的方法利用深度学习模型,如卷积神经网络(CNN)或递归神经网络(RNN),直接从人脸图像中学习姿势估计。这些方法能够学习更复杂的特征表征,并在大规模数据集上取得优异的性能。方法二:基于二维校准信息估计三维姿态信息(计算机视觉 PnP 问题)。 特征点定位:人脸姿态估计的第一步是通过特征点定位来检测和定位人脸的关键点,如眼睛、鼻子和嘴巴。这些关键点提供了人脸的局部结构信息,可用于后续的姿势估计。 旋转表示:常见的旋转表示方法包括欧拉角和旋转矩阵。欧拉角通过三个旋转角度(通常是俯仰、偏航和滚动)描述头部的旋转姿态。旋转矩阵是一个 3x3 矩阵,表示头部从一个坐标系到另一个坐标系的变换。 三维模型重建:根据特征点的定位结果,三维人脸模型可用于姿势估计。通过将人脸的二维图像映射到三维模型上,可以估算出人脸的旋转和平移信息。这就需要建立人脸的三维模型,然后通过优化方法将模型与特征点对齐,从而获得姿势估计结果。 特征点定位 特征点定位是用于检测人脸关键部位的五官基础部分,还有其他更多的特征点表示方法,大家可以参考我上一篇文章中介绍的特征点检测方案实践:人脸校正二次定位操作来解决人脸校正的问题,客户在检测关键点的代码上略有修改,坐标转换部分客户见上图 def get_face_info(image). img_copy = image.copy image.flags.writeable = False image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = face_detection.process(image) # 在图像上绘制人脸检测注释。 image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) box_info, facial = None, None if results.detections: for detection in results. for detection in results.detections: mp_drawing.Drawing.detection = 无 mp_drawing.draw_detection(image, detection) 面部 = detection.location_data.relative_keypoints 返回面部 在上述代码中,返回的数据是五官(6 个关键点的坐标),这是用 mediapipe 库实现的,下面我们可以尝试用另一个库:dlib 来实现。 使用 dlib 使用 Dlib 库在 Python 中实现人脸关键点检测的步骤如下: 确保已安装 Dlib 库,可使用以下命令: pip install dlib 导入必要的库: 加载 Dlib 的人脸检测器和关键点检测器模型: 读取图像并将其灰度化: 使用人脸检测器检测图像中的人脸: 对检测到的人脸进行遍历,并使用关键点检测器检测人脸关键点: 显示绘制了关键点的图像: 以下代码将参数 landmarks_part 添加到要返回的关键点坐标中。
-
UE4实战:实操教程 - 在运行时利用Assimp库导入各类3D模型(fbx、obj、gltf)的网格处理
-
搞定!OpenGL实战笔记第十八章:用Assimp导入自定义模型(Model类)-步骤详解