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


歡迎關注我的公衆號 江達小記

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