Learn OpenGL In Qt之炫酷进度条


竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
公众号: C++学习与探索 | 个人主页: rainInSunny | 个人专栏: Learn OpenGL In Qt


文章目录

经过之前内容的学习,我们已经掌握了如何通过OpenGL在Qt提供的环境下绘制一个三角形,我们知道绘制一个三角形需要VAO,VBO,在一些场景还需要EBO,然后我们需要搞定着色器,最后我们绑定VAO,调用绘制接口就能绘制出想要的三角形。下面是时候来绘制一些有趣的进度条了。

设计实现

目录结构

先让我们看看完成后的目录结构:

文件很简单,因为在工程实现的角度,绘制有趣的进度条和绘制一个三角形基本是一样的。CMakeLists是CMake的构建配置文件,它帮我们将下面这些文件添加到构建体系。coolprogress.h/.cpp就是核心实现进度条绘制的文件,它提供了一个进度条的控件类,可以像使用Qt其它控件一样使用它。main.cpp是整个程序的入口函数,所有C++程序都有这样一个入口函数。progressexample.h/.cpp主要是构造一个控件去使用coolprogress.h/.cpp中提供的进度条。progressshader.h提供了绘制进度条所需要的着色器程序,这一部分十分复杂,需要很好的数学功底才能完全弄懂,可惜我还没有>-<,只能看懂一些简单的,但在工程实现的角度,我们不用太关心这其中的原理,抄过来也许是个不错的选择。最后是shaderprogram.h,这就是我们之前写的shader类,更换了命名防止后续会发生命名冲突,然后稍微做了一些修改。

需要哪些类

所需要的类也很简单,当然如果你有更好的设计完全可以按照自己的想法去写。

这里的ShaderProgram提供着色器构造、激活、Uniform变量设置等功能。CoolProgress是一个继承于QOpenGLWidget的控件类,对外提供接口。CoolProgressImpl类包含进度条实现过程中的数据集合,不对外暴露,该类的实例被CoolProgress类创建,并由智能指针管理生命周期。pImpl设计模式经常会在实际过程中用到,这样做的好处是能够保证一定的二进制兼容性。

接口设计

核心的接口都在coolprogress.h文件中:

cpp 复制代码
#ifndef COOL_PROGRESS_H
#define COOL_PROGRESS_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <memory>

class CoolProgressImpl;

class CoolProgress : public QOpenGLWidget
{
	Q_OBJECT

public:
	enum ProgressStyle
	{
		Ring_1 = 0,
		Ring_2,
		Ring_3,
		FlashDot_1,
		FlashDot_2,
		FlashDot_3,
		FlashDot_4,
		Rect_1,
		Rect_2,
		Rect_3,
		Polygon_1,
		WaterWave_1
	};
public:
	CoolProgress(bool bTransparent, ProgressStyle style, int width = 100, int height = 100, QWidget *parent = nullptr);
	~CoolProgress();

	void startProgress(float speed = 1.0);
	void stopProgress();
	void updateProgress();
	// only valid in Ring style
	void setRingRadius(float r1, float r2);

protected:
	void initializeGL() override;
	void paintGL() override;
	void resizeGL(int w, int h) override;

private:
	void initShader();
	void setShaderUniform();

private:
	std::unique_ptr<CoolProgressImpl> m_impl;
};
#endif
  • CoolProgressImpl是上面提到的数据类,m_impl指针指向该类实例,通过std::unique_ptr管理生命周期。
  • ProgressStyle枚举表示进度条的样式类型。
  • CoolProgressbTransparent表示是否需要背景透明,透明则背景显示底层控件的颜色,不透明显示黑色。
  • startProgressstopProgress表示开始和结束进度动效,开始时可以设置一个动画效果速度。
  • updateProgress用于更新进度条界面。
  • setRingRadius用于给圆环类进度设置内外半径。
  • initializeGLpaintGLresizeGL前面文章有解释过。
  • initShadersetShaderUniform用于初始化shader内容和设置初始Uniform值。

关键函数

关键的实现依旧是前面讲到的initializeGLpaintGL

cpp 复制代码
void CoolProgress::initializeGL()
{
	if (m_impl->m_bInit)
		return;

	m_impl->m_bInit = true;
	makeCurrent();
	QOpenGLContext *pContext = context();
	if (pContext)
		m_impl->m_funcs = pContext->versionFunctions<QOpenGLFunctions_3_3_Core>();
	if (!m_impl->m_funcs)
	{
		qWarning() << "Could not obtain required OpenGL context version";
		Q_ASSERT(false);
		return;
	}
	m_impl->m_funcs->glViewport(0, 0, width(), height());

    ShaderProgram *pShaderProgram = new ShaderProgram(pContext);
    m_impl->m_shaderProgram.reset(pShaderProgram);
    const char *vertexShaderSource = m_impl->vShaderMap[m_impl->m_style].c_str();
    const char *fragmentShaderSource = m_impl->fShaderMap[m_impl->m_style].c_str();
    if (!m_impl->m_shaderProgram->compileSourceCode(vertexShaderSource, fragmentShaderSource))
    {
        Q_ASSERT(false);
        return;
    }

	float vertices[] = {
		// 第一个三角形
		1.0f, 1.0f, 0.0f,   // 右上角
		1.0f, -1.0f, 0.0f,  // 右下角
		-1.0f, 1.0f, 0.0f,  // 左上角
		// 第二个三角形
		1.0f, -1.0f, 0.0f,  // 右下角
		-1.0f, -1.0f, 0.0f, // 左下角
		-1.0f, 1.0f, 0.0f   // 左上角
	};

	m_impl->m_funcs->glGenVertexArrays(1, &m_impl->m_VAO);
	m_impl->m_funcs->glGenBuffers(1, &m_impl->m_VBO);
	m_impl->m_funcs->glBindVertexArray(m_impl->m_VAO);

	m_impl->m_funcs->glBindBuffer(GL_ARRAY_BUFFER, m_impl->m_VBO);
	m_impl->m_funcs->glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	m_impl->m_funcs->glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	m_impl->m_funcs->glEnableVertexAttribArray(0);
	m_impl->m_funcs->glBindBuffer(GL_ARRAY_BUFFER, 0);
	m_impl->m_funcs->glBindVertexArray(0);

	setShaderUniform();
}

initializeGL:

  1. 判断是否初始化过,防止重入。
  2. 获取QOpenGLFunctions_3_3_Core对应的函数指针,获取后就能使用OpenGL接口。
  3. 通过ShaderProgram类构造着色器。
  4. 定义绘制的顶点,这里顶点对应一个铺满控件的正方形,x、y坐标范围在(-1,1)。
  5. 设置VAB、VBO。
cpp 复制代码
void CoolProgress::paintGL()
{
	m_impl->m_funcs->glClear(GL_COLOR_BUFFER_BIT);
	m_impl->m_funcs->glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	m_impl->m_funcs->glEnable(GL_BLEND);
	m_impl->m_funcs->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	m_impl->m_shaderProgram->use();
	if (m_impl->m_timer.isActive())
	{
		qint64 time = m_impl->m_elapsedTimer.elapsed();
		float value = time / 1000.0 * m_impl->m_speed;
		m_impl->m_shaderProgram->setFloat("deltaTime", value);
	}
	m_impl->m_funcs->glBindVertexArray(m_impl->m_VAO);
	m_impl->m_funcs->glDrawArrays(GL_TRIANGLES, 0, 6);
}

paintGL:

  1. 进行颜色清理,开启颜色混合。
  2. 激活着色器。
  3. 设置deltaTime对应Uniform的值。
  4. 绑定VAO。
  5. 绘制三角形。

实现效果

背景不透明:

背景透明:

Shader解析

GLSL基本函数

GLSL提供了一些基本函数供我们使用:

  • abs(x):绝对值
  • ceil(x):向上取整
  • floor(x):向下取整
  • fract(x):小数部分
  • sqrt(x):平方根
  • pow(x, y):x 的 y 次幂
  • exp(x):e 的 x 次幂
  • log(x):自然对数
  • sin(x), cos(x), tan(x):三角函数
  • asin(x), acos(x), atan(x):反三角函数
  • min(x, y) / max(x, y):取最小/最大值
  • clamp(x, minVal, maxVal):限制 x 的范围
  • smoothstep(edge0, edge1, x):edge0和edge1区间进行平滑插值

这里对clamp函数和smoothstep函数进行简单说明:

clamp

  • 函数原型
cpp 复制代码
genType clamp(genType x, genType minVal, genType maxVal);
  • 参数说明

    • x:要限制的值(可以是标量、向量或矩阵)。
    • minVal:限制的最小值。
    • maxVal:限制的最大值。
  • 返回值

    • 如果x < minVal,返回 minVal。
    • 如果x > maxVal,返回 maxVal。
    • 如果minVal <= x <= maxVal,返回x。
  • 使用示例

cpp 复制代码
void main()
{
    float value = 1.5;
    float clampedValue = clamp(value, 0.0, 1.0);
    // clampedValue 将被限制为 1.0,因为 1.5 超出了范围

    vec3 color = vec3(1.2, -0.5, 0.7);
    vec3 clampedColor = clamp(color, vec3(0.0), vec3(1.0));
    // clampedColor 将变为 (1.0, 0.0, 0.7)
}
  • 应用场景
    • 颜色处理:在处理颜色值时,确保其在有效范围内(如0到1)。
    • 位置或尺寸限制:在计算物体位置或尺寸时,确保其不会超出特定范围。
    • 控制效果:可以用来限制某些效果的强度,如光照、透明度等。

smoothstep

  • 函数原型
cpp 复制代码
float smoothstep(float edge0, float edge1, float x);
  • 参数说明

    • edge0:插值的起始边界。
    • edge1:插值的结束边界。
    • x:输入值,将在edge0和edge1之间进行插值。
  • 返回值

    smoothstep根据输入值x的位置返回一个平滑插值的值:

    • 如果x < edge0,返回 0。
    • 如果x > edge1,返回 1。
    • 如果edge0 <= x <= edge1,返回一个平滑过渡的值,通常采用S型曲线插值。
  • 使用示例

cpp 复制代码
void main()
{
    float value = 0.5;  // 输入值
    float edge0 = 0.0;  // 起始边界
    float edge1 = 1.0;  // 结束边界
    
    float result = smoothstep(edge0, edge1, value);
    // result 将会是 0.5 的平滑插值
}
  • 应用场景
    • 抗锯齿效果:在渲染时,可以使用smoothstep处理边缘,减少锯齿现象。
    • 渐变效果:在颜色或透明度变化中使用,以实现更平滑的过渡。
    • 生成自然的动画:在动画中,使物体的移动或变化更加自然平滑。

实现分析

水平有限,只能分析下两个圆环进度动效的实现,这里标识上面效果图中从左到右依次为效果一、二、三。简单从动效分析可以得到下面基本的规律,下面片段着色器的实现也是围绕这些点来展开的。

  • 需要在整个画布上绘制圆环,圆环以外的点颜色值都为黑色或者透明。
  • 效果一在固定某一时刻,圆环上的的像素随上图中角1变化而变化。
  • 效果一在时间变化过程中,可以看作是点(固定颜色值)在绕圆心旋转,或者可以看成是点不动,每个点的颜色随着时间周期变化。由于我们操作的是片段着色器,所以看作是后者。
  • 效果二圆环上颜色随着圆环半径变化,圆环半径随着时间周期性变化。

效果一

片段着色器代码:

cpp 复制代码
"#version 330 core\n"
"#define SMOOTH(r) (mix(1.0, 0.0, smoothstep(0.9,1.0, r)))\n"
"#define M_PI 3.1415926535897932384626433832795\n"
"out vec4 fragColor;\n"
"in vec3 aPosFrag;\n"
"uniform float deltaTime;\n"
"uniform float r1;\n"
"uniform float r2;\n"
"float movingRing(vec2 uv, vec2 center, float r1, float r2)\n"
"{\n"
"	vec2 d = uv - center;\n"
"	float r = sqrt( dot( d, d ) );\n"
"	d = normalize(d);\n"
"	float theta = -atan(d.y,d.x);\n"
"	theta  = mod(-deltaTime+0.5*(1.0+theta/M_PI), 1.0);\n"
"	//anti aliasing for the ring's head (thanks to TDM !)\n"
"	theta -= max(theta - 1.0 + 1e-2, 0.0) * 1e2;\n"
"	return theta*(SMOOTH(r/r2)-SMOOTH(r/r1));\n"
"}\n"
"void main()\n"
"{\n"
"	vec2 uv = aPosFrag.xy;\n"
"	float ring = movingRing(uv, vec2(0.0), r1, r2);\n"
"	fragColor = vec4( 0.0 + 0.9*ring );\n"
"}\0";

过程解析如下:

  • #version 330 core:版本号是330。
  • #define SMOOTH® (mix(1.0, 0.0, smoothstep(0.9,1.0, r))):定义SMOOTH®用于抗锯齿。
  • #define M_PI 3.1415926535897932384626433832795:定义PI。
  • out vec4 fragColor:定义输出颜色变量。
  • in vec3 aPosFrag:顶点着色器传入的坐标,由于绘制二维图形,z坐标都是0,没有意义。
  • uniform float deltaTime:主程序中传入的时间变量,随程序运行改变。
  • uniform float r1/r2:主程序传入的圆环半径。
  • float movingRing(vec2 uv, vec2 center, float r1, float r2):定义函数。
    • vec2 d = uv - center:计算当前点坐标向量和中心向量的差。
    • float r = sqrt( dot( d, d ) ):计算当前点的半径。
    • d = normalize(d):将差值向量归一化。
    • float theta = -atan(d.y,d.x):计算当前点与圆心连线和x轴的角度,用角1标识。
    • theta = mod(-deltaTime+0.5*(1.0+theta/M_PI), 1.0):构造了一个函数,该函数满足当deltaTime固定时,计算得到的theta值(可以看作颜色值)随当前点对应角1周期变化,当角1不变,当前点颜色值随时间周期性变化,满足上面的规律,并且得到的值在0到1范围(mod保证)可以表示颜色值。
    • theta -= max(theta - 1.0 + 1e-2, 0.0) * 1e2:存疑。
    • return theta*(SMOOTH(r/r2)-SMOOTH(r/r1)):由于smoothstep的特点,(SMOOTH(r/r2)-SMOOTH(r/r1))可以表示一个半径为r1和r2的圆环(r1 > r2),根据r的值不同分析可得r > r1 > r2时,表达式值为0,此时表示圆环外部的点;当r1 > r > r2,表达式的值为1,表示圆环中的点;当r1 > r2 > r,表达式值为0,表示圆环内部的点。另外这里添加了抗锯齿,当r在0.9至1.0倍r1大小和在0.9至1.0倍r2大小时,由于smoothstep中间段的插值,有抗锯齿的效果。最后再乘上颜色值theta,保证了只有圆环中的点颜色值不为0(笼统表述,抗锯齿边界部分除外)。
  • void main():定义main函数。
    • vec2 uv = aPosFrag.xy:获取当前点坐标。
    • float ring = movingRing(uv, vec2(0.0), r1, r2):调用函数计算颜色值。
    • fragColor = vec4( 0.0 + 0.9*ring ):输出颜色值。

效果二

片段着色器代码:

cpp 复制代码
"#version 330 core\n"
"uniform float deltaTime;\n"
"out vec4 fragColor;\n"
"in vec3 aPosFrag;\n"
"vec4 DARK_UI = vec4(0.0);\n"
"vec4 BU_BLUE = vec4(.2,.4,.7, 1.0);\n"
"vec4 BU_BLUE_END = vec4(.2,.4,.7, 0.0);\n"
"void main()\n"
"{\n"
"	vec2 uv = aPosFrag.xy * 0.5;\n"
"	vec4 c = DARK_UI;\n"
"	float q = smoothstep(0.,1.,mod(deltaTime/15.,.1)/.1);\n"
"	float m = clamp(length(uv)*2.5,0.,1.);\n"
"	if (abs(length(uv)-q/5.)<.01) {\n"
"		c=mix(BU_BLUE,BU_BLUE_END,m);\n"
"	}\n"
"	fragColor = c;\n"
"}\0";

过程解析如下:

  • #version 330 core:版本号是330。
  • uniform float deltaTime:主程序中传入的时间变量,随程序运行改变。
  • out vec4 fragColor:输出颜色变量。
  • in vec3 aPosFrag:顶点着色器传入的坐标。
  • vec4 DARK_UI = vec4(0.0):定义背景色。
  • vec4 BU_BLUE = vec4(.2,.4,.7, 1.0):定义起始颜色。
  • vec4 BU_BLUE_END = vec4(.2,.4,.7, 0.0):定义终止颜色。
  • void main():定义main函数。
    • vec2 uv = aPosFrag.xy * 0.5:根据顶点着色器传入坐标转换为绘制的坐标,这里相当于将图形宽高放大两倍。
    • vec4 c = DARK_UI:将颜色值设置为背景色。
    • float q = smoothstep(0.,1.,mod(deltaTime/15.,.1)/.1):构造一个基准半径q,该半径随着时间变量在0.0至1.0范围做周期变化。
    • float m = clamp(length(uv)*2.5,0.,1.):构造一个混合系数,该系数随着距离圆心距离增加在0.0至1.0范围变化。
    • if (abs(length(uv)-q/5.)<.01):相当于构造了一个圆环,圆环基准半径随着q周期变化,圆环宽度在0.01*2。满足判断条件表示在圆环内。
    • c=mix(BU_BLUE,BU_BLUE_END,m):圆环内的颜色值设置为起始颜色和终止颜色根据m值混合,半径越大,m越大,越接近终止颜色。
    • fragColor = c:输出颜色值。

对于其他效果,大家有兴趣可以自行下载源码尝试。(终于写完这篇了,时间仓促,有错误的地方大家多多指正~)

关注公众号:C++学习与探索,有惊喜哦~

相关推荐
ragnwang1 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
kiiila2 小时前
【Qt】对象树(生命周期管理)和字符集(cout打印乱码问题)
开发语言·qt
lqqjuly4 小时前
特殊的“Undefined Reference xxx“编译错误
c语言·c++
黄金右肾5 小时前
Qt之数据库使用(十四)
sql·qt·sqlite·database
冰红茶兑滴水5 小时前
云备份项目--工具类编写
linux·c++
刘好念5 小时前
[OpenGL]使用 Compute Shader 实现矩阵点乘
c++·计算机图形学·opengl·glsl
酒鬼猿6 小时前
C++进阶(二)--面向对象--继承
java·开发语言·c++
姚先生976 小时前
LeetCode 209. 长度最小的子数组 (C++实现)
c++·算法·leetcode
杨德杰7 小时前
QT多媒体开发(一):概述
qt·音视频·多媒体
小王爱吃月亮糖7 小时前
QT开发【常用控件1】-Layouts & Spacers
开发语言·前端·c++·qt·visual studio