Opengl (2)之Qt窗口

Opengl (2)之Qt窗口

这一学习系列,基于Qt UI组件以及QOpenGL,学习三维软件的开发。 暂定目标是做一个简单的机器人仿真软件。如果快速开发,可以直接用OSG 或者 OCCT 等开源3d引擎,加上自己熟悉的图形用户界面应用程序框架(如Qt,C#,MFC,Java等),整合出一个三维软件。这些内容在后面有空慢慢学习。这一系列就关注Qt 以及其opengl模块。

  • OpenGL是Khronos Group开发维护的一个规范,它主要为我们定义了用来操作图形和图片的一系列函数的API,需要注意的是OpenGL本身并非API。GPU的硬件开发商则需要提供满足OpenGL规范的实现,这些实现通常被称为“驱动”,它们负责将OpenGL定义的API命令翻译为GPU指令。

  • Qt 是一个开源图形用户界面应用程序框架
    这里用的QOpenGL是经过Qt封装后的一个模块。因此,opengl更新的一些新特性,如果使用了QOpengl模块,我们就没办法第一时间体验,毕竟Qt那个并不会第一时间更新QOpenGL这个模块,不过这并不影响我们学习,Qt只是按照类的思想对opengl做了一层封装,其使用方法是没多大区别的。 之所以用QOpeng,主要是想结合Qt 开发一款3d软件。

入门demo: OpenGLWindow

如何在QWindow中使用OpenGL

这个Demo目的是使用OpengL,实现基于应用程序创建一个最小QWindow
注意】这个低等级的例子是将QWindow上OpengL结合起来, 而在实际应用中我们会采用更高级的窗口类QOpenGLWindow

OpenGLWindow 超级类

OpenGLWindow 类充当一个API,它的子类做实际渲染。 它的一个函数render()用于请求渲染。要么调函数renderNow()做立即渲染,要么通过调用renderLater()实现在当前事件循环完后再处理渲染的批事件 。OpenGLWindow 的子类可以通过基于OpenGL的渲染重新实现render() 方法,或者使用QPainter渲染来重新实现render(QPainter *)方法。假设底层OpenGl驱动的使能垂直同步, 设置OpenGLWindow::setAnimating(true)时,在垂直刷新率 render()会被调用。
在窗口类中要使用OpenGL渲染,我们通常会想到继承 QOpenGLFunctions,OpenGLWindow这么实现是为了做到平台无关的访问OpenGL ES 2.0 functions。如你的应用程序使OpenGL以及OpenGL ES 2.0,继承 QOpenGLFunctions所包含的OpenGL的函数将会被优先使用,你不用关心如何解析这些函数的调用。

这里我是使用OpenGL3.3版本进行开发,因此直接继承QOpenGLFunctions_3_3_Core
OpenGLWindow.h的实现

  class OpenGLWindow : public QWindow, protected QOpenGLFunctions
  {
      Q_OBJECT
  public:
      explicit OpenGLWindow(QWindow *parent = 0);
      ~OpenGLWindow();

      virtual void render(QPainter *painter);
      virtual void render();

      virtual void initialize();

      void setAnimating(bool animating);

  public slots:
      void renderLater();
      void renderNow();

  protected:
      bool event(QEvent *event) override;

      void exposeEvent(QExposeEvent *event) override;

  private:
      bool m_animating;

      QOpenGLContext *m_context;
      QOpenGLPaintDevice *m_device;
  };
  • 窗口的表面类型必须设置为 QSurface::OpenGLSurface用于指示窗口是被用于OpenGL渲染,以及不使用QPainterQBackingStore(为窗口提供一个绘制区域)呈现光栅化内容。
OpenGLWindow::OpenGLWindow(QWindow *parent)
    : QWindow(parent)
{
    /*使用OpenGL渲染*/
    setSurfaceType(QWindow::OpenGLSurface);
}
  • OpenGL的任何初始化都需要重载initialize() 函数来完成。initialize()函数必须在调用render()函数之前被调用一次,从而获得一个当前有效的 QOpenGLContext。下面的实现中,默认的 the default render(QPainter *)initialize() 是没有实现的,而默认的 render()函数实现了QOpenGLPaintDevice的初始化,以及紧接着调用了render(QPainter *)函数
void OpenGLWindow::render(QPainter *painter)
{
    Q_UNUSED(painter);
}

void OpenGLWindow::initialize()
{
}

void OpenGLWindow::render()
{
    /*新建OpenGL绘制设备*/
    if (!m_device)
        m_device = new QOpenGLPaintDevice;

    /*清空颜色缓存,深度缓存,模板缓存*/
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    /*初始化OpenGL绘制设备*/
    m_device->setSize(size() * devicePixelRatio());
    m_device->setDevicePixelRatio(devicePixelRatio());

    QPainter painter(m_device);
    render(&painter);
}

【备注】 几个专业术语:
颜色缓冲区: 用于绘图的缓冲区,它包含了颜色索引或者RGBA颜色数据。
深度缓冲区: 存储每个像素的深度值,当启动深度测试时,片段像素深度值和深度缓冲区深度值进行比较,决定片段哪些像素点数据可以替换到颜色缓冲区中。
模板缓冲区(Stencil Buffer): 与颜色缓冲区和深度缓冲区类似,模板缓冲区可以为屏幕上的每个像素点保存一个无符号整数值。这个值的具体意义视程序的具体应用而定。在渲染的过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值。这个比较的过程被称为模板测试。模板测试发生在透明度测试(alpha test)之后,深度测试(depth test)之前。如果模板测试通过,则相应的像素点更新,否则不更新。就像使用纸板和喷漆一样精确的混图一样,当启动模板测试时,通过模板测试的片段像素点会被替换到颜色缓冲区中,从而显示出来,未通过的则不会保存到颜色缓冲区中,从而达到了过滤的功能。下图描述了模板缓冲区的原理:
Opengl (2)之Qt窗口_第1张图片

当系统准备好重绘时,renderLater()函数可以直接的调用QWindow::requestUpdate()来安排更新。
当接收到一个 expose event(曝光事件)时,我们也可以直接调用renderNow()函数。当屏幕以及发生变化时,exposeEvent() 通告窗口的曝光度,也就是可见度。当接收到expose event,你可以查询QWindow::isExposed()找到是否窗口目前已经显示。在接收到expose event之前,不能渲染和调用 QOpenGLContext::swapBuffers(),在那之前窗口的大小可能都不知道,另外要在屏幕上渲染的内容可能还没结束。

void OpenGLWindow::renderLater()
{
    /*将更新事件提交消息队列*/
    requestUpdate();
}

bool OpenGLWindow::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::UpdateRequest:
        renderNow();
        return true;
    default:
        return QWindow::event(event);
    }
}

void OpenGLWindow::exposeEvent(QExposeEvent *event)
{
    Q_UNUSED(event);

    if (isExposed())
        renderNow();
}

renderNow()

  1. 在renderNow()函数中,如果判断没有接收到Expose Event(显示事件)则会立即返回不执行后面内容.
  2. 如果判断OpenGL 上下文为空,则采用相同的 QSurfaceFormat新建QOpenGLContext,并设置到OpenGLWindow,调用initialize()用于子类初始化, 接着调用initializeOpenGLFunctions()是为了高级类QOpenGLFunctions能够关联到准确的 QOpenGLContext
  3. 在任何情况下都是通过调用 QOpenGLContext::makeCurrent()关联当前的context, 在调用render()实现实际的渲染,最后调用QOpenGLContext::swapBuffers(OpenGLWindow*)`使我们制定的需要呈现的内容显示出来.
  4. 一旦调用QOpenGLContext::makeCurrent()将上下文和当前窗口关联起来,我们就可以使用OpenGL的指令,使用方式包括:通过包含 (其中还包含系统的OpenGL 头文件),或者使用QOpenGLFunctions(为了方便我们就直接继承),或者通过QOpenGLContext::functions()访问。
  5. QOpenGLFunctions提供对所有OpenGL ES 2.0级别的OpenGL调用的访问,这些调用在OpenGL ES 2.0和桌面OpenGL中都不是标准的。
    更多关于OpenGL 和 OpenGL ES 2.0的API,可以官方文档:OpenGL Registry, Khronos OpenGL ES API Registry.
  6. 如果调用OpenGLWindow::setAnimating(true)启用动画,我们需要调用 renderLater()去安排另一个更新请求。
void OpenGLWindow::renderNow()
{
    /*检查显示事件是否触发*/
    if (!isExposed())
        return;

    bool needsInitialize = false;
    
    /*检查上下文是否有效*/ 
    if (!m_context) {
       /*新建上下文*/
        m_context = new QOpenGLContext(this);
        /*设置相同的 QSurfaceFormat*/
        m_context->setFormat(requestedFormat());
        /*根据当前的配置创建上下文*/
        m_context->create();

        needsInitialize = true;
    }

    /*上下文关联当前窗口*/
    m_context->makeCurrent(this);

    if (needsInitialize) {
        /*QOpenGLFunctions关联到当前上下文*/
        initializeOpenGLFunctions();
        /*调用子类初始化的实现*/
        initialize();
    }
    /*实现实际渲染*/
    render();
    /*交换缓冲区*/
    m_context->swapBuffers(this);

    /*跟新动画*/
    if (m_animating)
        renderLater();
}
  • 使能动画也需要安排跟新请求,如下代码所示:
void OpenGLWindow::setAnimating(bool animating)
{
    /*使能动画*/
    m_animating = animating;
    /*更新*/
    if (animating)
        renderLater();
}

子类的实现

  1. TriangleWindow继承OpenGLWindow,展示OpenGL如何渲染一个旋转的三角形。通过间接子类化QOpenGLFunctions,我们可以访问所有OpenGL ES 2.0级别的功能。
class TriangleWindow : public OpenGLWindow
{
public:
    using OpenGLWindow::OpenGLWindow;

    void initialize() override;
    void render() override;

private:
    GLint m_posAttr = 0;
    GLint m_colAttr = 0;
    GLint m_matrixUniform = 0;

    QOpenGLShaderProgram *m_program = nullptr;
    int m_frame = 0;
};
  1. 下面这段代码是这个例子要用到的OpenGL着色器程序,顶点着色器和片段着色器比较简单,实现顶点位置变换已经顶点间颜色的插补。
static const char *vertexShaderSource =
    "attribute highp vec4 posAttr;\n"
    "attribute lowp vec4 colAttr;\n"
    "varying lowp vec4 col;\n"
    "uniform highp mat4 matrix;\n"
    "void main() {\n"
    "   col = colAttr;\n"
    "   gl_Position = matrix * posAttr;\n"
    "}\n";

static const char *fragmentShaderSource =
    "varying lowp vec4 col;\n"
    "void main() {\n"
    "   gl_FragColor = col;\n"
    "}\n";
  1. TriangleWindow::initialize()
void TriangleWindow::initialize()
{
   //新建着色器程序处理工具
    m_program = new QOpenGLShaderProgram(this);
    //加载顶点着色器程序
    m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
    //加载片段着色器程序
    m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
    //链接着色器程序
    m_program->link();
    //着色器程序位置属性
    m_posAttr = m_program->attributeLocation("posAttr");
    //断言是否成功
    Q_ASSERT(m_posAttr != -1);
    //着色器程序颜色属性
    m_colAttr = m_program->attributeLocation("colAttr");
    //断言是否成功
    Q_ASSERT(m_colAttr != -1);
    //返回统一值
    m_matrixUniform = m_program->uniformLocation("matrix");
    //断言是否成功
    Q_ASSERT(m_matrixUniform != -1);
}
  1. TriangleWindow::render()
    实现渲染函数:
  • 设置视口
  • 清除背景
  • 渲染旋转三角形
void TriangleWindow::render()
{
    /*获取设备像素比*/
    const qreal retinaScale = devicePixelRatio();
    //设置视口
    glViewport(0, 0, width() * retinaScale, height() * retinaScale);
    //清空颜色缓存
    glClear(GL_COLOR_BUFFER_BIT);
    //绑定着色器程序
    m_program->bind();
    
    QMatrix4x4 matrix;
    //设置透视矩阵
    matrix.perspective(60.0f, 4.0f / 3.0f, 0.1f, 100.0f);
    //沿着Z轴负方向平移-2
    matrix.translate(0, 0, -2);
    //绕Y轴旋转
    matrix.rotate(100.0f * m_frame / screen()->refreshRate(), 0, 1, 0);
    //为着色器程序传入当前变换
    m_program->setUniformValue(m_matrixUniform, matrix);
    //三角形三个顶点
    static const GLfloat vertices[] = {
         0.0f,  0.707f,
        -0.5f, -0.5f,
         0.5f, -0.5f
    };
    //三角形每个顶点对应的颜色值
    static const GLfloat colors[] = {
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f
    };
    //为着色器设置顶点属性
    glVertexAttribPointer(m_posAttr, 2, GL_FLOAT, GL_FALSE, 0, vertices);
    //为着色器设置颜色属性
    glVertexAttribPointer(m_colAttr, 3, GL_FLOAT, GL_FALSE, 0, colors);

    //启用顶点着色器属性
    glEnableVertexAttribArray(m_posAttr);
    //启用颜色着色器属性
    glEnableVertexAttribArray(m_colAttr);
    //绘制三角形
    glDrawArrays(GL_TRIANGLES, 0, 3);

    //关闭顶点着色器属性
    glDisableVertexAttribArray(m_colAttr);
    //关闭颜色着色器属性
    glDisableVertexAttribArray(m_posAttr);
    //解绑着色器
    m_program->release();
    //为下一一次绕y轴旋转做准备
    ++m_frame;
}

  1. 应用程序main 函数
int main(int argc, char **argv)
{
    QApplication app(argc, argv);


    QSurfaceFormat format;
    //设置每个每个像素采样样本个数,用于抗锯齿
    format.setSamples(16);

    //实例化窗口
    TriangleWindow window;
    window.setFormat(format);
    window.resize(640, 480);
    window.show();

    //设置动画
    window.setAnimating(true);

    return app.exec();
}

运行程序

Opengl (2)之Qt窗口_第2张图片

资源下载

OpenGLWindow

你可能感兴趣的:(Qt,OpenGL)