OpenGL with QtWidgets:练习之绘制2D环形进度条

1.实现思路

这里主要涉及几个点:绘制圆环,绘制文字,动画,抗锯齿。

绘制圆环网上有些人是计算好圆边的顶点后传入的,我这里直接在片段着色器里根据距离圆心的距离来渲染的圆环。

void main()
{
    float len = abs(sqrt(pow(thePos.x,2)+pow(thePos.y,2)));
    float alpha = abs(len-0.75);
    alpha = (alpha>0.15)?0.0:1.0;
    FragColor = vec4(0.4,0.1,0.6,alpha);
}

绘制文字的话,网上有的人是先绘制到纹理上再渲染问题,不过 Qt 的 QOpenGLWidget 类可以配合 QPianter 使用。因为是 2D,我直接使用 QPianter 绘制的文字。这里面也遇到问题,就是 QPainter 绘制的文字和在别的画布上呈现的效果不一样,最后选择的微软雅黑效果才好点。

void CircleProgressBar::paintGL()
{
    QPainter painter(this);
    painter.setPen(Qt::white);
    painter.setFont(QFont("Microsoft YaHei",16));
    const QString text_val=QString::number(progress*100,'f',2)+" %";
    const int text_x=width()/2-painter.fontMetrics().width(text_val)/2;
    const int text_y=height()/2+painter.fontMetrics().height()/2;
    painter.drawText(text_x,text_y,text_val);
}

动画我使用的属性动画而不是 QTimer,让 Qt 来决定刷新的时机。

抗锯齿我参照了网上的一些方式,比如多重采样什么的都没效果,最后用的 smoothstep 函数来实现的圆环部分的抗锯齿。

(参照:Shader smoothstep使用_冠位仓鼠--慕白-CSDN博客_smoothstep

void main()
{
    float len = abs(sqrt(pow(thePos.x,2)+pow(thePos.y,2)));
    //float alpha = 1.0-smoothstep(0.15,0.15+aSmoothWidth,abs(len-0.75));
    float alpha = smoothstep(0.15+aSmoothWidth,0.15,abs(len-0.75));
    FragColor = vec4(0.4,0.1,0.6,alpha);
}

色条上的锯齿用的 smoothstep 配合 mix 消除。

(题外话,我开始以为 GLSL 没有 atan2 函数,哪成想他的 atan 有个重载版本就是 atan2 的功能;此外,OpenGL 和 OpenGL ES  的 GLSL 在语法上有一些小的区别,有点坑) 

(2021-5-4)修改了进度值为 0 时因为 smoothstep 导致还有一条横线的 bug,增加了进度大于零的判断。

2.实现代码

(项目 git 链接:https://github.com/gongjianbo/EasyOpenGL2D

实现效果(GIF):

主要实现代码:

#ifndef CIRCLEPROGRESSBAR_H
#define CIRCLEPROGRESSBAR_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>

#include <QPropertyAnimation>

//龚建波:环形进度条
class CircleProgressBar : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
{
    Q_OBJECT
    Q_PROPERTY(double drawValue READ getDrawValue WRITE setDrawValue)
public:
    explicit CircleProgressBar(QWidget *parent = nullptr);
    ~CircleProgressBar();

    void setRange(double min,double max);
    void setValue(double value);

    double getDrawValue() const;
    void setDrawValue(double value);

protected:
    //设置OpenGL资源和状态。在第一次调用resizeGL或paintGL之前被调用一次
    void initializeGL() override;
    //渲染OpenGL场景,每当需要更新小部件时使用
    void paintGL() override;
    //设置OpenGL视口、投影等,每当尺寸大小改变时调用
    void resizeGL(int width, int height) override;

private:
    //着色器程序
    QOpenGLShaderProgram shaderProgram;
    //顶点数组对象
    QOpenGLVertexArrayObject vao;
    //顶点缓冲
    QOpenGLBuffer vbo;
    //属性动画
    QPropertyAnimation *animation;
    //进度值
    double progressMin=0;
    double progressMax=100;
    double progressValue=0; //设置的值
    double progressDraw=0; //绘制临时值
};

#endif // CIRCLEPROGRESSBAR_H
#include "CircleProgressBar.h"

#include <QPainter>
#include <QDebug>

CircleProgressBar::CircleProgressBar(QWidget *parent)
    : QOpenGLWidget(parent)
{
    animation=new QPropertyAnimation(this,"drawValue");
    animation->setDuration(2000); //动画持续时间
    animation->setEasingCurve(QEasingCurve::OutQuart); //先快后慢
}

CircleProgressBar::~CircleProgressBar()
{
    makeCurrent();
    vbo.destroy();
    vao.destroy();
    doneCurrent();
}

void CircleProgressBar::setRange(double min, double max)
{
    if(progressMax<=progressMin)
        return;
    progressMin=min;
    progressMax=max;
}

void CircleProgressBar::setValue(double value)
{
    if(value<progressMin||value>progressMax)
        return;
    progressDraw=progressValue;
    progressValue=value;

    animation->setStartValue(progressDraw);
    animation->setEndValue(progressValue);
    animation->start();
}

double CircleProgressBar::getDrawValue() const
{
    return progressDraw;
}

void CircleProgressBar::setDrawValue(double value)
{
    progressDraw=value;
    update();
}

void CircleProgressBar::initializeGL()
{
    //为当前上下文初始化OpenGL函数解析
    initializeOpenGLFunctions();

    //着色器代码
    //in输入,out输出,uniform从cpu向gpu发送
    //[aPos]两个三角的顶点数据
    //[thePos]表示当前像素点
    const char *vertex_str=R"(#version 330 core
                           layout (location = 0) in vec2 aPos;
                           out vec2 thePos;
                           void main()
                           {
                             gl_Position = vec4(aPos, 0.0, 1.0);
                             thePos = aPos;
                           })";
    //GLSL的atan2也叫atan,不过参数不同,我们封装一个0-360度的归一化值[0,1]的版本
    //[FragColor]该点输出颜色,gl_FragColor在3移除了,自己声明一个
    //[aValue]进度值
    //[aSmoothWidth]用来计算平滑所需宽度,根据绘制区域大小来计算
    //[len]坐标点距离圆心的距离,[0,1],勾股定理
    //[alpha]使用smoothstep平滑函数取0.75±0.15的圆圈透明度为1
    //[angle]thePos像素点对应的角度值,用于调节渐变,归一化到[0,1]
    //[angle_smooth]进度值那条斜线取平滑
    //[ret smoothstep(a,b,x)]可以用来生成0-1的平滑过渡,达到抗锯齿效果
    //返回0: x<a<b 或者 x>a>b
    //返回1: x<b<a 或者 x>b>a
    //返回n: 根据x在ab间位置,返回[0,1]过度值
    const char *fragment_str=R"(#version 330 core
                             #define PI 3.14159265
                             uniform float aValue;
                             uniform float aSmoothWidth;
                             in vec2 thePos;
                             out vec4 FragColor;

                             float myatan2(float y,float x)
                             {
                               float ret_val = 0.0;
                               if(x != 0.0){
                                 ret_val = atan(y,x);
                                 if(ret_val < 0.0){
                                   ret_val += 2.0*PI;
                                 }
                               }else{
                                 ret_val = y>0 ? PI*0.5 : PI*1.5;
                               }
                               return ret_val/(2.0*PI);
                             }

                             void main()
                             {
                             float len = abs(sqrt(pow(thePos.x,2.0)+pow(thePos.y,2.0)));
                             float alpha = smoothstep(0.15+aSmoothWidth,0.15,abs(len-0.75));
                             float angle = myatan2(thePos.y,thePos.x);
                             float angle_smooth = smoothstep(aValue+aSmoothWidth/3.0,aValue,angle);

                             if(angle_smooth>0.0 && aValue>0.0){
                               if(angle_smooth>=1.0){
                                 FragColor = vec4(1.0,0.1,(1.0-angle),alpha);
                               }else{
                                 FragColor = vec4(mix(vec3(1.0,0.1,(1.0-angle)),vec3(0.4,0.1,0.6),1.0-angle_smooth),alpha);
                               }
                             }else{
                               FragColor = vec4(0.4,0.1,0.6,alpha);
                             }
                             })";


    //将source编译为指定类型的着色器,并添加到此着色器程序
    if(!shaderProgram.addCacheableShaderFromSourceCode(
                QOpenGLShader::Vertex,vertex_str)){
        qDebug()<<"compiler vertex error"<<shaderProgram.log();
    }
    if(!shaderProgram.addCacheableShaderFromSourceCode(
                QOpenGLShader::Fragment,fragment_str)){
        qDebug()<<"compiler fragment error"<<shaderProgram.log();
    }
    //使用addShader()将添加到该程序的着色器链接在一起。
    if(!shaderProgram.link()){
        qDebug()<<"link shaderprogram error"<<shaderProgram.log();
    }

    //两个三角拼接的一个矩形
    const float vertices[] = {
        -1.0f, -1.0f, //左下角
        +1.0f, -1.0f, //右下角
        +1.0f, +1.0f, //右上角

        +1.0f, +1.0f, //右上角
        -1.0f, +1.0f, //左上角
        -1.0f, -1.0f, //左下角
    };
    vao.create();
    vao.bind();
    vbo=QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    vbo.create();
    vbo.bind();
    vbo.allocate(vertices,sizeof(vertices));

    // position attribute
    int attr = -1;
    attr = shaderProgram.attributeLocation("aPos");
    //setAttributeBuffer(int location, GLenum type, int offset, int tupleSize, int stride = 0)
    shaderProgram.setAttributeBuffer(attr, GL_FLOAT, 0, 2, sizeof(GLfloat) * 2);
    shaderProgram.enableAttributeArray(attr);
}

void CircleProgressBar::paintGL()
{
    //以短边为边长,保持比例,Qt这里有个问题,在resize里设置的没用
    const int item_w=width()>height()?height():width();
    glViewport((width()-item_w)/2,
               (height()-item_w)/2,
               item_w,
               item_w);

    glClearColor(0.1f, 0.2f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glEnable(GL_BLEND);
    //基于源像素Alpha通道值的半透明混合函数
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    //开启多重采样抗锯齿,貌似没啥效果
    //glEnable(GL_MULTISAMPLE);

    shaderProgram.bind();
    //把进度[min,max]归一化[0,1]
    const float progress=(progressDraw-progressMin)/(progressMax-progressMin);
    qDebug()<<"draw progress"<<progress;
    shaderProgram.setUniformValue("aValue", progress);
    //aSmoothWidth用来计算平滑所需宽度,根据不同的大小来计算,这里用N px的宽度
    shaderProgram.setUniformValue("aSmoothWidth", float(3.0/item_w));
    vao.bind();

    glDrawArrays(GL_TRIANGLES, 0, 6);

    vao.release();
    shaderProgram.release();

    //目前文字用QPainter绘制
    QPainter painter(this);
    painter.setPen(Qt::white);
    painter.setFont(QFont("Microsoft YaHei",16));
    const QString text_val=QString::number(progress*100,'f',2)+" %";
    const int text_x=width()/2-painter.fontMetrics().width(text_val)/2;
    const int text_y=height()/2+painter.fontMetrics().height()/2;
    painter.drawText(text_x,text_y,text_val);
}

void CircleProgressBar::resizeGL(int width, int height)
{
    //以短边为边长,保持比例,Qt这里有个问题,在resize里设置的没用
    const int item_w=width>height?height:width;
    glViewport((width-item_w)/2,
               (height-item_w)/2,
               item_w,
               item_w);
}