前言
网上查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) 的参数来实现的实例。本篇会提供通过键盘值改变V+P值的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_H
glwidget.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 个 float(3 位置 + 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 VBO
VBO中存在了立方体(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();
}
四、效果图
