0.前言
该示例展示了如何在 Qt 中使用 OpenGL ES 2.0 编写可鼠标操作旋转的 3D 立方体。示例整体比较简单,只需要一点点 OpenGL 基础。主要有两个点可作为学习参考,一是 Qt 封装的 OpenGL 便捷类(顶点缓冲,着色器程序等)的基本使用,二是四元数类 QQuaternion 的基本使用。
在 Qt Creator 搜索 cube,或者源码示例中查找
Qt 安装后示例路径:D:QtOnlineExamplesQt-5.15.2openglcube
Qt 源码路径:D:Downloadqt-everywhere-src-5.12.2qtbaseexamplesopenglcubecube.pro
示例文档:https://doc.qt.io/qt-5/qtopengl-cube-example.html
注释链接:https://github.com/gongjianbo/MyTestCode2021/tree/master/Qt/QtExampleCube
1.示例学习
该示例最终呈现的是一个立方体的骰子,六个点数分别贴上了对应的纹理。纹理是存在一张大的精灵图上的,通过纹理坐标来取对应位置的图。下图来自 Qt 示例和文档:
该示例由两个类组成:
MainWidget 扩展 QOpenGLWidget 并包含 OpenGL 初始化和绘图,以及鼠标和定时器事件处理。
GeometryEngine 处理顶点数据。将多边形几何图形传输到顶点缓冲区对象并从顶点缓冲区对象绘制几何图形。
继承 QOpenGLWidget 后,对 OpenGL 对象的操作需要确保当前上下文可用,操作放到 makeCurrent 和 doneCurrent 之间,而 initializeGL、paintGL、resizeGL 三个接口已经内部预先调用过了,所以不用再次调用。initializeGL 在我们 show 窗口的时候才会调用,所以没有显示的窗口是不会执行初始化的,析构中需要判断是否已初始化再进行释放。
class MainWidget : public QOpenGLWidget{ Q_OBJECTpublic: ~MainWidget() { //initializeGL在显示时才调用,释放若未初始化的会导致异常 if(!isValid()) { return; } //切换到当前上下文 makeCurrent(); //释放操作 doneCurrent(); }protected: //【】继承QOpenGLWidget后重写这三个虚函数 //设置OpenGL资源和状态。在第一次调用resizeGL或paintGL之前被调用一次 void initializeGL() override {} //设置OpenGL视口、投影等,每当尺寸大小改变时调用 void resizeGL(int w, int h) override {} //渲染OpenGL场景,每当需要更新小部件时使用 void paintGL() override {}}
着色器程序对象的初始化:
program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vshader.glsl");program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/fshader.glsl");program.link();program.bind();
纹理对象初始化:
QOpenGLTexture *texture = new QOpenGLTexture(QImage(":/cube.png").mirrored());texture->setMinificationFilter(QOpenGLTexture::Nearest);texture->setMagnificationFilter(QOpenGLTexture::Linear);texture->setWrapMode(QOpenGLTexture::Repeat);texture->bind();
顶点缓冲初始化:
VertexData vertices[] = { };GLushort indices[] = { };arrayBuf.create();arrayBuf.bind();arrayBuf.allocate(vertices, 24 * sizeof(VertexData));indexBuf.create();indexBuf.bind();indexBuf.allocate(indices, 34 * sizeof(GLushort));
释放:
makeCurrent();delete texture;arrayBuf.destroy();indexBuf.destroy();doneCurrent();
相较于直接使用 OpenGL 的接口进行初始化,Qt 的便捷类可以省不少代码。
再来看看四元数 QQuaternion 的使用。示例中,鼠标拖动后,根据起止两点坐标得到一个旋转方向向量,而 QQuaternion 有个静态函数可以从旋转轴和旋转角度转换得到四元数:
static QQuaternion fromAxisAndAngle(const QVector3D& axis, float angle);
示例中交换方向向量的 xy,得到垂直于方向的旋转轴向量。得到旋转的四元数值后,再和之前的四元数相乘,就得到的当前 modelview 矩阵旋转的四元数值。
QQuaternion rotation = QQuaternion::fromAxisAndAngle( rotationAxis, angularSpeed) * rotation;QMatrix4x4 matrix;matrix.rotate(rotation);
取纹理坐标部分就比较简单了,立方体六个面,每个面两个三角对应四个顶点:
{QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.0f, 0.0f)}, // v0{QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.0f)}, // v1{QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.0f, 0.5f)}, // v2{QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v3
因为是从大图中取两行三列六个小图,所以纹理坐标有 0.33、0.66、0.5 这样的值。1/3 取 0.33 明显精度太低,所以最终效果边缘不是很对称。
2.主要代码注释
(贴在这里便于以后我自己查看)
#pragma once#include "geometryengine.h"#include #include #include #include #include #include #include #include //管理顶点缓冲class GeometryEngine;//QOpenGLWidget窗口上下文,作用同GLFW库//QOpenGLFunctions访问OpenGL接口,可以不继承作为成员变量使用,作用同GLAD库class MainWidget : public QOpenGLWidget, protected QOpenGLFunctions{ Q_OBJECTpublic: //继承构造函数 using QOpenGLWidget::QOpenGLWidget; //析构释放纹理和顶点缓冲 ~MainWidget();protected: //release-press获取到鼠标拖动的方向 //配合四元数计算出物体旋转的向量 void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; //物体旋转时累减偏移量实现动画效果 void timerEvent(QTimerEvent *e) override; //【】继承QOpenGLWidget后重写这三个虚函数 //设置OpenGL资源和状态。在第一次调用resizeGL或paintGL之前被调用一次 void initializeGL() override; //设置OpenGL视口、投影等,每当尺寸大小改变时调用 void resizeGL(int w, int h) override; //渲染OpenGL场景,每当需要更新小部件时使用 void paintGL() override; //初始化着色器 void initShaders(); //初始化纹理 void initTextures();private: //定时器,配合timerEvent使用 QBasicTimer timer; //Qt着色器类封装 QOpenGLShaderProgram program; //管理顶点缓冲,内部使用QOpenGLBuffer GeometryEngine *geometries = nullptr; //Qt纹理类封装 QOpenGLTexture *texture = nullptr; //投影矩阵,resizeGL时更新,使最终渲染长宽保持比例 QMatrix4x4 projection; //鼠标按下时坐标 QVector2D mousePressPosition; //物体拖动旋转轴 QVector3D rotationAxis; //动画步进 qreal angularSpeed = 0; //四元数,记录物体叠加的旋转状态 //通过rotationAxis和angularSpeed来更新 QQuaternion rotation;};
#include "mainwidget.h"#include #include MainWidget::~MainWidget(){ //QOpenGLWidget //三个虚函数不需要makeCurrent,对应的操作已由框架完成 //但是释放时需要设置当前上下文 //initializeGL在显示时才调用,释放若未初始化的会导致异常 //可以在释放前判断:if(!isValid()) { return; } makeCurrent(); //释放纹理数据 delete texture; //释放顶点缓冲 delete geometries; doneCurrent();}void MainWidget::mousePressEvent(QMouseEvent *e){ //保存鼠标按下时的坐标 mousePressPosition = QVector2D(e->localPos());}void MainWidget::mouseReleaseEvent(QMouseEvent *e){ //鼠标按下和释放两点间差值 QVector2D diff = QVector2D(e->localPos()) - mousePressPosition; //旋转轴垂直于鼠标位置差向量,所以x和y交换 //QVector3D::normalized归一化 QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized(); //根据拖动距离来计算旋转速度 qreal acc = diff.length() / 100.0; //计算新的旋转轴 //上一次的旋转还未结束时会叠加到一起 rotationAxis = (rotationAxis * angularSpeed + n * acc).normalized(); //加速旋转 angularSpeed += acc;}void MainWidget::timerEvent(QTimerEvent *){ //旋转减速 angularSpeed *= 0.99; //低于某个阈值时停止转动 if (angularSpeed < 0.01) { angularSpeed = 0.0; } else { //当前旋转方向向量和之前的状态叠加,计算得到最新的四元数 rotation = QQuaternion::fromAxisAndAngle(rotationAxis, angularSpeed) * rotation; //刷新ui,重新绘制 update(); }}void MainWidget::initializeGL(){ //为当前上下文初始化OpenGL函数解析 initializeOpenGLFunctions(); //设置glClear填充的颜色 glClearColor(0, 0, 0, 1); //初始化着色器程序 initShaders(); //初始化纹理 initTextures(); //使能深度缓冲GL_DEPTH_TEST glEnable(GL_DEPTH_TEST); //使能面剔除GL_CULL_FACE //glCullFace可以设置GL_FRONT正面剔除或者GL_BACK背面剔除(默认) //glFrontFace指定正面的绕序,GL_CCW逆时针(默认)或者GL_CW顺时针 glEnable(GL_CULL_FACE); //初始化顶点缓冲 geometries = new GeometryEngine; //启动定时器,12ms刷新一次 timer.start(12, this);}void MainWidget::initShaders(){ //【注意】这里面的close关闭窗口其实并没有实际效用 //编译顶点着色器,此处作用同:glCreateShader+glShaderSource+glCompileShader+glAttachShader if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vshader.glsl")) close(); //编译片元着色器 if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/fshader.glsl")) close(); //着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。 //如果要使用刚才编译的着色器我们必须把它们链接(link)为一个着色器程序对象, //然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。 //当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。 //当输出和输入不匹配的时候,你会得到一个连接错误。 //此处作用同:gllinkProgram if (!program.link()) close(); //激活着色器程序对象,此处作用同:glUseProgram //因为后面还需要初始化纹理和顶点 if (!program.bind()) close();}void MainWidget::initTextures(){ //加载一张六个面的精灵图作为纹理,然后使用纹理坐标裁剪对应的区域进行贴图 //QImage::mirrord默认是上下翻转,因为GL的y轴和屏幕坐标系是相反的 texture = new QOpenGLTexture(QImage(":/cube.png").mirrored()); //指定放大缩小的纹理过滤,GL_NEAREST使用临近插值,GL_LINEAR使用线性插值 //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); texture->setMinificationFilter(QOpenGLTexture::Nearest); //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); texture->setMagnificationFilter(QOpenGLTexture::Linear); //纹理环绕方式,当纹理坐标超出默认范围时Repeat重复填充数据 //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); texture->setWrapMode(QOpenGLTexture::Repeat);}void MainWidget::resizeGL(int w, int h){ //计算宽高比 qreal aspect = qreal(w) / qreal(h ? h : 1); //投影参数 const qreal zNear = 3.0, zFar = 7.0, fov = 45.0; //重置投影矩阵为单位矩阵 projection.setToIdentity(); //透视投影 //参数fovy定义视野在Y-Z平面的角度,范围是[0.0, 180.0]; //参数aspect是投影平面宽度与高度的比率; //参数Near和Far分别是近远裁剪面到视点(沿Z负轴)的距离 projection.perspective(fov, aspect, zNear, zFar);}void MainWidget::paintGL(){ //清除颜色和深度缓冲 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //绑定纹理 //着色器程序在初始化时绑定了且未解绑,所以不用再次绑定 texture->bind(); //计算modelview矩阵 QMatrix4x4 matrix; //-5离camera远一点,使之能看到cube全貌 matrix.translate(0.0, 0.0, -5.0); //四元数旋转 matrix.rotate(rotation); //设置modelview-projection矩阵,Qt封装的矩阵相乘顺序好像和OpenGL一样的 program.setUniformValue("mvp_matrix", projection * matrix); //激活纹理单元,纹理单元GL_TEXTURE0默认总是被激活 //program.setUniformValue("texture", 0); //绘制顶点数据 geometries->drawCubeGeometry(&program);}
#pragma once#include #include #include //此类用于初始化和使用顶点和索引//QOpenGLFunctions访问OpenGL接口class GeometryEngine : protected QOpenGLFunctions{public: //在initializeGL中构造该对象,初始化顶点缓冲 GeometryEngine(); //释放顶点缓冲 virtual ~GeometryEngine(); //paintGL中调用,渲染顶点 void drawCubeGeometry(QOpenGLShaderProgram *program);private: //初始化顶点缓冲 void initCubeGeometry(); //vbo顶点缓冲,默认为QOpenGLBuffer::VertexBuffer QOpenGLBuffer arrayBuf; //ebo索引缓冲,在初始化列表中设置为QOpenGLBuffer::IndexBuffer QOpenGLBuffer indexBuf;};
#include "geometryengine.h"#include #include //定义顶点数据结构struct VertexData{ QVector3D position; //顶点坐标 QVector2D texCoord; //纹理坐标};//初始化设置为QOpenGLBuffer::IndexBuffer纹理缓冲GeometryEngine::GeometryEngine() : indexBuf(QOpenGLBuffer::IndexBuffer){ //为当前上下文初始化OpenGL函数解析 initializeOpenGLFunctions(); //创建缓冲区对象 arrayBuf.create(); indexBuf.create(); //初始化vbo、ebo数据 initCubeGeometry();}GeometryEngine::~GeometryEngine(){ //释放缓冲区对象 arrayBuf.destroy(); indexBuf.destroy();}void GeometryEngine::initCubeGeometry(){ //从精灵图取六个面的贴图 //每个面两个三角四个点,然后用索引来取这些顶点 VertexData vertices[] = { // Vertex data for face 0 {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.0f, 0.0f)}, // v0 {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.0f)}, // v1 {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.0f, 0.5f)}, // v2 {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v3 // Vertex data for face 1 {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D( 0.0f, 0.5f)}, // v4 {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.5f)}, // v5 {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.0f, 1.0f)}, // v6 {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v7 // Vertex data for face 2 {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v8 {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(1.0f, 0.5f)}, // v9 {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)}, // v10 {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(1.0f, 1.0f)}, // v11 // Vertex data for face 3 {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v12 {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(1.0f, 0.0f)}, // v13 {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v14 {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(1.0f, 0.5f)}, // v15 // Vertex data for face 4 {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.0f)}, // v16 {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v17 {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v18 {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v19 // Vertex data for face 5 {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v20 {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v21 {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v22 {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)} // v23 }; //立方体顶点索引,每个面两个三角,配合后面的GL_TRIANGLE_STRIP GLushort indices[] = { 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15) 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19) 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) }; //绑定顶点数据到缓冲区 arrayBuf.bind(); arrayBuf.allocate(vertices, 24 * sizeof(VertexData)); indexBuf.bind(); indexBuf.allocate(indices, 34 * sizeof(GLushort));}void GeometryEngine::drawCubeGeometry(QOpenGLShaderProgram *program){ //绑定vbo和ebo arrayBuf.bind(); indexBuf.bind(); //offset对应顶点缓冲数组中的偏移,QVector3D+QVector2D quintptr offset = 0; //顶点坐标,location对应着色器layout int vertexLocation = program->attributeLocation("a_position"); //此处作用同:glVertexAttribPointer+glEnableVertexAttribArray program->enableAttributeArray(vertexLocation); program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData)); //纹理坐标偏移 offset += sizeof(QVector3D); //纹理坐标 int texcoordLocation = program->attributeLocation("a_texcoord"); program->enableAttributeArray(texcoordLocation); program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData)); //根绝顶点数据渲染图元 //GL_TRIANGLE_STRIP-从最开始的两个顶点出发,遍历每个顶点,与前2个顶点一起组成一个三角形。 glDrawElements(GL_TRIANGLE_STRIP, 34, GL_UNSIGNED_SHORT, nullptr);}
//veshader.glsl#ifdef GL_ES//设置默认精度precision mediump int;precision mediump float;#endif//movel-view-projection矩阵计算后的值uniform mat4 mvp_matrix;//顶点坐标属性attribute vec4 a_position;//纹理坐标属性attribute vec2 a_texcoord;//纹理坐标传递到图元着色器varying vec2 v_texcoord;void main(){ //计算顶点在屏幕空间的位置 gl_Position = mvp_matrix * a_position; //纹理坐标传递到图元着色器 v_texcoord = a_texcoord;}//fshader.glsl#ifdef GL_ES//设置默认精度precision mediump int;precision mediump float;#endif//纹理uniform sampler2D texture;//纹理坐标varying vec2 v_texcoord;void main(){ //获取对应位置纹理的颜色值 gl_FragColor = texture2D(texture, v_texcoord);}