1. 程式人生 > >OpenGL系列學習教程(零)---在Qt/Quick中使用OpenGL

OpenGL系列學習教程(零)---在Qt/Quick中使用OpenGL

【寫在前面】

首先,想要說明的是,本系列學習教程是根據我自己學習的經歷而寫,並非完全科普性的,零基礎的教程,而且其水平也很受我本身的水平影響,so 如果有不足之處,還請多多指教~~

其次,本系列使用 Qt/Quick 來編寫所有的opengl程式,所以和原生的opengl有一些區別,當然也不要擔心,我會另開一個使用glfw的教程來完成同樣的opengl程式。

【正文開始】

在Qt中使用OpenGL,我所知的有三種方法:

1. 繼承QOpenGLWidget(老版本Qt為QGLWidget),然後重新實現:

+void initializeGL() 此函式在第一次呼叫paintGL()或resizeGL()之前呼叫一次,主要用於初始化opengl環境,以及設定任何需要的OpenGL資源和狀態。

+viod resizeGL(int w, int h) 當該部件大小發生改變時呼叫此函式,主要用於重新設定縱橫比(用於投影矩陣)。

+void paintGL() 重新繪製該部件時呼叫此函式,主要用於實際的繪製。

2. 繼承QOpenGLWindow,同QOpenGLWidget,差別是繼承自QWindow,並且提供比widget更好的效能。

3. 本系列所使用的方法,繼承QQuickItem,並connect必要的訊號,這種方法好處是可以很輕鬆的和其他的Qml元件搭配使用,接下來我將詳細講解其步驟。

首先,我們需要一個繼承QQuickItem的類,我將它命名為OpenGLItem:

#ifndef OPENGLWINDOW_H
#define OPENGLWINDOW_H
#include "render.h"
#include <QTime>
#include <QQuickItem>
#include <QBasicTimer>

class MyRender : public Render
{
public:
    MyRender() { }
    ~MyRender() { }

    void render()
    {
        glClearColor(0.2, 0.3, 0.3, 1.0);
        glClear(GL_COLOR_BUFFER_BIT);
    }
};

class OpenGLItem : public QQuickItem
{
    Q_OBJECT

public:
    OpenGLItem();
    ~OpenGLItem();

public slots:
    void sync();
    void cleanup();

protected:
    void timerEvent(QTimerEvent *event);

private:
    QBasicTimer m_timer;
    Render *m_render;
};

#endif // OPENGLWINDOW_H

先看OpenGLItem,MyRender待會再進行講解,關鍵的兩個槽函式 :void sync(), void cleanup() 實現如下:

void OpenGLItem::sync()
{
    if (!m_render)
    {
        m_render = new MyRender();
        m_render->initializeGL();   //可以放在Render的建構函式中
        m_render->resizeGL(window()->width(), window()->height());
        connect(window(), &QQuickWindow::beforeRendering, this, [this]()
        {
            //window()->resetOpenGLState();
            m_render->render();
        }, Qt::DirectConnection);
        connect(window(), &QQuickWindow::afterRendering, this, [this]()
        {
            //渲染後呼叫,計算fps
        }, Qt::DirectConnection);
        connect(window(), &QQuickWindow::widthChanged, this, [this]()
        {
            m_render->resizeGL(window()->width(), window()->height());
        });
        connect(window(), &QQuickWindow::heightChanged, this, [this]()
        {
            m_render->resizeGL(window()->width(), window()->height());
        });
    }
}

void OpenGLItem::cleanup()
{
    if (m_render)
    {
        delete m_render;
        m_render = nullptr;
    }
}

可以看到,sync()函式主要是進行一些初始化工作,並連線相應的訊號,其中beforeRendering是在真正渲染之前發出的,要理解Qt Quick整個渲染過程,我先上一張圖片:

所以我們要把渲染工作放到beforeRendering()和afterRender()之間,我這裡直接在beforeRendering()發出之後立即開始繪製:m_render->render(),這裡有一個地方必須要注意,那就是要確保connect的連線型別為:Qt::DirectConnection

而afterRender()應該是計算fps(幀率)的地方,這裡不會用到,當視窗大小發生改變時,就必須重置opengl的視口,計算縱橫比等等,這些應該在Render::resizeGL(int w, int h)中進行,所以這裡只需要連線訊號並呼叫:m_render->resizeGL()。

cleanup()做一些清理工作,這裡僅僅是釋放並置空m_render。

那麼,sync()和cleanup()在何時呼叫呢?來看OpenGLItem的建構函式:

OpenGLItem::OpenGLItem()
    : m_render(0)
{
    m_timer.start(12, this);
    connect(this, &QQuickItem::windowChanged, this, [this](QQuickWindow *window)
    {
        if (window)
        {
            connect(window, &QQuickWindow::beforeSynchronizing, this, &OpenGLItem::sync,
                    Qt::DirectConnection);
            connect(window, &QQuickWindow::sceneGraphInvalidated, this, &OpenGLItem::cleanup,
                    Qt::DirectConnection);

            window->setClearBeforeRendering(false);
        }
        else return;
    });
}

我們看到,在beforeSynchronizing()訊號發出時呼叫sync(),此訊號在場景圖與QML狀態同步之前發出,可以理解為:繪製準備的訊號,所以sync()也確實是做繪製準備的工作,而sceneGraphInvalidated()是連線到cleanup()的,這個訊號意味著所使用的圖形呈現上下文已經失效,所有與該上下文相關的使用者資源都應該被釋放,並且,兩個connect的型別還是:Qt::DirectConnection

還有一點要注意的就是最後一行:window->setClearBeforeRendering(false); setClearBeforeRendering()設定QML場景圖形渲染是否在開始渲染之前清除顏色緩衝區,禁用它可以保證呈現我們自己的OpengGL內容。

這些工作是在QQuickItem發出windowChanged()訊號後進行的,windowChanged()在Item的視窗發生改變時發出。

還有一個timerEvent(),它提醒視窗進行重繪,間隔為12毫秒:

void OpenGLItem::timerEvent(QTimerEvent *event)
{
    Q_UNUSED(event);
    window()->update();
}

接下來是Render類,寫的很簡單,但是已經可以看出OpenGL的繪製步驟了:

#ifndef RENDER_H
#define RENDER_H
#include <QOpenGLFunctions>

class Render : protected QOpenGLFunctions
{

public:
    Render() { }
    virtual ~Render() { }

public:
    virtual void initializeGL();
    virtual void initializeShader();

    virtual void resizeGL(int w, int h);
    virtual void render();    //與paintGL相似,但我更喜歡叫render
};

#endif // RENDER_H
#include "render.h"

void Render::initializeGL()
{
    initializeOpenGLFunctions();
    initializeShader();
}

void Render::initializeShader()
{

}

void Render::resizeGL(int w, int h)
{
    glViewport(0, 0, w, h);
}

void Render::render()
{

}

關於這些函式的命名我還是按照QOpenGLWidget裡面的來的,當然大致的工作也可以從函式名可以看出,不過這一篇只講怎麼在Qt Quick中使用opengl,所以這些函式都只做了最基本的工作。

然後自己的render類只需要繼承Render,重寫這幾個虛擬函式就行了,就像我上面的MyRender。

最後還是老樣子,註冊到qml中就可以愉快的使用了:

#include "openglItem.h"
#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);
#ifdef Q_OS_ANDROID
    app.setAttribute(Qt::AA_UseOpenGLES);
#else
    app.setAttribute(Qt::AA_UseDesktopOpenGL);
#endif
    qmlRegisterType<OpenGLItem>("an.OpenGLItem", 1, 0, "OpenGLItem");
    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}

main.qml:

import QtQuick 2.9
import QtQuick.Window 2.2
import an.OpenGLItem 1.0

Window
{
    visible: true
    width: Qt.platform.os == "android" ? Screen.desktopAvailableWidth  : 640
    height: Qt.platform.os == "android" ? Screen.desktopAvailableHeight : 480

    title: qsTr("MPS Opengl Qt/Quick 教程(0)!")

    OpenGLItem
    {
        id: openGLItem
        visible: true
        anchors.fill: parent
    }
}

最後來看一下效果圖(只是一個灰綠色的空視窗而已):

【結語】

啊終於講完了這最開始的一篇,主要還是視窗相關的東西,接下來將會把注意力轉移到render中,畢竟那才是真正的核心,但就像我開頭所說,很多基礎的東西都不會仔細講,但我一般會給出在哪可以學習這些東西。