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中,畢竟那才是真正的核心,但就像我開頭所說,很多基礎的東西都不會仔細講,但我一般會給出在哪可以學習這些東西。