前言
网上查opengl入门学习网站,介绍LearnOpenGL比较多,虽然不是基于qt项目,但原理是一致的,本篇基于样例把着色器、纹理和变换3个知识点合并到一个qt项目中,方便关联阅读。
一、着色器
opengl是用显卡来绘制的,VBO负责把数据从内存搬进显存,VBO可以存储坐标、颜色、纹理。
1.1顶点着色器
决定每个顶点最终落在屏幕哪个位置,位置由三维坐标(x,y,z)来决定,z=0.0f时即为二维坐标系。
QOpenGLShader vShader(QOpenGLShader::Vertex);
vShader.compileSourceCode(
"#version 150\n"
"in vec3 aPos;\n" // 输入:来自顶点的坐标(VBO 的前3个float:0、1、2)
"in vec3 aColor;\n" // 输入:来自顶点的颜色(VBO 接下来的3个float:3、4、5)
"in vec2 aTexCoord;\n" // 输入:来自顶点的纹理(VBO 的最后2个float:6、7)
"out vec3 vColor;\n" // 输出:转发给片段着色器
"out vec2 vTexCoord;\n" // ★ 纹理输出
"uniform mat4 uMatrix;\n" // 外部传入的变换矩阵
"void main() {\n"
" gl_Position = uMatrix * vec4(aPos, 1.0);\n" // 顶点最终位置(系统内置输出)
" vColor = aColor;\n" // 把颜色往下传
" vTexCoord = aTexCoord;\n"
"}"
);
vecn:包含n个float分量的默认向量,下面代码段说明VBO中存储的值是(3+3+2)8个一组
"in vec3 aPos;\n" // 输入:来自顶点的坐标(VBO 的前3个float:0、1、2)
"in vec3 aColor;\n" // 输入:来自顶点的颜色(VBO 接下来的3个float:3、4、5)
"in vec2 aTexCoord;\n" // 输入:来自顶点的纹理(VBO 的最后2个float:6、7)
compileSourceCode中看起来像是类C的代码,这整段字符串不是直接交给GPU运行的,而是提交给显卡驱动,由显卡驱动去解析、编译(是CPU来跑),编译产物才进显存。
QOpenGLShaderProgram m_program;//头文件中声明
m_program.addShader(&vShader);// 字符串拷给驱动(CPU 内存)
m_program.addShader(&fShader);
if (!m_program.link()) {//编译并链接程序
qDebug() << "Shader Link Error:" << m_program.log();
} else {
qDebug() << "Shader Linked Successfully!";
}
1.2片段着色器
决定顶点组成的形状内部每个像素是什么颜色。
QOpenGLShader fShader(QOpenGLShader::Fragment);
fShader.compileSourceCode(
"#version 150\n"
"in vec3 vColor;\n" // 输入:从顶点着色器插值而来的颜色
"in vec2 vTexCoord;\n" // 输入:纹理
"out vec4 FragColor;\n" // 输出:这个像素最终颜色,如果在片段着色器没有定义输出颜色,OpenGL会把物体渲染为黑色(或白色)。
"uniform sampler2D uTexture;\n" // 输出:纹理
"void main() {\n"
" vec4 texColor = texture(uTexture, vTexCoord);\n"
" FragColor = texColor * vec4(vColor, 1.0);\n" // 纹理 × 顶点色
"}"
);
判断着色器的语法与顶点着色器差不多,职能不同。下面这张从网络获取的图跟容易看出它是干嘛的。

二、纹理
粗俗些的说法就是让着色器显示指定的图片,看起来像生活中的实物,而不是有规律的RGB。本篇引用了LearnOpenGL提供的示例纹理图。
【一、着色器】比较基础也比较重要,上面是没有给出代码过程的,纹理按照代码关键流程来。
2.1 initializeGL准备
创建 VBO / 创建 EBO / 编译着色器 / 链接程序 / 加载纹理
以上代码段是准备工作中的关键环节,需要注意的是,上面的顺序是可以调换的,只要是在initializeGL()里面即可。
2.1.1 创建VBO
GLfloat vertices[] = {
// ---- 位置 x,y,z ------ -- 颜色 r,g,b -- -- 纹理 u,v --
/*0*/ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // 左下
/*1*/ 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
/*2*/ 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, 1.0f, // 正上(U取中间)
/*3*/ -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, // 左上
/*4*/ 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f // 右上
};
//VBO:把数据从内存搬进显存
m_vbo.create(); // 申请一个 VBO 句柄
m_vbo.bind(); // 绑定它,之后的操作都针对它
m_vbo.allocate(vertices, sizeof(vertices));// 把数据上传到显存
如上面的代码片段,中纹理坐标存储在第7,8个,注意以上不是二维数组,一维数组需要自律排行顺序,uv值代表的意思如下,与前面的二维坐标位置是可以对应得上的。
//u = 0:图片最左边 u = 1:图片最右边
//v = 0:图片最下边 v = 1:图片最上边
2.1.2 创建EBO
这项不是必须的,但是如果有个形状有出现重复位置出现在VBO时,使用EBO可以节省VBO的处理耗时。
// 2. EBO 索引:告诉 GPU "按哪个顺序、跳到哪些下标去取顶点"
// 前 3 个 {0,1,2} -> 三角形 (GL_TRIANGLES)
// 后 4 个 {0,1,3,4} -> 四边形 (GL_TRIANGLE_STRIP,之字排列,自动拼 2 个三角形)
// 顶点 0、1 被两段索引同时引用,这就是"复用"。
GLushort indices[] = {
0, 1, 2, // 三角形索引段(EBO 偏移 0 字节起,共 3 个索引)
0, 1, 3, 4 // 四边形索引段(EBO 偏移 3*2=6 字节起,共 4 个索引)
};
// EBO:把索引数据搬进显存(绑定后它占据 GL_ELEMENT_ARRAY_BUFFER 目标)
m_ebo.create();
m_ebo.bind();
m_ebo.allocate(indices, sizeof(indices));
2.1.3 着色器
本文的qt样例中同时包含了着色器、纹理、变换,为了体现特性切换的特点,我设置了一个开启纹理功能的开关,在代码中体现。
#define TEXTURE_STATUS false //是否开启纹理功能
在【1.1顶点着色器】中已经给出了包含纹理坐标输出的代码片段,这个片段即使不使用,也不会影响到效果,而片段着色器如果如同【1.2片段着色器】中增加了代码片段,又不提供输入时,会导致出现全黑的BUG,所以可以如下用预编译来处理
QOpenGLShader fShader(QOpenGLShader::Fragment);
#if TEXTURE_STATUS
fShader.compileSourceCode(
"#version 150\n"
"in vec3 vColor;\n"
"in vec2 vTexCoord;\n"
"out vec4 FragColor;\n"
"uniform sampler2D uTexture;\n"
"void main() {\n"
" FragColor = texture(uTexture, vTexCoord) * vec4(vColor, 1.0);\n"
"}");
#else
fShader.compileSourceCode(
"#version 150\n"
"in vec3 vColor;\n"
"out vec4 FragColor;\n"
"void main() {\n"
" FragColor = vec4(vColor, 1.0);\n" // 纯顶点色,无采样
"}");
#endif
2.1.4 编译并链接程序
QOpenGLShaderProgram m_program;
m_program.addShader(&vShader);
m_program.addShader(&fShader);
if (!m_program.link()) {
qDebug() << "Shader Link Error:" << m_program.log();
} else {
qDebug() << "Shader Linked Successfully!";
}
2.1.5 加载纹理
实际上就是加载一张图片。
QOpenGLTexture *m_texture = nullptr; // 纹理对象
if(TEXTURE_STATUS){
// 6. 加载纹理
QImage img(":/images/wall.jpg");
if (img.isNull()) {
qDebug() << "Texture load failed!";
} else {
// QImage 原点在左上角,OpenGL 纹理原点在左下角 → 必须垂直翻转,否则图片上下颠倒
m_texture = new QOpenGLTexture(img.mirrored());
m_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear); // 缩小时用 mipmap
m_texture->setMagnificationFilter(QOpenGLTexture::Linear); // 放大时线性插值
m_texture->setWrapMode(QOpenGLTexture::ClampToEdge); // UV 超出 [0,1] 时截断
}
}
2.2 paintGL渲染
前面是initializeGL()中的准备工作,接下来是向显卡提交渲染的工作。
2.2.1 清屏和绑定
把上一帧的画面擦掉,准备好新的画布,激活着色器程序、绑定 VBO/EBO/纹理,把 GPU 需要的数据和状态就位。
// 1.清屏(深蓝色背景)
glClearColor(0.05f, 0.05f, 0.12f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 2.使用着色器程序
m_program.bind();
// 3. 绑定顶点数据 + 索引数据
m_vbo.bind();
m_ebo.bind();
2.2.2 配置顶点属性
告诉 GPU 怎么解析顶点缓冲里的位置、颜色、UV 数据。
//1. 使用 attributeLocation 从顶点着色器中获取位置,避免硬编码
int posAttr = m_program.attributeLocation("aPos");
int colorAttr = m_program.attributeLocation("aColor");
int texAttr = m_program.attributeLocation("aTexCoord"); // 纹理
//2.设置属性值
if(TEXTURE_STATUS){
// 顶点着色器纹理坐标:偏移 6 个 float,取 2 个分量
m_program.setAttributeBuffer(texAttr, GL_FLOAT, 6 * sizeof(GLfloat), 2, GLF_SIZE * sizeof(GLfloat));
m_program.enableAttributeArray(texAttr);
}
2.2.3 传递Uniform
把 CPU 侧算好的矩阵、采样器编号等参数推给 GPU。
//3.片段着色器取样
if(TEXTURE_STATUS){
// 片段着色器采样器是 int 类型:告诉它去 0 号纹理单元取样
m_program.setUniformValue("uTexture", 0);
if (m_texture) m_texture->bind(0);
}
2.2.4 发出绘制命令
调用 glDrawElements / glDrawArrays,触发 GPU 执行顶点着色器 → 光栅化 → 片段着色器 → 输出到帧缓冲。
if (m_shape == 0) {
//glDrawArrays(GL_TRIANGLES, 0, 3); // 用下标0~2这3个顶点:三角形
// 三角形:索引 0,1,2
glDrawElements(GL_TRIANGLE_STRIP, 3, GL_UNSIGNED_SHORT,
(const void *)(m_shapelist[ELEMNNT_TRIANGLE] * sizeof(GLushort)));
} else {
// 四边形:索引 0,1,3,4 —— 偏移 3 个索引 = 3 * 2 字节
glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT,
(const void *)(m_shapelist[ELEMNNT_QUAD] * sizeof(GLushort)));
}
//6.关闭属性数组,释放绑定---收尾
if(TEXTURE_STATUS){
if (m_texture) m_texture->release();
m_program.disableAttributeArray(texAttr);
}
注意,glDrawArrays只能按顺序读顶点,跨索引时,必须用EBO+glDrawElements。
2.2.4 收尾解绑
释放纹理、禁用属性数组、解绑程序,避免污染下一帧的状态。
m_program.disableAttributeArray(posAttr);
m_program.disableAttributeArray(colorAttr);
m_program.release();
2.3效果图


无纹理效果图 有纹理效果图
三、变换
物体经过着色、加入纹理还只是还是静态的物体,OpenGL 变换的核心就是:顶点数据从“模型自己的坐标”一步步变成“屏幕上的像素位置”,中间主要靠模型矩阵、视图矩阵、投影矩阵这三组 4×4 矩阵完成。
| 阶段 | 输入 | 输出 | 谁负责 |
|---|---|---|---|
| 模型变换 | 模型局部坐标 | 世界坐标 | 模型矩阵 Model |
| 视图变换 | 世界坐标 | 相机/眼坐标 | 视图矩阵 View |
| 投影变换 | 相机坐标 | 裁剪坐标 | 投影矩阵 Projection |
本篇只以模型矩阵的缩放、平移、旋转为例。在LearnOpenGL中,学习变换,需要去学习向量和矩阵运算,但QT已经封装好,以下只给出与模型矩阵相关的。
在顶点着色器中,有以下代码片段及意义说明:
gl_Position = uMatrix * vec4(aPos, 1.0);
aPos → 原始顶点,比如 (x, y, z)
vec4(aPos, 1.0) → 补成齐次坐标
uMatrix → 模型 × 视图 × 投影矩阵
gl_Position → 变换后的裁剪空间坐标
三维空间里,旋转和缩放可以用 3×3 矩阵表示,但平移是加法,不能直接用 3×3 矩阵乘法表示,需要加上第四个分量,就变成vec4(aPos, 1.0)。
3.1 缩放
即x、y轴同时缩写/放大相同的比例,opengl对四位矩阵提供了scale函数进行缩放,如下
float pulse = 0.75f + 0.35f * qSin(qDegreesToRadians(m_angle * 2.0f)); // 缩放 0.4~1.1
QMatrix4x4 matrix;//四维矩阵
matrix.scale(pulse, pulse, 1.0f); // 缩放(Scale),x、y轴等比变化,Z轴不变
其四维矩阵如下:
| sx 0 0 0 |
| 0 sy 0 0 |
| 0 0 sz 0 |
| 0 0 0 1 |
其中matrix.scale(pulse, pulse, 1.0f);相当于令sz=1.0f
3.2 旋转
旋转不会改变坐标轴长度,但会改变它们指向哪里,本篇是绕Z轴旋转,即x、y坐标变换,Z不变。
matrix.rotate(m_angle, 0.0f, 0.0f, 1.0f); // 积木二:旋转(Rotate) 绕Z轴
m_angle → 旋转的角度θ
0.0f, 0.0f, 1.0f → 前面两个0.0f是x、y轴,1.0f是X轴
其四维矩阵如下:
| cosθ -sinθ 0 0 |
| sinθ cosθ 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
第1、2行和第1、2列有变换的是x、y轴。
3.3 平移
即把整个空间移动一段距离,以下代码是只在X轴平移
float driftX = 0.25f * qCos(qDegreesToRadians(m_angle)); // 水平来回漂浮
matrix.translate(driftX, 0.0f, 0.0f); // 平移(Translate),沿 X 轴左右移动
其四维矩阵如下:
| 1 0 0 tx |
| 0 1 0 ty |
| 0 0 1 tz |
| 0 0 0 1 |
在x轴平移,相当于上面的四位矩阵变为tx=driftX,ty=0,tz=0。
四、qt项目代码
glwidget.h
#ifndef GLWIDGET_H
#define GLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QTimer>
#include <QMatrix4x4>
#include <QList>
#include <QtMath>
#include <QOpenGLTexture>
#define GLF_SIZE 8
#define TEXTURE_STATUS false //是否开启纹理功能
typedef enum enElementsType{
ELEMNNT_TRIANGLE=0,
ELEMNNT_QUAD
}TEnElementsType;
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;//每一帧都重复:把画面画出来(每做一道菜)
private:
QOpenGLShaderProgram m_program;
QOpenGLTexture *m_texture = nullptr; // 纹理对象
QOpenGLBuffer m_vbo;// 顶点缓冲:存坐标+颜色(瘦身后的 5 个顶点)
QOpenGLBuffer m_ebo; // 新增:索引缓冲(IndexBuffer),存"按什么顺序取顶点"
QTimer *m_timer;
float m_angle = 0.0f;
int m_shape = 0; // 新增:当前形状 0=三角形 1=四边形
QList<int> m_shapelist;
};
#endif // GLWIDGET_H
glwidget.cpp
#include "glwidget.h"
#include <QDebug>
#include <QDir>
GLWidget::GLWidget(QWidget *parent)
: QOpenGLWidget(parent)
, m_vbo(QOpenGLBuffer::VertexBuffer) // 顶点缓冲:存坐标+颜色
, m_ebo(QOpenGLBuffer::IndexBuffer) // 索引缓冲:存"按什么顺
{
m_shapelist={0,3};
// 创建定时器,每秒刷新60次 (1000 / 60 ≈ 16)
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, [=]{
m_angle += 1.0f; // 累积一个角度
if (m_angle >= 360.0f) { // 整个图形转满一圈
m_angle -= 360.0f; // 归零,避免数值无限累加溢出
m_shape = !m_shape; // 形状切换:0=三角形 1=四边形
}
update(); // 请求重绘 -> 触发 paintGL()
});
}
GLWidget::~GLWidget()
{
makeCurrent();
delete m_texture;
doneCurrent();
}
void GLWidget::initializeGL()
{
initializeOpenGLFunctions();
//glDrawElements 决定用哪些顶点、按什么顺序、连成什么图元
//顶点着色器 决定每个顶点最终落在屏幕哪个位置
//片段着色器 决定三角形内部每个像素是什么颜色
// 1. VBO:5 个互不重复的顶点 (XYZ + RGB)
GLfloat vertices[] = {
// ---- 位置 x,y,z ------ -- 颜色 r,g,b -- -- 纹理 u,v --
//u = 0:图片最左边 u = 1:图片最右边
//v = 0:图片最下边 v = 1:图片最上边
/*0*/ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // 左下
/*1*/ 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
/*2*/ 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, 1.0f, // 正上(U取中间)
/*3*/ -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, // 左上
/*4*/ 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f // 右上
};
// 2. EBO 索引:告诉 GPU "按哪个顺序、跳到哪些下标去取顶点"
// 前 3 个 {0,1,2} -> 三角形 (GL_TRIANGLES)
// 后 4 个 {0,1,3,4} -> 四边形 (GL_TRIANGLE_STRIP,之字排列,自动拼 2 个三角形)
// 顶点 0、1 被两段索引同时引用,这就是"复用"。
GLushort indices[] = {
0, 1, 2, // 三角形索引段(EBO 偏移 0 字节起,共 3 个索引)
0, 1, 3, 4 // 四边形索引段(EBO 偏移 3*2=6 字节起,共 4 个索引)
};
//VBO:把数据从内存搬进显存
m_vbo.create(); // 申请一个 VBO 句柄
m_vbo.bind(); // 绑定它,之后的操作都针对它
m_vbo.allocate(vertices, sizeof(vertices));// 把数据上传到显存
// EBO:把索引数据搬进显存(绑定后它占据 GL_ELEMENT_ARRAY_BUFFER 目标)
m_ebo.create();
m_ebo.bind();
m_ebo.allocate(indices, sizeof(indices));
// 3. 顶点着色器 (使用 GLSL 150 标准--类C语言)
QOpenGLShader vShader(QOpenGLShader::Vertex);
vShader.compileSourceCode(
"#version 150\n"
"in vec3 aPos;\n" // 输入:来自顶点的坐标(VBO 的前3个float:0、1、2)
"in vec3 aColor;\n" // 输入:来自顶点的颜色(VBO 接下来的3个float:3、4、5)
"in vec2 aTexCoord;\n" // 输入:来自顶点的纹理(VBO 的最后2个float:6、7)
"out vec3 vColor;\n" // 输出:转发给片段着色器
"out vec2 vTexCoord;\n" // ★ 纹理输出
"uniform mat4 uMatrix;\n" // 外部传入的变换矩阵
"void main() {\n"
" gl_Position = uMatrix * vec4(aPos, 1.0);\n" // 顶点最终位置(系统内置输出)
" vColor = aColor;\n" // 把颜色往下传
" vTexCoord = aTexCoord;\n"
"}"
);
// 4. 片段着色器
QOpenGLShader fShader(QOpenGLShader::Fragment);
#if TEXTURE_STATUS
fShader.compileSourceCode(
"#version 150\n"
"in vec3 vColor;\n"
"in vec2 vTexCoord;\n"
"out vec4 FragColor;\n"
"uniform sampler2D uTexture;\n"
"void main() {\n"
" FragColor = texture(uTexture, vTexCoord) * vec4(vColor, 1.0);\n"
"}");
#else
fShader.compileSourceCode(
"#version 150\n"
"in vec3 vColor;\n"
"out vec4 FragColor;\n"
"void main() {\n"
" FragColor = vec4(vColor, 1.0);\n" // 纯顶点色,无采样
"}");
#endif
// 5. 编译并链接程序---把两个编译好的着色器加进一个程序对象并链接
m_program.addShader(&vShader);
m_program.addShader(&fShader);
if (!m_program.link()) {
qDebug() << "Shader Link Error:" << m_program.log();
} else {
qDebug() << "Shader Linked Successfully!";
}
if(TEXTURE_STATUS){
// 6. 加载纹理
QImage img(":/images/wall.jpg");
if (img.isNull()) {
qDebug() << "Texture load failed!";
} else {
// QImage 原点在左上角,OpenGL 纹理原点在左下角 → 必须垂直翻转,否则图片上下颠倒
m_texture = new QOpenGLTexture(img.mirrored());
m_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear); // 缩小时用 mipmap
m_texture->setMagnificationFilter(QOpenGLTexture::Linear); // 放大时线性插值
m_texture->setWrapMode(QOpenGLTexture::ClampToEdge); // UV 超出 [0,1] 时截断
}
}
// 启动定时器
m_timer->start(16);//16 毫秒(约 60 帧/秒)一次的定时器
}
void GLWidget::resizeGL(int w, int h)
{
//glViewport 的作用是:告诉 GPU“把 NDC 里 -1~1 的画面,映射到窗口这块 w×h 的矩形像素区域”。
glViewport(0, 0, w, h);
}
void GLWidget::paintGL()
{
// 1.清屏(深蓝色背景)
glClearColor(0.05f, 0.05f, 0.12f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 2.使用着色器程序
m_program.bind();
// 3. 绑定顶点数据 + 索引数据
m_vbo.bind();
m_ebo.bind();
// 使用 attributeLocation 获取位置,避免硬编码
int posAttr = m_program.attributeLocation("aPos");
int colorAttr = m_program.attributeLocation("aColor");
int texAttr = m_program.attributeLocation("aTexCoord"); // 纹理
// 设置属性数组:(位置/步长6个GLfloat)
//把显存布局接到 aPos/aColor
// 位置坐标:从偏移 0 开始,取3 个 float
m_program.setAttributeBuffer(posAttr, GL_FLOAT, 0, 3, GLF_SIZE * sizeof(GLfloat));
m_program.enableAttributeArray(posAttr);
// 颜色坐标:偏移 3 个 float,取 3 个分量
m_program.setAttributeBuffer(colorAttr, GL_FLOAT, 3 * sizeof(GLfloat), 3, GLF_SIZE * sizeof(GLfloat));
m_program.enableAttributeArray(colorAttr);
if(TEXTURE_STATUS){
// 纹理坐标:偏移 6 个 float,取 2 个分量
m_program.setAttributeBuffer(texAttr, GL_FLOAT, 6 * sizeof(GLfloat), 2, GLF_SIZE * sizeof(GLfloat));
m_program.enableAttributeArray(texAttr);
}
//Uniform是另一种从我们的应用程序在 CPU 上传递数据到 GPU 上的着色器的方式
// ★ 重点:矩阵传值的正确写法 (Qt 5.12 必须先获取 location) ★
//4.用 uniform 矩阵传旋转角(动画的真相)
int matrixLocation = m_program.uniformLocation("uMatrix");//查询uniform ourColor的位置值
if (matrixLocation != -1) {
// 用 m_angle 派生出"会呼吸的缩放"和"来回漂浮的平移",全是局部变量,不用改头文件
float pulse = 0.75f + 0.35f * qSin(qDegreesToRadians(m_angle * 2.0f)); // 缩放 0.4~1.1
float driftX = 0.25f * qCos(qDegreesToRadians(m_angle)); // 水平来回漂浮
QMatrix4x4 matrix;
matrix.scale(pulse, pulse, 1.0f); // 积木一:缩放(Scale),x、y轴等比变化,Z轴不变
matrix.rotate(m_angle, 0.0f, 0.0f, 1.0f); // 积木二:旋转(Rotate) 绕Z轴
matrix.translate(driftX, 0.0f, 0.0f); // 积木三:平移(Translate),沿 X 轴左右移动
m_program.setUniformValue(matrixLocation, matrix);
if(TEXTURE_STATUS){
// 采样器是 int 类型:告诉它去 0 号纹理单元取样
m_program.setUniformValue("uTexture", 0);
if (m_texture) m_texture->bind(0);
}
}
// 5. 用索引绘制:靠"字节偏移"切三角形段 / 四边形段
// 三角形段:从 EBO 第 0 个索引起,3 个索引 -> GL_TRIANGLES
// 四边形段:从 EBO 第 3 个索引起,4 个索引 -> GL_TRIANGLE_STRIP
// 最后一个参数是"字节偏移",不是索引个数:每个索引 2 字节(GLushort)。
if (m_shape == 0) {
//glDrawArrays(GL_TRIANGLES, 0, 3); // 用下标0~2这3个顶点:三角形
// 三角形:索引 0,1,2
glDrawElements(GL_TRIANGLE_STRIP, 3, GL_UNSIGNED_SHORT,
(const void *)(m_shapelist[ELEMNNT_TRIANGLE] * sizeof(GLushort)));
} else {
// 四边形:索引 0,1,3,4 —— 偏移 3 个索引 = 3 * 2 字节
glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT,
(const void *)(m_shapelist[ELEMNNT_QUAD] * sizeof(GLushort)));
}
//6.关闭属性数组,释放绑定---收尾
if(TEXTURE_STATUS){
if (m_texture) m_texture->release();
m_program.disableAttributeArray(texAttr);
}
m_program.disableAttributeArray(posAttr);
m_program.disableAttributeArray(colorAttr);
m_program.release();
}
项目源码可在资源中下载。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/liangyuna8787/article/details/166995110



