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
。要加載網格,我們需要執行以下操作:
- 計算索引偏移量(
Mesh::indexOffset
)。這將告訴我們該網格的數據在緩衝區中的何處開始。 - 將所有頂點數據從
aiMesh::mVertices[]
複製到我們的頂點緩衝區(ModelLoader::m_vertices
)。 - 將所有法線數據從
aiMesh::mNormals[]
複製到我們的法線緩衝區(ModelLoader::m_normals
)。 - (可選,本教程未介紹)複製紋理相關數據。
- 計算索引數據並添加到我們的索引緩衝區(
ModelLoader::m_indices
)。 - 設置網格的索引計數(
Mesh::indexCount
),這是網格中的頂點數。 - 設置網格的材質(
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
實現的步驟。我們需要執行以下步驟:
- (可選)設置節點的名稱。
- 設置節點的轉換矩陣。
- 將指針複製到該節點的每個網格。
- 添加子節點,併爲每個子節點調用
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模型
參考文獻
- Open Asset Import Library, accessed April 30, 2014, assimp.sourceforge.net
- Phong Shading, Wikipedia article, accessed April 30, 2014, en.wikipedia.org/wiki/Phong_shading
- Assimp Material System, accessed April 30, 2014, assimp.sourceforge.net/lib_html/materials.html
- 此博客文章的可下載代碼:OpenGL博客文章文件
關於作者
Eric Stone
Eric是ICS的軟件工程師,具有使用C++進行編程的豐富經驗。他已經使用Qt和OpenGL進行編程超過六年,並且在開發臺式機和嵌入式設備上的應用程序方面具有實踐經驗。
原文:https://www.ics.com/blog/qt-and-opengl-loading-3d-model-open-asset-import-library-assimp
歡迎關注我的公衆號 江達小記