
前言网上查opengl入门学习网站介绍LearnOpenGL比较多虽然不是基于qt项目但原理是一致的本篇基于样例把坐标系和相机的知识点合并到一个qt项目中方便关联阅读。一、坐标系统对我们来说比较重要的总共有5个不同的坐标系统空间含义谁负责局部空间模型自己的坐标模型数据世界空间放到场景里的位置Model 矩阵观察空间从相机视角看到的空间View 矩阵裁剪空间透视/正交投影后的空间Projection 矩阵屏幕空间最终窗口像素坐标GPU 自动完成这些坐标点在顶点着色器中有相关的表达式表达式如下表达式得到的是什么aPos局部坐标model * vec4(aPos, 1.0)世界坐标view * model * vec4(aPos, 1.0)观察坐标projection * view * model * vec4(aPos, 1.0)裁剪坐标比如上一篇分享的着色器、纹理、变换的顶点着色器中就包含有以下两种坐标局部坐标aPos世界坐标uMatrix * vec4(aPos, 1.0) //uMatrix是时间空间对应的Model 矩阵的变量名称本篇的样例中将会用到裁剪坐标来展示投影后的特性。二、摄像机简单的理解是改变V(View) P(Projection) 的参数来实现的实例。本篇会提供通过键盘值改变VP值的qt项目案例。三、案例3.1 核心代码glwidget.h#ifndef GLWIDGET_H #define GLWIDGET_H #include QOpenGLWidget #include QOpenGLFunctions #include QOpenGLShaderProgram #include QOpenGLBuffer #include QMatrix4x4 #include QVector3D #include QTimer #include QKeyEvent #define GLF_SIZE 6 class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions { Q_OBJECT public: explicit GLWidget(QWidget *parent nullptr); ~GLWidget(); protected: void initializeGL() override; void resizeGL(int w, int h) override; // ★ 窗口尺寸变化时必须更新投影矩阵 void paintGL() override; void keyPressEvent(QKeyEvent *event) override; // 键盘控制相机位置 private: QOpenGLShaderProgram m_program; QOpenGLBuffer m_vbo{QOpenGLBuffer::VertexBuffer}; QTimer *m_timer; // ★ 坐标系统的三大主角 QMatrix4x4 m_model; // 模型矩阵物体 → 世界 QMatrix4x4 m_view; // 视图矩阵世界 → 相机 QMatrix4x4 m_projection; // 投影矩阵相机 → 裁剪 float m_angle 0.0f; // 立方体自转角度 // 相机参数用于构建 view 矩阵 QVector3D m_cameraPos QVector3D(0.0f, 0.0f, 5.0f); QVector3D m_cameraTarget QVector3D(0.0f, 0.0f, 0.0f); QVector3D m_cameraUp QVector3D(0.0f, 1.0f, 0.0f); void buildMatrices(int w, int h); // 抽出来resizeGL 和 paintGL 共用 }; #endif // GLWIDGET_Hglwidget.cpp#include glwidget.h GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent) { setFocusPolicy(Qt::StrongFocus); // 获取焦距捕获键盘事件 m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, []{ m_angle 1.5f; if (m_angle 360.0f) m_angle - 360.0f; update(); // 触发 paintGL }); } GLWidget::~GLWidget() default; void GLWidget::initializeGL() { initializeOpenGLFunctions(); glEnable(GL_DEPTH_TEST); // ★ 3D 必须开深度测试否则立方体背面盖住正面 // ---- 立方体顶点36 个顶点 6 面 × 2 三角形 × 3 顶点---- // 只含位置和颜色简化示例纹理可自行扩展 GLfloat vertices[] { // ---- 位置 ---- ---- 颜色 ---- // 前面 (z 0.5) -0.5f,-0.5f, 0.5f, 1.0f,0.0f,0.0f, 0.5f,-0.5f, 0.5f, 1.0f,0.0f,0.0f, 0.5f, 0.5f, 0.5f, 1.0f,0.0f,0.0f, -0.5f,-0.5f, 0.5f, 1.0f,0.0f,0.0f, 0.5f, 0.5f, 0.5f, 1.0f,0.0f,0.0f, -0.5f, 0.5f, 0.5f, 1.0f,0.0f,0.0f, // 后面 (z -0.5) -0.5f,-0.5f,-0.5f, 0.0f,1.0f,0.0f, 0.5f,-0.5f,-0.5f, 0.0f,1.0f,0.0f, 0.5f, 0.5f,-0.5f, 0.0f,1.0f,0.0f, -0.5f,-0.5f,-0.5f, 0.0f,1.0f,0.0f, 0.5f, 0.5f,-0.5f, 0.0f,1.0f,0.0f, -0.5f, 0.5f,-0.5f, 0.0f,1.0f,0.0f, // 上面 (y 0.5) -0.5f, 0.5f,-0.5f, 0.0f,0.0f,1.0f, 0.5f, 0.5f,-0.5f, 0.0f,0.0f,1.0f, 0.5f, 0.5f, 0.5f, 0.0f,0.0f,1.0f, -0.5f, 0.5f,-0.5f, 0.0f,0.0f,1.0f, 0.5f, 0.5f, 0.5f, 0.0f,0.0f,1.0f, -0.5f, 0.5f, 0.5f, 0.0f,0.0f,1.0f, // 下面 (y -0.5) -0.5f,-0.5f,-0.5f, 1.0f,1.0f,0.0f, 0.5f,-0.5f,-0.5f, 1.0f,1.0f,0.0f, 0.5f,-0.5f, 0.5f, 1.0f,1.0f,0.0f, -0.5f,-0.5f,-0.5f, 1.0f,1.0f,0.0f, 0.5f,-0.5f, 0.5f, 1.0f,1.0f,0.0f, -0.5f,-0.5f, 0.5f, 1.0f,1.0f,0.0f, // 右面 (x 0.5) 0.5f,-0.5f, 0.5f, 1.0f,0.0f,1.0f, 0.5f,-0.5f,-0.5f, 1.0f,0.0f,1.0f, 0.5f, 0.5f,-0.5f, 1.0f,0.0f,1.0f, 0.5f,-0.5f, 0.5f, 1.0f,0.0f,1.0f, 0.5f, 0.5f,-0.5f, 1.0f,0.0f,1.0f, 0.5f, 0.5f, 0.5f, 1.0f,0.0f,1.0f, // 左面 (x -0.5) -0.5f,-0.5f, 0.5f, 0.0f,1.0f,1.0f, -0.5f,-0.5f,-0.5f, 0.0f,1.0f,1.0f, -0.5f, 0.5f,-0.5f, 0.0f,1.0f,1.0f, -0.5f,-0.5f, 0.5f, 0.0f,1.0f,1.0f, -0.5f, 0.5f,-0.5f, 0.0f,1.0f,1.0f, -0.5f, 0.5f, 0.5f, 0.0f,1.0f,1.0f, }; //VBO把数据从内存搬进显存 m_vbo.create(); m_vbo.bind(); m_vbo.allocate(vertices, sizeof(vertices)); // ---- 顶点着色器三个矩阵按顺序合并 ---- m_program.addShaderFromSourceCode(QOpenGLShader::Vertex, #version 150\n in vec3 aPos;\n in vec3 aColor;\n out vec3 vColor;\n uniform mat4 uModel;\n // 模型矩阵 uniform mat4 uView;\n // 视图矩阵 uniform mat4 uProjection;\n // 投影矩阵 void main() {\n gl_Position uProjection * uView * uModel * vec4(aPos, 1.0);\n vColor aColor;\n } ); // ---- 片段着色器纯顶点色聚焦矩阵本身 ---- m_program.addShaderFromSourceCode(QOpenGLShader::Fragment, #version 150\n in vec3 vColor;\n out vec4 FragColor;\n void main() {\n FragColor vec4(vColor, 1.0);\n } ); // 5. 编译并链接程序 if (!m_program.link()) { qDebug() Shader Link Error: m_program.log(); return; } m_timer-start(16); // 约 60 FPS } // ★★★ 坐标系统最容易踩坑的地方aspect ratio 必须在窗口变化时更新 ★★★ void GLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); buildMatrices(w, h); // 重新构建投影矩阵 } void GLWidget::buildMatrices(int w, int h) { // ---- 1. 模型矩阵把物体从自身坐标放到世界坐标 ---- m_model.setToIdentity(); m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f); // 绕斜轴自转好看 // ---- 2. 视图矩阵把世界变换到相机视角 ---- m_view.setToIdentity(); m_view.lookAt( m_cameraPos, // 相机位置 (0,0,5) —— 往后拉 5 个单位才能看到物体 m_cameraTarget, // 相机看向哪里原点 m_cameraUp // 上向量定义哪边是上 ); // ---- 3. 投影矩阵把 3D 场景投到 2D 屏幕 ---- m_projection.setToIdentity(); //m_projection.ortho(-2.0f, 2.0f, -1.5f, 1.5f, 0.1f, 100.0f); m_projection.perspective( 45.0f, // fov视野角度越大越广角 float(w) / float(h), // ★ aspect必须按真实宽高比否则物体被拉伸 0.1f, // near近平面比这更近的都裁掉 100.0f // far 远平面 ); } void GLWidget::paintGL() { // 1.清屏深蓝色背景 glClearColor(0.05f, 0.05f, 0.12f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // ★ 深度缓冲也要清 //2.使用着色器程序 m_program.bind(); // 3. 绑定顶点数据 m_vbo.bind(); // 使用 attributeLocation 获取位置 int posAttr m_program.attributeLocation(aPos); int colorAttr m_program.attributeLocation(aColor); // 步长 6 个 float3 位置 3 颜色 m_program.setAttributeBuffer(posAttr, GL_FLOAT, 0, 3, GLF_SIZE * sizeof(GLfloat)); m_program.enableAttributeArray(posAttr); m_program.setAttributeBuffer(colorAttr, GL_FLOAT, 3 * sizeof(GLfloat), 3, GLF_SIZE * sizeof(GLfloat)); m_program.enableAttributeArray(colorAttr); // ★ 每帧更新模型矩阵自转角度在变view/projection 只在 resizeGL/按键时更新 m_model.setToIdentity(); m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f); m_program.setUniformValue(uModel, m_model); m_program.setUniformValue(uView, m_view); m_program.setUniformValue(uProjection, m_projection); glDrawArrays(GL_TRIANGLES, 0, 36); m_program.disableAttributeArray(posAttr); m_program.disableAttributeArray(colorAttr); m_program.release(); } // 键盘 WASD 控制相机位置直观感受 view 矩阵的作用 void GLWidget::keyPressEvent(QKeyEvent *event) { const float speed 0.3f; switch (event-key()) { case Qt::Key_W: m_cameraPos.setZ(m_cameraPos.z() - speed); break; // 前进 case Qt::Key_S: m_cameraPos.setZ(m_cameraPos.z() speed); break; // 后退 case Qt::Key_A: m_cameraPos.setX(m_cameraPos.x() - speed); break; // 左移 case Qt::Key_D: m_cameraPos.setX(m_cameraPos.x() speed); break; // 右移 case Qt::Key_Q: m_cameraPos.setY(m_cameraPos.y() speed); break; // 升高 case Qt::Key_E: m_cameraPos.setY(m_cameraPos.y() - speed); break; // 下降 } buildMatrices(width(), height()); // 相机动了重算 view 矩阵 update(); }3.2 代码讲解3.2.1 VBOVBO中存在了立方体(6个面)的坐标和颜色属性GLfloat vertices[] { // ---- 位置 ---- ---- 颜色 ---- // 前面 (z 0.5) -0.5f,-0.5f, 0.5f, 1.0f,0.0f,0.0f, 0.5f,-0.5f, 0.5f, 1.0f,0.0f,0.0f, 0.5f, 0.5f, 0.5f, 1.0f,0.0f,0.0f, -0.5f,-0.5f, 0.5f, 1.0f,0.0f,0.0f, 0.5f, 0.5f, 0.5f, 1.0f,0.0f,0.0f, -0.5f, 0.5f, 0.5f, 1.0f,0.0f,0.0f, // 后面 (z -0.5) -0.5f,-0.5f,-0.5f, 0.0f,1.0f,0.0f, 0.5f,-0.5f,-0.5f, 0.0f,1.0f,0.0f, 0.5f, 0.5f,-0.5f, 0.0f,1.0f,0.0f, -0.5f,-0.5f,-0.5f, 0.0f,1.0f,0.0f, 0.5f, 0.5f,-0.5f, 0.0f,1.0f,0.0f, -0.5f, 0.5f,-0.5f, 0.0f,1.0f,0.0f, // 上面 (y 0.5) -0.5f, 0.5f,-0.5f, 0.0f,0.0f,1.0f, 0.5f, 0.5f,-0.5f, 0.0f,0.0f,1.0f, 0.5f, 0.5f, 0.5f, 0.0f,0.0f,1.0f, -0.5f, 0.5f,-0.5f, 0.0f,0.0f,1.0f, 0.5f, 0.5f, 0.5f, 0.0f,0.0f,1.0f, -0.5f, 0.5f, 0.5f, 0.0f,0.0f,1.0f, // 下面 (y -0.5) -0.5f,-0.5f,-0.5f, 1.0f,1.0f,0.0f, 0.5f,-0.5f,-0.5f, 1.0f,1.0f,0.0f, 0.5f,-0.5f, 0.5f, 1.0f,1.0f,0.0f, -0.5f,-0.5f,-0.5f, 1.0f,1.0f,0.0f, 0.5f,-0.5f, 0.5f, 1.0f,1.0f,0.0f, -0.5f,-0.5f, 0.5f, 1.0f,1.0f,0.0f, // 右面 (x 0.5) 0.5f,-0.5f, 0.5f, 1.0f,0.0f,1.0f, 0.5f,-0.5f,-0.5f, 1.0f,0.0f,1.0f, 0.5f, 0.5f,-0.5f, 1.0f,0.0f,1.0f, 0.5f,-0.5f, 0.5f, 1.0f,0.0f,1.0f, 0.5f, 0.5f,-0.5f, 1.0f,0.0f,1.0f, 0.5f, 0.5f, 0.5f, 1.0f,0.0f,1.0f, // 左面 (x -0.5) -0.5f,-0.5f, 0.5f, 0.0f,1.0f,1.0f, -0.5f,-0.5f,-0.5f, 0.0f,1.0f,1.0f, -0.5f, 0.5f,-0.5f, 0.0f,1.0f,1.0f, -0.5f,-0.5f, 0.5f, 0.0f,1.0f,1.0f, -0.5f, 0.5f,-0.5f, 0.0f,1.0f,1.0f, -0.5f, 0.5f, 0.5f, 0.0f,1.0f,1.0f, };3.2.2 顶点着色器顶点着色器增加了View 矩阵和Projection矩阵// ---- 顶点着色器三个矩阵按顺序合并 ---- m_program.addShaderFromSourceCode(QOpenGLShader::Vertex, #version 150\n in vec3 aPos;\n in vec3 aColor;\n out vec3 vColor;\n uniform mat4 uModel;\n // 模型矩阵 uniform mat4 uView;\n // 视图矩阵 uniform mat4 uProjection;\n // 投影矩阵 void main() {\n gl_Position uProjection * uView * uModel * vec4(aPos, 1.0);\n vColor aColor;\n } );3.2.3 paintGL在渲染函数paintGL中也增加了View和Projection的取样m_program.setUniformValue(uView, m_view); m_program.setUniformValue(uProjection, m_projection);3.2.4 重构mvp模型为了测试重构mvp的功能增加了一个buildMatrices函数void GLWidget::buildMatrices(int w, int h) { // ---- 1. 模型矩阵把物体从自身坐标放到世界坐标 ---- m_model.setToIdentity(); m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f); // 绕斜轴自转好看 // ---- 2. 视图矩阵把世界变换到相机视角 ---- m_view.setToIdentity(); m_view.lookAt( m_cameraPos, // 相机位置 (0,0,5) —— 往后拉 5 个单位才能看到物体 m_cameraTarget, // 相机看向哪里原点 m_cameraUp // 上向量定义哪边是上 ); // ---- 3. 投影矩阵把 3D 场景投到 2D 屏幕 ---- m_projection.setToIdentity(); //m_projection.ortho(-2.0f, 2.0f, -1.5f, 1.5f, 0.1f, 100.0f); m_projection.perspective( 45.0f, // fov视野角度越大越广角 float(w) / float(h), // ★ aspect必须按真实宽高比否则物体被拉伸 0.1f, // near近平面比这更近的都裁掉 100.0f // far 远平面 ); }当应用窗口变换或者是捕获到指定的键盘值时触发void GLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); buildMatrices(w, h); // 重新构建投影矩阵 } // 键盘 WASD 控制相机位置直观感受 view 矩阵的作用 void GLWidget::keyPressEvent(QKeyEvent *event) { const float speed 0.3f; switch (event-key()) { case Qt::Key_W: m_cameraPos.setZ(m_cameraPos.z() - speed); break; // 前进 case Qt::Key_S: m_cameraPos.setZ(m_cameraPos.z() speed); break; // 后退 case Qt::Key_A: m_cameraPos.setX(m_cameraPos.x() - speed); break; // 左移 case Qt::Key_D: m_cameraPos.setX(m_cameraPos.x() speed); break; // 右移 case Qt::Key_Q: m_cameraPos.setY(m_cameraPos.y() speed); break; // 升高 case Qt::Key_E: m_cameraPos.setY(m_cameraPos.y() - speed); break; // 下降 } buildMatrices(width(), height()); // 相机动了重算 view 矩阵 update(); }四、效果图