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

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 Vertices
    if (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 Normals
    if (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 indexes
    for (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 Stone

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

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


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章