竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
公众号: 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
枚举表示进度条的样式类型。CoolProgress
中bTransparent
表示是否需要背景透明,透明则背景显示底层控件的颜色,不透明显示黑色。startProgress
、stopProgress
表示开始和结束进度动效,开始时可以设置一个动画效果速度。updateProgress
用于更新进度条界面。setRingRadius
用于给圆环类进度设置内外半径。initializeGL
、paintGL
和resizeGL
前面文章有解释过。initShader
和setShaderUniform
用于初始化shader内容和设置初始Uniform值。
关键函数
关键的实现依旧是前面讲到的initializeGL
和paintGL
:
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:
- 判断是否初始化过,防止重入。
- 获取QOpenGLFunctions_3_3_Core对应的函数指针,获取后就能使用OpenGL接口。
- 通过ShaderProgram类构造着色器。
- 定义绘制的顶点,这里顶点对应一个铺满控件的正方形,x、y坐标范围在(-1,1)。
- 设置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:
- 进行颜色清理,开启颜色混合。
- 激活着色器。
- 设置deltaTime对应Uniform的值。
- 绑定VAO。
- 绘制三角形。
实现效果
背景不透明:
背景透明:
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++学习与探索,有惊喜哦~