翻译自: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
#include
#include
这是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模型
Eric是ICS的软件工程师,具有使用C++进行编程的丰富经验。他已经使用Qt和OpenGL进行编程超过六年,并且在开发台式机和嵌入式设备上的应用程序方面具有实践经验。
原文:https://www.ics.com/blog/qt-and-opengl-loading-3d-model-open-asset-import-library-assimp
欢迎关注我的公众号 江达小记