使用 QGraphicsScene 繪製 widget、產生 OpenGL Texture

| | 0 Comments| 15:59
Categories:

從去年開始,Heresy 就有在試著用 Qt 搭配 OpenVR,開發支援 SteamVR 的虛擬實境的程式。當時在寫的時候,一部分是參考 GitHub 上的「QVRViewer」這個專案(連結),也算是成功地把 Qt OpenGL(官網)的框架,成功地和 OpenVR 整合到一定程度了。

而接下來,則是希望可以在虛擬實境的環境裡面,顯示圖形介面的部分。這部分,則是參考了 OpenVR 的「helloworldoverlay」這個範例(連結);在它裡面的「openvroverlaycontroller.cpp」這個檔案(連結)就有把 Qt 的 Widget 繪製成 OpenGL Texture、並手動傳送滑鼠事件進去的操作方法。

首先,他這邊的概念,就是透過 Qt Graphics View Framework 的框架,來繪製 QWidget;而和一般使用 QGraphicsScene 時不同的地方,是這邊不會去使用 QGraphicsView 來做呈現,而是特別去設定要使用的 QPainter,來決定要畫到哪裡。

它的使用邏輯,基本上如下(這邊把 OpenVR overlay 的部分省略):

  1. 初始化(Init()

    在這邊由於他的 OpenGL 使用目的是拿來繪製貼圖,並沒有要直接畫出來,所以他不是直接使用 QOpenGLWindowQOpenGLWidget;也因此,這邊必須要自己去管理 OpenGL context 以及 surface。

    Init() 這個函式裡面,可以看到他自行建立了 QOpenGLContext 的物件、以及一個 QOffscreenSurface 物件,來做為繪製的目標。

    而為了讓圖形介面在有更動時會自動更新對應的 OpenGL Texture,所以這邊他透過 Qt signal – slot 的機制,讓 QGraphicsScene(這邊是m_pScene 這個物件)在有場景變動的時候(changed 這個 signal),就去呼叫自行定義的 OnScreenChanged 這個函式,來重新繪製。

  2. 設定要繪製的介面(SetWidget()

    接下來,在 SetWidget() 這個函式中,則是將要繪製的 QWidget 在移到原點後,放進 m_pScene 中。

    之後,則是根據 QWidget 的大小,產生對應大小的 QOpenGLFramebufferObject,來做為對應的繪製目標。

  3. 繪製(OnSenceChanged()

    在上面都設定好了之後,之後只要 m_pScene 裡面有變動、需要重新繪製的時候,就會觸發到 OnScreenChanged() 這個函式,進行繪製。

    而在這個函式裡面可以看到,它的操作方法基本上就是先呼叫 m_pOpenGLContextmakeCurrent() 函式,然後再 bind 之前產生的 QOpenGLFramebufferObjectm_pFbo)。

    之後,則是建立一個 QOpenGLPaintDevice 的物件 device、並指定他的大小等於 m_pFbo 的大小(也就是要繪製的 QWidget 的大小);然後,再建立一個 QPainter 的物件 painter、並告訴他要畫在 device 上。

    而在上面設定都完成後,接下來就是呼叫 m_pScenerender() 這個函式,並告訴他要使用 painter 來做繪製。如此一來,m_pScene 就會把場景的內容,都繪製在 m_pFbo 這個 QOpenGLFramebufferObject 的物件上了。

    繪製好之後,則是要先 release m_pFbo,然後就可以透過 m_pFbotexture() 這個函式,取得一個 OpenGL 的 texture id;而這個 texture 的內容,就是 QWidget 的內容了~

上面基本上就是整個繪製流程的基本概念了。

而實際在使用的時候,如果本身就是繼承 QOpenGLWindowQOpenGLWidget 來開發的話,那其實可以省略 QOpenGLContextQOffscreenSurface 的部分,使用既有的 OpenGL context 和 surface 來操作。

另外,在事件處理的部分,「helloworldoverlay」這個範例是透過固定時間去執行 OnTimeoutPumpEvents() 這個函式的方法,在裡面透過 OpenVR overlay 的提供的 API,來取得對應的滑鼠事件的;不過由於 Heresy 這邊並不打算使用 OpenVR 的 overlay 架構,所以很多東西可能就得自己做了。

但是,在這個函式中,還是有提供了如何設定 QGraphicsSceneMouseEvent、手動建立滑鼠事件給 QGraphicsScene 的示範;這點對要自己處理的人來說,也是有相當的參考價值的~


理論上,這樣就可以用 OpenGL 當 backend、來把 Qt Widget 畫到 OpenGL 的 texture 了。

不過,現實其實沒這麼美好…在 Heresy 這邊的狀況來說,這樣的寫法,對於ㄧ般的線條、圖形都沒有問題,但是碰到文字,就會整個爛掉了…

上面的截圖,就是透過 QOpenGLPaintDevice 繪製出來的一個 QPushButton,它裡面的文字是「Hello World」;但是這邊可以看到,雖然部分字母還可以看的出來,但是很多字母是直接爛掉、完全看不出是什麼鬼了…(上下顛倒是座標系統的問題)

研究了一陣子後,感覺上這似乎是 Qt5 很久以來都有的問題,也有人向官方回報過了(QTBUG-35000)…而這個問題似乎只會出現在 Android 和 Windows 平台上,但是從 2013 到現在,似乎都還沒有解掉…

最後,為了讓他能正常繪製文字,Heresy 後來只好捨棄 QOpenGLPaintDevice、而是先畫到 QImage 上,再自己去把它轉換成 OpenGL texture 了。

要修改的部分,主要就是先把建立 QOpenGLFramebufferObject 物件的地方,改成建立一個 QImage 的物件(要指定大小和格式);同時自己手動透過 OpenGL 的 texture 相關函式,建立對應的 OpenGL Texture(或者也可以使用對應的 QOpenGLTexture)。

這邊的程式可能會寫成下面的樣子:

m_qbaseImage = QImage(m_pWidget->width(), m_pWidget->height(), QImage::Format_RGBA8888);
initialTexture(m_qbaseImage);

其中,initialTexture() 這個函式,則是自己撰寫、針對 QImage 建立 OpenGL Texture 的函式。

而在要繪製的部分,則相對簡單,可以寫成:

QPainter qPainter(&m_qbaseImage);
m_Scene.render(&qPainter);
updateTexture(m_qbaseImage);

這邊就是用一般的 QPainter,指定他的繪製目標是 m_qbaseImage 這個影像;然後一樣去呼叫 m_Scenerender() 函式,進行繪製。

在繪製完成後,則是再呼叫 updateTexture() 這個函式,把 m_qbaseImage 的內容更新到 texture 內了。

而這樣繪製的結果,文字的顯示效果就是正確的了!


另外,上面的介面看起來,除了按鈕的立體效果似乎消失了以外,其他似乎都還算可以接受?

不過實際上,如果真的把 Widget 的大小拿來當作 frame buffer object/QPixmap 的大小的話,那其實可能會發生文字太小,或是放大後介面的像素點太大、或是模糊的問題(如下圖)。

要解決這樣的問題,比較直接的方法,就是在建立 QOpenGLFramebufferObject 的物件的時候,把大小放大;像上面的圖,就是 Heresy 就是把長寬都乘 4,這樣畫出來才會比較好看。


這篇先寫到這邊。由於 Heresy 也還在邊寫邊試,也還沒寫完,這邊主要是怕自己忘記,先做個筆記了。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *