Qt和OpenGL:使用Open Asset Import Library(ASSIMP)加载3D模型

翻译自:https://www.ics.com/blog/qt-and-opengl-loading-3d-model-open-asset-import-library-assimp

By Eric Stone Wednesday, May 21, 2014

Twitter LinkedIn Facebook Reddit

这篇博客文章是该系列的第一篇文章,该系列文章将介绍如何将OpenGL与Qt一起使用。在本期中,我们将研究如何使用Open Asset Import Library(ASSIMP)(1)从某些常见3D模型格式加载3D模型。该示例代码需要Assimp 3.0以上版本。该代码还将使用Qt的多个便利类(QString,QVector,QSharedPointer等)。

阅读第2部分Qt和OpenGL:使用Open Asset Import Library(ASSIMP)加载3D模型

介绍

首先,我们将创建一些简单的类来保存模型的数据。结构MaterialInfo将包含有关材料外观的信息。我们将使用Phong着色模型(2)进行着色。

struct MaterialInfo
{QString Name;QVector3D Ambient;QVector3D Diffuse;QVector3D Specular;float Shininess;
};

LightInfo结构将包含有关光源的信息:

struct LightInfo
{QVector4D Position;QVector3D Intensity;
};

Mesh类将为我们提供有关网格的信息。它实际上不包含网格的顶点数据,但是具有我们需要从顶点缓冲区中获取的信息。 Mesh::indexCount是网格中的顶点数,Mesh::indexOffset是缓冲区中顶点数据开始的位置,Mesh::material是网格的材质信息。

struct Mesh
{QString name;unsigned int indexCount;unsigned int indexOffset;QSharedPointer<MaterialInfo> material;
};

单个模型可能具有许多不同的网格。 Node类将包含网格以及将其放置在场景中的转换矩阵。每个节点还可以具有子节点。我们可以将所有网格存储在单个数组中,但是将它们存储在树形结构中可以使我们更轻松地为对象设置动画。可以将其视为人体,就好像身体是根节点,上臂将是根节点的子节点,下臂将是上臂节点的子节点,而手将是下臂节点的子节点。

struct Node
{QString name;QMatrix4x4 transformation;QVector<QSharedPointer<Mesh> > meshes;QVector<Node> nodes;
};

ModelLoader类将用于将信息加载到单个根节点中:

class ModelLoader
{public:ModelLoader();bool Load(QString pathToFile);void getBufferData(QVector<float> **vertices, QVector<float> **normals,QVector<unsigned int> **indices);QSharedPointer<Node> getNodeData() { return m_rootNode; }

此类的用法将很简单。 ModelLoader::Load()接受3D模型文件的路径,并加载模型。 ModelLoader::getBufferData()用于检索已索引图形的顶点位置、法线和索引。 ModelLoader::getNodeData()将返回根节点。

下面是ModelLoader私有的函数和变量:

    QSharedPointer<MaterialInfo> processMaterial(aiMaterial *mater);QSharedPointer<Mesh> processMesh(aiMesh *mesh);void processNode(const aiScene *scene, aiNode *node, Node *parentNode, Node &newNode);void transformToUnitCoordinates();void findObjectDimensions(Node *node, QMatrix4x4 transformation, QVector3D &minDimension, QVector3D &maxDimension);QVector<float> m_vertices;QVector<float> m_normals;QVector<unsigned int> m_indices;QVector<QSharedPointer<MaterialInfo> > m_materials;QVector<QSharedPointer<Mesh> > m_meshes;QSharedPointer<Node> m_rootNode;

下一步是加载模型。如果没有安装Assimp 3.0,则必须安装Assimp 3.0。请注意,Assimp 2.0在此示例中不起作用。首先,我们包含必要的Assimp标头:

#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <assimp/Importer.hpp>

这是ModelLoader::Load()函数的代码:

bool ModelLoader::Load(QString pathToFile)
{Assimp::Importer importer;const aiScene* scene = importer.ReadFile(pathToFile.toStdString(),aiProcess_GenSmoothNormals |aiProcess_CalcTangentSpace |aiProcess_Triangulate |aiProcess_JoinIdenticalVertices |aiProcess_SortByPType);if (!scene){qDebug() << "Error loading file: (assimp:) " << importer.GetErrorString();return false;}

Assimp将有关模型的所有信息存储在此处创建的aiScene实例中。 importer对象保留了aiScene对象的所有权,因此我们不必担心以后其被删除。如果发生错误,返回的场景对象将为null,因此我们在此处进行检查,如果发生错误,则从函数返回false

有关传递给importer的标志的更多详细信息,请参见Assimp的postprocess.h文件。下面是上面提到的标志的介绍:

  • GenSmoothNormals:如果模型中没有法线,则生成法线。
  • CalcTangentSpace:计算切线空间,只有在进行法线贴图时才需要。
  • Triangulate:将具有三个以上顶点的图元拆分为三角形。
  • JoinIdenticalVertices:连接相同的顶点数据,并通过索引图形改善性能。

如果scene不为null,那么我们可以假设模型已正确加载并开始复制所需的数据。数据将按以下顺序读取:

1.材质(Materials)
2.网格(Meshes)
3.节点(Nodes)

材质必须在网格之前加载,而网格必须在节点之前加载。

加载材质

下一步是加载材质:

    if (scene->HasMaterials()){for (unsigned int ii = 0; ii < scene->mNumMaterials; ++ii){QSharedPointer<MaterialInfo> mater = processMaterial(scene->mMaterials[ii]);m_materials.push_back(mater);}}

所有材质都存储在aiScene::mMaterials数组中,并且数组大小为aiScene::nNumMaterials。我们遍历每个对象,并将其传递给我们的processMaterial函数,该函数将向我们返回一个新的MaterialInfo对象。然后,变量m_materials将包含场景中所有网格的材质信息(如果可用)。

让我们仔细看看我们将使用的ModelLoader::processMaterial实现:

QSharedPointer<MaterialInfo> ModelLoader::processMaterial(aiMaterial *material)
{QSharedPointer<MaterialInfo> mater(new MaterialInfo);aiString mname;material->Get(AI_MATKEY_NAME, mname);if (mname.length > 0)mater->Name = mname.C_Str();int shadingModel;material->Get(AI_MATKEY_SHADING_MODEL, shadingModel);if (shadingModel != aiShadingMode_Phong && shadingModel != aiShadingMode_Gouraud){qDebug() << "This mesh's shading model is not implemented in this loader, setting to default material";mater->Name = "DefaultMaterial";}else...

aiMaterial类使用键值对存储材质数据。我们复制名称,然后检查这种材质的照明模型。在本教程中,我们仅需关注Phong或Gouraud着色模型,因此,如果不是其中之一,则将名称设置为“DefaultMaterial”以表明渲染应使用其自身的材质值。

继续上面的代码:

    ...}else{aiColor3D dif(0.f,0.f,0.f);aiColor3D amb(0.f,0.f,0.f);aiColor3D spec(0.f,0.f,0.f);float shine = 0.0;material->Get(AI_MATKEY_COLOR_AMBIENT, amb);material->Get(AI_MATKEY_COLOR_DIFFUSE, dif);material->Get(AI_MATKEY_COLOR_SPECULAR, spec);material->Get(AI_MATKEY_SHININESS, shine);mater->Ambient = QVector3D(amb.r, amb.g, amb.b);mater->Diffuse = QVector3D(dif.r, dif.g, dif.b);mater->Specular = QVector3D(spec.r, spec.g, spec.b);mater->Shininess = shine;mater->Ambient *= .2;if (mater->Shininess == 0.0)mater->Shininess = 30;}return mater;
}

我们只对环境光照,漫反射,镜面反射和光泽特性感兴趣。您可以在此处(3)中看到更长的可用属性列表。调用aiMaterial::Get(key, value)获取所需的值,然后将其复制到MaterialInfo对象。

请注意,我们在此处缩小了环境光照值。这是因为我们用于渲染的OpenGL着色器只会针对环境光照,漫反射和镜面入射光使用同一个照明强度向量(LightInfo::Intensity)。另外,我们的着色器可以对光源的环境光照,漫反射和镜面反射分量使用单独的矢量,以实现更好的控制。我们还检查是否为模型指定了亮度值,如果没有,则将默认值设置为30。

加载网格

回到 Load() 函数中:

    if (scene->HasMeshes()){for (unsigned int ii = 0; ii < scene->mNumMeshes; ++ii){m_meshes.push_back(processMesh(scene->mMeshes[ii]));}}else{qDebug() << "Error: No meshes found";return false;}

所有网格都存储在aiScene::mMeshes数组中,并且数组大小为aiScene::nNumMeshes。我们遍历每个对象,并将其传递给我们的ModelLoader::processMesh函数,该函数将为我们返回一个新的Mesh对象。变量m_meshes将包含场景中的所有网格。

此时,每个网格将与一种材质相关联。如果在模型中未指定任何材质,它将具有默认材质,其MaterialInfo::Name设置为DefaultMaterial。要加载网格,我们需要执行以下操作:

  1. 计算索引偏移量(Mesh::indexOffset)。这将告诉我们该网格的数据在缓冲区中的何处开始。
  2. 将所有顶点数据从aiMesh::mVertices[]复制到我们的顶点缓冲区(ModelLoader::m_vertices)。
  3. 将所有法线数据从aiMesh::mNormals[]复制到我们的法线缓冲区(ModelLoader::m_normals)。
  4. (可选,本教程未介绍)复制纹理相关数据。
  5. 计算索引数据并添加到我们的索引缓冲区(ModelLoader::m_indices)。
  6. 设置网格的索引计数(Mesh::indexCount),这是网格中的顶点数。
  7. 设置网格的材质(Mesh::material)。
QSharedPointer<Mesh> ModelLoader::processMesh(aiMesh *mesh)
{QSharedPointer<Mesh> newMesh(new Mesh);newMesh->name = mesh->mName.length != 0 ? mesh->mName.C_Str() : "";newMesh->indexOffset = m_indices.size();unsigned int indexCountBefore = m_indices.size();int vertindexoffset = m_vertices.size()/3;// Get Verticesif (mesh->mNumVertices > 0){for (uint ii = 0; ii < mesh->mNumVertices; ++ii){aiVector3D &vec = mesh->mVertices[ii];m_vertices.push_back(vec.x);m_vertices.push_back(vec.y);m_vertices.push_back(vec.z);}}// Get Normalsif (mesh->HasNormals()){for (uint ii = 0; ii < mesh->mNumVertices; ++ii){aiVector3D &vec = mesh->mNormals[ii];m_normals.push_back(vec.x);m_normals.push_back(vec.y);m_normals.push_back(vec.z);};}// Get mesh indexesfor (uint t = 0; t < mesh->mNumFaces; ++t){aiFace* face = &mesh->mFaces[t];if (face->mNumIndices != 3){qDebug() << "Warning: Mesh face with not exactly 3 indices, ignoring this primitive.";continue;}m_indices.push_back(face->mIndices[0]+vertindexoffset);m_indices.push_back(face->mIndices[1]+vertindexoffset);m_indices.push_back(face->mIndices[2]+vertindexoffset);}newMesh->indexCount = m_indices.size() - indexCountBefore;newMesh->material = m_materials.at(mesh->mMaterialIndex);return newMesh;
}

其中大多数很简单。由于每个顶点只使用一个缓冲区(Assimp每个网格有一个缓冲区),因此需要将偏移量添加到索引值。

aiMesh将索引数据存储在aiFace对象数组中。 aiFace表示一个基本绘制图形。如果face的索引数量不等于3,则它不是三角形,因此在本教程中我们将忽略它。

如果face是三角形,则将索引值添加到m_indices。请记住要向其中添加顶点偏移值,因为Assimp给出的索引是相对于网格的,而我们将所有网格的索引存储在一个缓冲区中。

由于我们已经处理了该网格的所有索引,因此现在我们可以计算该网格的索引计数,并设置网格的材质。

在本教程中,我们仅关注顶点,法线和索引,但是您可以在此处加载其他信息,例如顶点纹理坐标或切线。可下载的示例代码(4)也包含获得这些信息的函数。

加载节点

接下来,我们必须从根节点开始处理aiScene中的节点。节点定义相对于彼此绘制网格的位置。确保aiScene的根节点不为null,然后将其传递给processNode(),这将实现用所有模型数据填充ModelLoader::m_rootNode

    if (scene->mRootNode != NULL){Node *rootNode = new Node;processNode(scene, scene->mRootNode, 0, *rootNode);m_rootNode.reset(rootNode);}else{qDebug() << "Error loading model";return false;}return true;
}

这是processNode实现的步骤。我们需要执行以下步骤:

  1. (可选)设置节点的名称。
  2. 设置节点的转换矩阵。
  3. 将指针复制到该节点的每个网格。
  4. 添加子节点,并为每个子节点调用ModelLoader::processNode。这将递归处理所有子级。
void ModelLoader::processNode(const aiScene *scene, aiNode *node, Node *parentNode, Node &newNode)
{newNode.name = node->mName.length != 0 ? node->mName.C_Str() : "";newNode.transformation = QMatrix4x4(node->mTransformation[0]);newNode.meshes.resize(node->mNumMeshes);for (uint imesh = 0; imesh < node->mNumMeshes; ++imesh){QSharedPointer<Mesh> mesh = m_meshes[node->mMeshes[imesh]];newNode.meshes[imesh] = mesh;}for (uint ich = 0; ich < node->mNumChildren; ++ich){newNode.nodes.push_back(Node());processNode(scene, node->mChildren[ich], parentNode, newNode.nodes[ich]);}
}

收尾工作

该类可以如下使用:

    ModelLoader model;if (!model.Load("head.3ds")){m_error = true;return;}QVector<float> *vertices;QVector<float> *normals;QVector<unsigned int> *indices;model.getBufferData(&vertices, &normals, &indices);m_rootNode = model.getNodeData();

至此,您已经拥有了使用OpenGL显示模型所需的所有数据。

可下载示例(4)的完整源代码,包括qmake项目文件。如果存在Assimp 3以上的库,则它可以在任何平台上运行。您可能需要调整项目文件中的路径。在最新版本的Linux(例如Ubuntu)上,合适的版本的Assimp可作为Linux发行版的一部分提供。在Mac和Windows上,您可能需要从源代码构建Assimp。着色器和场景类有两组,一组用于OpenGL 3.3,另一组用于OpenGL 2.1 / OpenGL ES2。它将尝试运行3.3版本,但在必要时应自动回退到GL 2版本。

总结

这篇博客文章演示了如何使用Qt和Assimp库加载3D模型。

阅读第2部分Qt和OpenGL:使用Open Asset Import Library(ASSIMP)加载3D模型

参考文献

  1. Open Asset Import Library, accessed April 30, 2014, assimp.sourceforge.net
  2. Phong Shading, Wikipedia article, accessed April 30, 2014, en.wikipedia.org/wiki/Phong_shading
  3. Assimp Material System, accessed April 30, 2014, assimp.sourceforge.net/lib_html/materials.html
  4. 此博客文章的可下载代码:OpenGL博客文章文件

关于作者

Eric Stone

Eric是ICS的软件工程师,具有使用C++进行编程的丰富经验。他已经使用Qt和OpenGL进行编程超过六年,并且在开发台式机和嵌入式设备上的应用程序方面具有实践经验。

原文:https://www.ics.com/blog/qt-and-opengl-loading-3d-model-open-asset-import-library-assimp


欢迎关注我的公众号 江达小记

Qt和OpenGL:使用Open Asset Import Library(ASSIMP)加载3D模型相关推荐

  1. Qt Quick 3D系列(一):加载3d模型

    如果我们想在QML中使用3D且你之前没有三维程序开发的基础,使用Qt Quick 3D是个不错的选择,下面我介绍如何使用Qt Quick 3D加载3d模型.注意:Qt Quick 3D从Qt 5.15 ...

  2. OpenGL ES 加载3D模型

    前面绘制的矩形.立方体确实确实让人看烦了,并且实际生活中的物体是非常复杂的,我们不可能像前面哪样指定顶点来绘制,因此本篇博客就说明通过OpenGL ES加载一个3D模型.这样复杂物体的设计工作就可以交 ...

  3. qt opengl 加载3d模型(obj格式)

    和一般c++程序加载3d模型一样,解读出数据内容,再用一个常规的着色程序就可以了. 我实现的效果如下,采用的免费模型 实现思路和前面的略有不同,就是把自己生成顶点.纹理.法线的过程变成从文件读取了. ...

  4. Open Asset Import Library

    2019独角兽企业重金招聘Python工程师标准>>> 今天无意中发现了个可以读取各种3等格式的库:Open Asset Import Library.缩略名:Assimp. 比较爽 ...

  5. OpenGL教程翻译 第二十二课 使用Assimp加载模型

    第二十二课 使用Assimp加载模型 原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载) 背景 到现在为止我们都在使用手动生成的模型.正如你所想的,指明每个顶点 ...

  6. OpenGL深入探索——使用Assimp加载模型

    转载自:第二十二课 使用Assimp加载模型 背景 到现在为止我们都在使用手动生成的模型.正如你所想的,指明每个顶点的位置和其他属性有点时候并不是十分方便.对于一个箱子.锥体和简单平面还好,但是像人们 ...

  7. OpenGL模型加载之模型

    参考: https://learnopenglcn.github.io/03%20Model%20Loading/03%20Model/ 定义一个模型类 class Model {public:/* ...

  8. reactjs通过lazy函数配合import函数动态加载路由组件

    路由组件的lazyLoad //1.通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包const Login = lazy(()=>i ...

  9. OpenGL通过Assimp加载模型

    OpenGL通过Assimp加载模型 OpenGL通过Assimp加载模型简介 源代码剖析 主要源代码 OpenGL通过Assimp加载模型简介 到目前为止,我们已经使用了手动创建的模型.如您所见,为 ...

最新文章

  1. Adam那么棒,为什么还对SGD念念不忘 (1) —— 一个框架看懂优化算法
  2. oracle19c怎么创建Scott,Oracle db-sample-schema-19c安装(scott hr oe pm ix sh bi用户创建部署)...
  3. 两种方法设置html表格的宽高
  4. 全球及中国汽车流通行业营销模式及十四五竞争格局展望报告2021-2027年
  5. android ui自动化测试框架有哪些,自动化测试框架对比(UIAutomator、Appium、Robotium)...
  6. android耳机广播,Android利用广播实现耳机的线控
  7. 一款不错的编程字体Source Code Pro
  8. 不可求的电脑上必备软件,你也许听过
  9. 本地VM安装虚拟机,使用xshell连接
  10. 自定义Android中Dialog的弹出动画
  11. 阶段3 1.Mybatis_11.Mybatis的缓存_2 延迟加载和立即加载的概念
  12. 反射:类,构造器,方法使用
  13. windows 用户基本查看命令
  14. cns/clns搭建给clnc(udp转发)
  15. JavaWeb后端代码自动生成工具
  16. winserver2012设置开机自启动
  17. 利用python转载朋友微信表情包
  18. tp5.1 db助手与db::name混合使用数据库操作失效
  19. webpack2.0+ vue2.0
  20. [bzoj1455]罗马游戏

热门文章

  1. java做的企业网站源码 java开发的公司网站源码 java ssm框架开发的门户网站源码 java 企业官网源代码公司门户网站模板源码带后台SSM框架开发建设
  2. 搜狐白社会邀请bai.sohu.com
  3. 关于外部FLASH芯片的初步使用—以M25P80为例
  4. Asp.net MVC中表单验证
  5. 【合泰HT32F52352初次使用之LED闪烁】
  6. zip的mysql_.zip压缩版MySql的安装( )
  7. STM32_TIM输出PWM波形
  8. 运营商大数据精准获客是怎么做到的?企业如何以低成本获取精准客户?
  9. C语言/C++常见习题问答集锦(七十四) 之裨补阙漏
  10. Uncaught TypeError: Converting circular structure to JSON