如果你是中途开始学习本教程的,即使你对OpenGL已经非常熟悉,请至少了解以下几个章节,因为Qt中提供了OpenGL的很多便捷操作,熟悉这些操作可以让我们在Qt中高效的使用OpenGL进行绘图。
模型
现在是时候着手启用Assimp,并开始创建实际的加载和转换代码了。我们重新创建一个项目,回忆一下是否还能想起如何创建opengl窗口?
——创建新项目(ModelLoading)
——以QWidget为基类,取消勾选创建界面
——修改Widget公有继承自 QOpenGLWidget,QOpenGLExtraFunctions(注意引入头文件)
——实现三个虚函数
——在initializeGL中初始化opengl
——在paintGL中清屏
——在resizeGL中设置Viewport
绘制图形还需要我们创建一个着色器程序以及两个着色器文件
——添加成员变量 QOpenGLShaderProgram shaderProgram;
——创建qrc资源文件夹
——在资源文件夹中创建顶点着色器(model.vert)和片段着色器(model.frag)
——在initializeGL添加着色器,并进行编译链接
如果你创建成功,你的项目结构应该是这样的:
开始模型加载之旅
原learn opengl教程中提供的模型加载类不是很完善,最重要的是与Qt的结构的兼容性不是很好,这里我并不打算讲怎么去构建一个模型加载类,我只选取其中有用的部分来讲解
上节中,我们已经编译了一个可供使用的Assimp库,我们只需要在当前项目中导入库的文件以及包含目录即可(传送门)
Assimp加载模型使用的是一个Importer类,使用它可以从文件加载3D模型,转换成一个aiScene*对象。
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
这里我们先声明一个Importer
对象,它的名字空间是Assimp
,然后调用它的ReadFile
函数。这个函数需要一个文件路径,第二个参数是后处理(post-processing)选项。除了可以简单加载文件外,Assimp允许我们定义几个选项来强制Assimp去对导入数据做一些额外的计算或操作。通过设置aiProcess_Triangulate
,我们告诉Assimp如果模型不是(全部)由三角形组成,应该转换所有的模型的原始几何形状为三角形。aiProcess_FlipUVs
基于y轴翻转纹理坐标,在处理的时候是必须的(你可能记得,我们在纹理教程中,我们说过在OpenGL大多数图像会被沿着y轴反转,所以这个小小的后处理选项会为我们修正这个)。一少部分其他有用的选项如下:
aiProcess_GenNormals
: 如果模型没有包含法线向量,就为每个顶点创建法线。aiProcess_SplitLargeMeshes
: 把大的网格成几个小的的下级网格,当你渲染有一个最大数量顶点的限制时或者只能处理小块网格时很有用。aiProcess_OptimizeMeshes
: 和上个选项相反,它把几个网格结合为一个更大的网格。以减少绘制函数调用的次数的方式来优化。
aiScene*结构图
Scene就是我们加载成功之后的模型数据,我们现在要做的,只是读取Scene中的数据来进行绘图。
Scene里有什么呢?
一些整体属性,比方说材质、纹理、动画......
用于获取所有绘图数据(顶点、索引、纹理坐标...)的根节点。
我们要做什么?
Scene包含了模型的所有数据,我们要做的只是从Scene中读取这些数据,并将这些数据按我们的数据结构来存储。大致过程就是从根节点开始,递归遍历子节点,为子节点的每一个网格创建一个单独的VAO、VBO、EBO以及模型矩阵。当我们需要绘制模型时,只需要调用模型的draw函数。
你对模型加载类的实现感兴趣吗?
不,我觉得你不会感兴趣(要把这个说清楚简直是要我的命)。
因此,我直接上代码吧!
你只需要在你的项目中添加两个类Mesh,Model:
模型加载类
mesh.h
#ifndef MESH_H
#define MESH_H
#include <QString>
#include <QVector>
#include <QVector2D>
#include <QVector3D>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLExtraFunctions>
#include <QOpenGLTexture>
#include <QOpenGLWidget>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
struct Vertex
{
QVector3D Position;
QVector3D Normal;
QVector2D TexCoords;
QVector3D Tangent;
QVector3D Bitangent;
};
struct Texture
{
QOpenGLTexture texture;
QString type;
QString path;
Texture():texture(QOpenGLTexture::Target2D){
texture.create();
texture.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
texture.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);
texture.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
}
};
class Mesh {
public:
/* 网格数据 */
QVector<Vertex> vertices; //顶点数据
QVector<unsigned int> indices; //索引数组
QVector<Texture*> textures; //纹理数据
QMatrix4x4 model; //模型矩阵
QOpenGLFunctions* glFunc; //opengl函数入口
QOpenGLShaderProgram* shaderProgram; //着色器程序
/* 函数 */
Mesh(QOpenGLFunctions* glFunc,QOpenGLShaderProgram* shaderProgram,aiMatrix4x4 model);
void Draw();
void setupMesh();
private:
/* 渲染数据 */
QOpenGLVertexArrayObject VAO;
QOpenGLBuffer VBO,EBO;
};
#endif // MESH_H
mesh.cpp
#include "mesh.h"
#include <QtOpenGLExtensions/QOpenGLExtensions>
Mesh::Mesh(QOpenGLFunctions* glFunc, QOpenGLShaderProgram* shaderProgram, aiMatrix4x4 model)
: glFunc(glFunc)
, shaderProgram(shaderProgram)
, VBO(QOpenGLBuffer::VertexBuffer)
, EBO(QOpenGLBuffer::IndexBuffer)
{
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
this->model(i,j)=model[i][j];
}
}
}
void Mesh::Draw()
{
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
unsigned int normalNr = 1;
unsigned int heightNr = 1;
for(unsigned int i = 0; i < textures.size(); i++)
{
glFunc->glActiveTexture(GL_TEXTURE0 + i); // 在绑定之前激活相应的纹理单元
// 获取纹理序号(diffuse_textureN 中的 N)
QString number;
QString name = textures[i]->type;
if(name == "texture_diffuse")
number = QString::number(diffuseNr++);
else if(name == "texture_specular")
number = QString::number(specularNr++);
else if(name == "texture_normal")
number = QString::number(normalNr++); // transfer unsigned int to stream
else if(name == "texture_height")
number = QString::number(heightNr++); // transfer unsigned int to stream
textures[i]->texture.bind();
shaderProgram->setUniformValue((name + number).toStdString().c_str(),i);
}
// 绘制网格
QOpenGLVertexArrayObject::Binder bind(&VAO);
shaderProgram->setUniformValue("model",model);
glFunc->glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
}
void Mesh::setupMesh()
{
VAO.create();
VAO.bind();
VBO.create();
EBO.create();
VBO.bind();
VBO.allocate(vertices.data(),vertices.size() * sizeof(Vertex));
EBO.bind();
EBO.allocate(indices.data(),indices.size() * sizeof(unsigned int));
shaderProgram->enableAttributeArray(0);
shaderProgram->setAttributeBuffer(0,GL_FLOAT,0,3,sizeof(Vertex));
shaderProgram->enableAttributeArray(1);
shaderProgram->setAttributeBuffer(1,GL_FLOAT,offsetof(Vertex,Normal),3,sizeof(Vertex));
shaderProgram->enableAttributeArray(2);
shaderProgram->setAttributeBuffer(2,GL_FLOAT,offsetof(Vertex,TexCoords),2,sizeof(Vertex));
shaderProgram->enableAttributeArray(1);
shaderProgram->setAttributeBuffer(3,GL_FLOAT,offsetof(Vertex, Tangent),3,sizeof(Vertex));
shaderProgram->enableAttributeArray(1);
shaderProgram->setAttributeBuffer(4,GL_FLOAT,offsetof(Vertex, Bitangent),3,sizeof(Vertex));
VAO.release();
}
model.h
#ifndef MODEL_H
#define MODEL_H
#include "mesh.h"
#include <QDir>
class Model
{
public:
void draw();
void destroy();
static Model* createModel(QString path,QOpenGLContext* context,QOpenGLShaderProgram* shaderProgram);
private:
Model(QString path,QOpenGLContext* context,QOpenGLShaderProgram* shaderProgram);
~Model();
QOpenGLContext* context; //opengl函数入口
QOpenGLShaderProgram* shaderProgram ; //着色器程序
/* 模型数据 */
QVector<Texture*>textures_loaded; //纹理
QVector<Mesh*> meshes; //网格
QDir directory; //模型所在路径
//递归遍历结点
void processNode(aiNode *node, const aiScene *scene,aiMatrix4x4 mat4=aiMatrix4x4());
//加载网格
Mesh* processMesh(aiMesh *mesh, const aiScene *scene, aiMatrix4x4 model);
//加载材质纹理
QVector<Texture*> loadMaterialTextures(aiMaterial *mat, aiTextureType type,QString typeName);
};
#endif // MODEL_H
model.cpp
#include "model.h"
#include <QOpenGLTexture>
#include <QOpenGLContext>
Model::Model(QString path,QOpenGLContext* context, QOpenGLShaderProgram*shaderProgram)
: context(context)
, shaderProgram(shaderProgram)
, directory(path)
{
Assimp::Importer import;
const aiScene *scene = import.ReadFile(directory.absolutePath().toLocal8Bit(), aiProcess_Triangulate | aiProcess_FlipUVs);
if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
qDebug() << "ERROR::ASSIMP::"<<import.GetErrorString() << endl;
return;
}
qDebug() <<directory.absolutePath()<<"加载成功";
qDebug() <<"网格:"<<scene->mNumMeshes;
qDebug() <<"材质:"<<scene->mNumMaterials;
qDebug() <<"纹理:"<<scene->mNumTextures;
qDebug() <<"动画:"<<scene->mNumAnimations;
directory.cdUp();
processNode(scene->mRootNode, scene);
}
Model::~Model() //销毁对象
{
for(auto&it:textures_loaded){
it->texture.destroy();
delete it;
}
for(auto&it:meshes){
delete it;
}
}
void Model::draw(){
shaderProgram->bind();
for(Mesh* mesh:meshes){
mesh->Draw();
}
}
void Model::destroy()
{
delete this;
context->doneCurrent();
}
Model *Model::createModel(QString path, QOpenGLContext *context, QOpenGLShaderProgram *shaderProgram)
{
return new Model(path,context,shaderProgram);
}
void Model::processNode(aiNode *node, const aiScene *scene, aiMatrix4x4 mat4)
{
// 处理节点所有的网格(如果有的话)
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene,mat4));
}
// 接下来对它的子节点重复这一过程
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene,mat4*node->mChildren[i]->mTransformation);
}
}
Mesh* Model::processMesh(aiMesh *mesh,const aiScene *scene ,aiMatrix4x4 model)
{
// 初始化网格
Mesh* m_mesh=new Mesh(context->functions(),shaderProgram,model);
// 遍历网格的每个顶点
for(unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
QVector3D vector; //将assimp的数据转化为QtOpenGL支持的数据
// 位置
vector.setX(mesh->mVertices[i].x) ;
vector.setY(mesh->mVertices[i].y) ;
vector.setZ(mesh->mVertices[i].z) ;
vertex.Position = vector;
// 法向量
if(mesh->mNormals){
vector.setX(mesh->mNormals[i].x);
vector.setY(mesh->mNormals[i].y);
vector.setZ(mesh->mNormals[i].z);
vertex.Normal = vector;
}
// 纹理坐标
if(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates?
{
QVector2D vec;
//一个顶点最多可以包含8个不同的纹理坐标。因此我们假设我们不用
//使用一个顶点可以有多个纹理坐标的模型,所以我们总是取第一个集合(0)。
vec.setX(mesh->mTextureCoords[0][i].x);
vec.setY(mesh->mTextureCoords[0][i].y);
vertex.TexCoords = vec;
}
else{
vertex.TexCoords = QVector2D(0,0);
}
if(mesh->mTangents){
// tangent
vector.setX(mesh->mTangents[i].x);
vector.setY(mesh->mTangents[i].y);
vector.setZ(mesh->mTangents[i].z);
vertex.Tangent = vector;
}
if(mesh->mBitangents){
vector.setX(mesh->mBitangents[i].x);
vector.setY(mesh->mBitangents[i].y);
vector.setZ(mesh->mBitangents[i].z);
vertex.Bitangent = vector;
}
// bitangent
m_mesh->vertices.push_back(vertex);
}
for(unsigned int i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
// 将所有面的索引数据添加到索引数组中
for(unsigned int j = 0; j < face.mNumIndices; j++){
m_mesh->indices.push_back(face.mIndices[j]);
}
}
// 处理材质
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
// 1. 漫反射贴图
QVector<Texture*> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
for(auto &it:diffuseMaps)
m_mesh->textures.push_back(it);
// 2. 镜面贴图
QVector<Texture*> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
for(auto &it:specularMaps)
m_mesh->textures.push_back(it);
// 3. 法向量图
QVector<Texture*> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
for(auto &it:normalMaps)
m_mesh->textures.push_back(it);
// 4. 高度图
QVector<Texture*> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");
for(auto &it:heightMaps)
m_mesh->textures.push_back(it);
m_mesh->setupMesh();
return m_mesh;
}
QVector<Texture*> Model::loadMaterialTextures(aiMaterial *mat, aiTextureType type, QString typeName)
{
QVector<Texture*> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
// 检查纹理是否在之前加载过,如果是,则继续到下一个迭代:跳过加载新纹理
bool skip = false;
for(unsigned int j = 0; j < textures_loaded.size(); j++)
{
if(std::strcmp(textures_loaded[j]->path.toStdString().c_str(), str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true; //【优化】 带有相同filepath的纹理已经加载,继续到下一个
break;
}
}
if(!skip)
{ // 如果材质还没有加载,加载它
Texture* texture=new Texture;
QImage data(directory.filePath(str.C_Str()));
if(!data.isNull()){
texture->texture.setData(data);
texture->type = typeName;
texture->path = str.C_Str();
textures.push_back(texture);
textures_loaded.push_back(texture); // store it as texture loaded for entire model, to ensure we won't unnecesery load duplicate textures.
}
else{
qDebug()<<"未能成功加载纹理:"<<directory.filePath(str.C_Str());
}
}
}
return textures;
}
然后我们就可以利用它来绘制我们加载的模型了,Model无法直接创建,需要使用它的一个静态方法
Model *Model::createModel(QString path, QOpenGLContext *context, QOpenGLShaderProgram *shaderProgram)
QString path:模型的路径(如果使用的是相对路径,是相对于项目生成目录)
QOpenGLContext *context:由于我们需要对绘图做一些设置,因此需要传递窗口的上下文
QOpenGLShaderProgram *shaderProgram:绘制模型使用的着色器程序
注意:
- createModel必须在initializeOpenGLFunctions()函数和着色器程序编译链接之后才能调用
- 当不需要模型时,要调用model的destroy()释放model所占资源。
我们的第一个模型——源氏
你可以点击这里,下载源氏的模型(.fbx),解压到合适的位置,然后开始使用Model加载模型吧!
我们可以装配上之前写好的摄像机(别忘记使用定时器刷新)
另外我们需要修改一下着色器代码
代码节点
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include "camera.h"
#include "model.h"
#include <QOpenGLExtraFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLWidget>
#include <QTimer>
#include <QWidget>
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
virtual void initializeGL() override;
virtual void paintGL() override;
virtual void resizeGL(int w,int h) override;
virtual bool event(QEvent *e) override;
Model* model;
Camera camera;
QTimer timer;
private:
QOpenGLShaderProgram shaderProgram;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
Widget::Widget(QWidget *parent)
: QOpenGLWidget(parent)
, camera(this)
{
timer.setInterval(18);
connect(&timer,&QTimer::timeout,this,static_cast<void (Widget::*)()>(&Widget::update));
timer.start();
QSurfaceFormat format;
format.setSamples(50);
setFormat(format);
}
Widget::~Widget()
{
}
void Widget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(1.0f,1.0f,1.0f,1.0f);
shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/model.vert");
shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/model.frag");
shaderProgram.link();
model=Model::createModel("C:/Users/Administrator/Desktop/Genji/Genji.fbx",context(),&shaderProgram);
glEnable(GL_DEPTH_TEST);
camera.init();
}
void Widget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
QMatrix4x4 projection;
projection.perspective(45.0f,width()/(float)height(),0.1f,500.0f);
shaderProgram.setUniformValue("projection",projection);
shaderProgram.setUniformValue("view",camera.getView());
model->draw();
}
void Widget::resizeGL(int w, int h)
{
glViewport(0,0,w,h);
}
bool Widget::event(QEvent*e){
camera.handle(e);
return QWidget::event(e);
}
模型加载类
摄像机类
model.vert
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
TexCoords = aTexCoords;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
model.frag
#version 330 core
in vec2 TexCoords;
uniform sampler2D texture_diffuse1;
void main()
{
gl_FragColor = texture2D(texture_diffuse1,TexCoords);
}
源氏模型
项目文件
项目文件没有导入Assimp库,需要手动添加一下,源氏模型也是。