Vries的教程是我看过的最好的可编程管线OpenGL教程,没有之一,本文以Vries的2D游戏框架为基础,(链接https://learnopengl-cn.github.io/06%20In%20Practice/2D-Game/01%20Breakout/ ),通过改变视口函数与其他一些简单操作,将其扩展为3D OpenGL框架,方便在此框架基础上,简化OpenGL的参数赋值操作。
- Tip1:为什么要写这个框架,因为单纯的对OpenGL进行shader生成,纹理生成,参数赋值,真的太麻烦了,故写一个管理 器对这些东西进行管理,简化了操作步骤,真的十分方便。接下来的教程基本都是在这个框架的基础上完成。
- Tip2:之前的3D OpenGL框架因为过于追求与Vries的框架相似,有很多冗余的地方,故特此从写,老版本的源代码已放到本文最下。
- Tip3:该框架的详细思想细节,强烈建议先看Vries的教程,链接如上,本文只描述大致思想细节。
程序源代码链接:https://pan.baidu.com/s/1SzpHH9Bx-36TwVlAuaZhkw 提取码:9xu5
编译环境:Qt5.9.4
编译器:Desktop Qt5.9.4 MSVC2017 64bit
IDE:QtCreator
一,程序简介


这是一个简单的3D框架,具有自由移动视角,OpenGL与GUI交互的基本功能。已内嵌了三个常用的部件进行渲染,一个正方体,一个平面与一个坐标系。
操作:使用WASD按键进行视角的移动,按住鼠标左键进行视角的拖拽,鼠标滚轮进行视角的放大与缩小。
项目管理如下:

camera.h: 摄像机类,负责视角的转换
littlethings.h: 立方体,平面,坐标等一些基础部件
mainwindow.h: 主窗口,负责OpenGL界面与GUI的交互,每10ms刷新一次OpenGL窗口
oglmanager.h: 继承QOpenGLWidget,OpenGL窗口绘制类,shader,texture的加载与参数设置都在这里运行
resourcemanager.h: 一个静态类,管理shader, texture
shader.h: 为方便resourcemanager类管理,shader单独成类
texture2d.h: 同理,texture单独成类
二,如何使用框架
这是OGLManager.h头文件内容,这个类继承了QOpenGLWidget,负责关于OpenGL的一切调用。
#include <QOpenGLWidget>
#include <QKeyEvent>
#include <QOpenGLFunctions_3_3_Core>
#include <QTime>
#include "camera.h"
#include <QMouseEvent>
class OGLManager : public QOpenGLWidget{
public:
explicit OGLManager(QWidget *parent = 0);
~OGLManager();
void handleKeyPressEvent(QKeyEvent *event); //键盘按下事件
void handleKeyReleaseEvent(QKeyEvent *event); //键盘释放事件
GLboolean keys[1024];//获取键盘按键,实现多键触控
GLboolean isOpenLighting;
GLboolean isLineMode;
protected:
void mouseMoveEvent(QMouseEvent *event);
void wheelEvent(QWheelEvent *event) override; //滚轮事件
void mousePressEvent(QMouseEvent *event) override; //鼠标按下事件
void mouseReleaseEvent(QMouseEvent *event) override; //鼠标释放事件
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
private:
void processInput(GLfloat dt);//摄像机键盘处理函数,
void updateGL();//opengl 更新函数
QOpenGLFunctions_3_3_Core *core;
GLboolean isFirstMouse;
GLboolean isLeftMousePress;
GLint lastX;
GLint lastY;
QTime time;
GLfloat deltaTime;
GLfloat lastFrame;//上一帧
Camera *camera;
};
关于QOpenGLWidget:
这个Qt下的OpenGL绘制框架,有三个protected权限的成员函数:
在initializeGL()里做参数的初始化,paintGL()里进行物体的绘制,resizeGL(int w, int h)里进行glViewport()视口的选择。
所以据此,我们在
initializeGL()里进行shader的加载与赋值:ResourceManager是一个静态函数类,负责shader与texture的加载与赋值。
比如,只需要一行代码,ResourceManager::loadShader("shader名称", "顶点着色器路径", "片段着色器路径", "几何着色器路径"= null)即可完成一个着色器的加载。加载纹理同理。
..............
/************ 载入shader ***********/
ResourceManager::loadShader("coordinate", ":/shaders/res/shaders/coordinate.vert", ":/shaders/res/shaders/coordinate.frag");
ResourceManager::loadShader("cube", ":/shaders/res/shaders/cube.vert", ":/shaders/res/shaders/cube.frag");
ResourceManager::loadShader("plane", ":/shaders/res/shaders/plane.vert", ":/shaders/res/shaders/plane.frag");
/************ 载入Texture ***********/
ResourceManager::loadTexture("brickwall", ":/textures/res/textures/brickwall.jpg");
ResourceManager::loadTexture("cementwall", ":/textures/res/textures/cementwall.jpg");
/*********** cube shader参数 **************/
QMatrix4x4 model;
ResourceManager::getShader("cube").use().setMatrix4f("model", model);
ResourceManager::getShader("cube").use().setInteger("ambientMap", 0);
..............
paintGL()里专门负责各个部件的绘制,不做过多累赘
void OGLManager::paintGL(){
/*********** 计算两次帧数之间的时间间隔 ***************/
GLfloat currentFrame = (GLfloat)time.elapsed()/100;
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
this->processInput(deltaTime); //调用键盘响应操作
this->updateGL(); //调用OpenGL更新函数
/********* 绘制cube ************/
ResourceManager::getShader("cube").use();
core->glActiveTexture(GL_TEXTURE0);
ResourceManager::getTexture("brickwall").bind();
cube->draw(GL_TRUE);
/********* 绘制plane ************/
ResourceManager::getShader("plane").use();
core->glActiveTexture(GL_TEXTURE0);
ResourceManager::getTexture("cementwall").bind();
plane->draw(GL_TRUE);
/********* 绘制坐标系统 ************/
ResourceManager::getShader("coordinate").use();
coordinate->draw();
}
另定义updateGL()函数,负责着色器的一些更新操作,比如projection, view矩阵会跟随视角位置的改变而改变,放进更新函数里进行管理会很方便。
void OGLManager::updateGL(){
if(this->isLineMode)
core->glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
else
core->glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
QMatrix4x4 projection, view;
projection.perspective(camera->zoom, (GLfloat)width()/(GLfloat)height(), 0.1f, 2000.f);
view = camera->getViewMatrix();
ResourceManager::getShader("cube").use().setMatrix4f("projection", projection);
ResourceManager::getShader("cube").use().setMatrix4f("view", view);
ResourceManager::getShader("plane").use().setMatrix4f("projection", projection);
ResourceManager::getShader("plane").use().setMatrix4f("view", view);
ResourceManager::getShader("coordinate").use().setMatrix4f("projection", projection);
ResourceManager::getShader("coordinate").use().setMatrix4f("view", view);
}
三,小部件
关于littlethings.h这个类,这个类存储了一些小部件,为这些小部件单独建类又好麻烦,故放在一个类里进行管理。
目前这些小部件有:
- Cube立方体
- Plane平面体
- Coordinate坐标系
建立Cube与Plane类时,我留了个心眼,因为后续的内容经常需要各种类型的立方体或平面体,或有无纹理坐标,或有无法线,针对不同要求重写VBO是件很麻烦的事情。
故我将纹理坐标,法线用bool值进行管理,在draw函数中选择是否将对应值载入VBO。
以Plane类举例:
Plane头文件
/*
很简单的操作,
init()函数 初始化数据
draw()往VBO中绑定数据 并绘制
*/
class Plane{
public:
Plane();
~Plane();
void init();
void draw(GLboolean isTexture = GL_FALSE,GLboolean isNormal = GL_FALSE);
private:
QOpenGLFunctions_3_3_Core *core;
GLuint VBO;
};
Plane源文件:
void init() 函数负责数据的初始化与绑定进buffer
void draw(bool, bool) 根据参数决定是否将纹理坐标与法线信息绑定至指定着色器
Plane::Plane(): VBO(0){
core = QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_3_3_Core>();
}
Plane::~Plane(){
if(VBO != 0)
core->glDeleteBuffers(1, &VBO);
}
void Plane::init(){
float vertices[] = {
//!!!!!这里有一个很重要的知识点,当纹理的取值范围大于1时,且纹理为repeat时,show的方式为repeat.比如纹理坐标都是2,应映射效果为2*2共4个纹理
// positions // textures // normals
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.0f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.0f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.0f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
};
core->glGenBuffers(1, &VBO);
core->glBindBuffer(GL_ARRAY_BUFFER, VBO);
core->glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
}
void Plane::draw(GLboolean isTexture, GLboolean isNormal){
core->glBindBuffer(GL_ARRAY_BUFFER, VBO);
core->glEnableVertexAttribArray(0);
core->glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
if(isTexture){
core->glEnableVertexAttribArray(1);
core->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3*sizeof(float)));
}
if(isNormal){
core->glEnableVertexAttribArray(2);
core->glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(5*sizeof(float)));
}
core->glDrawArrays(GL_TRIANGLES, 0, 6);
}
/*********************************** 以下为旧版本 *************************************************/
Qt开发平台:5.8.0
编译器:Desktop Qt 5.8.0 MSVC2015_64bit
程序源代码链接: https://pan.baidu.com/s/196FAnQylehc9XHOUR8MLIg
主体内容仍是上一节的十个箱子:
代码结构所示,因其源文件过多,将项目文件上传至百度云,
项目结构:
以下是一些类的意义:
类desktop: 等于Vries中的main.cpp,作为main函数使用
类oglmanager:等于Vries中的类Game,作为管理器使用
其余一样