2013年12月14日土曜日

C++ Advent Calendar 2013-12-14: Qt5/QML2でteapot

この記事は、C++ Advent Calendar 2013の参加記事です。

C++のクロスプラットフォームでパワフルなGUI toolkitとしてQtがあります。去年はまだまだ登場したばかりだった新型のQt5も、今年は徐々に普及しだしている様に思います。

さて、そのQtにはQMLというECMA-262をベースにしたスクリプト環境があります。JavaScriptにとてもよく似た記述言語でGUIのデザインとその動作の両方をとても柔軟に扱え、とても便利です。

QMLでは標準で用意された「アイテム」(≈QMLアプリを組み立てる部品)を使いレイアウトや機能実装を行うだけでもとても重宝するのですが、もちろんこの「アイテム」はC++で実装されていて実行環境には共有ライブラリー(.soとか.dllとか)でインストールされており、標準のものだけではなく独自にC++で作成したライブラリーを使う事もできます。

今回は、Qt5で刷新されたQML2の描画系を使いOpenGLでお馴染みのteapotを描画するアイテムを作成してみようと思います。


今回のリポジトリー: https://github.com/usagi/qml2teapot

[1. アイテムを提供するソースコードの生成]

一般に、QML2の独自実装のアイテムはQQuickItemから派生させて作ります。

gl_teapot.h : https://github.com/usagi/qml2teapot/blob/master/gl_teapot.h

#pragma once
#include <QQuickItem>
#include <QOpenGLFramebufferObject>

class gl_teapot : public QQuickItem
{
  Q_OBJECT
  Q_DISABLE_COPY(gl_teapot)
    
public:
  gl_teapot(QQuickItem* parent = nullptr);
  ~gl_teapot();
  
protected:
  virtual void timerEvent(QTimerEvent*);
  virtual QSGNode* updatePaintNode(QSGNode*, UpdatePaintNodeData*);
  
private:
  QOpenGLFramebufferObject* fbo;
  int timer_id;
  uint64_t timer_counter;
  
  static constexpr int timer_interval = 16;
  static constexpr float pi = 3.14159265359f;
};

QML_DECLARE_TYPE(gl_teapot)

gl_teapot.cxx : https://github.com/usagi/qml2teapot/blob/master/gl_teapot.cxx

最も基礎的な実装では含めない今回の為の実装について触れていきます。なお、最も基礎的なQML2アイテムのC++コードはQtCreatorで"QML 2 Extension Plugin"を雛形に作成する事もできます。

今回はQt5/QML2からの描画で使われるようになったOpenGLのFBO直接teapotの描画を行います。

そのために、コンストラクターで setFlag(ItemHasContents) を行っています。このフラグセットを行わないと描画要素は無いものとして扱われてしまいます。

また、描画内容をアニメーション(表示を更新)する為に今回はQtの簡単なタイマーの仕組みも動かしています。コンストラクターのstartTimerによりimer_interval間隔でtimerEventメンバー関数を呼び出してくれる仕組みです。timerEventでは経過時間をtimer_counterに加算しつつ、基底クラスのupdateメンバー関数を呼び出しています。これで一定時間間隔でアニメーションするようになります。

続いて、updatePaintNodeメンバー関数を記述します。FBOに直接描画したい要求がある場合にはこの関数を実装する事でQt5/QML2のアイテムの描画処理に介入します。

QSGNode* gl_teapot::updatePaintNode(QSGNode* node_old, UpdatePaintNodeData* data)
{

と、言っても直接すぐに「はいどうぞ、このFBOを抽象化したオブジェクトににゃんにゃんしてね」とは行かず、ひと手間必要です。

    glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);
    glPushAttrib(GL_ALL_ATTRIB_BITS);
    glMatrixMode(GL_TEXTURE);
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
 
    glShadeModel(GL_FLAT);
    glDisable(GL_CULL_FACE);
    glDisable(GL_LIGHTING);
    glDisable(GL_STENCIL_TEST);
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

ここは他の描画とOpenGLの状態が混乱しない様に独自にFBOを使うOpenGLコードを記述する前の状態を保持して、一部の描画オプションを標準的な状態に設定しているだけです。後半にここで退避した状態を復帰させている部分があります。

    fbo = new QOpenGLFramebufferObject(width(), height());

Qt5ではFBOをQOpenGLFrambufferObjectにより扱います。これによりOpenGLの生のC APIに触れる必要はありません。ちなみに、第3パラメーターに GLenum target = GL_TEXTURE_2D が省略されていたりします。

    node->setTexture(window()->createTextureFromId(fbo->texture(), fbo->size()));

基底クラスとした描画用のnodeに対して先に生成したFBOをQQuickItemのwindowメンバー関数からテクスチャーとしてセットします。

    node->setRect(boundingRect());

nodeの矩形領域をセットします。

    QMatrix4x4 flipY;
    flipY.translate(0.5 * width(), 0.5 * height());
    flipY.scale(1.0, -1.0);
    flipY.translate(0.5 * -width(),0.5 * -height());
    data->transformNode->setMatrix(flipY);

OpenGLの描画の座標軸はQMLの座標軸とはY軸に反転しているので、反転して描画させる事で正しく描画できます。

あとは、

    fbo->bind();
      ...
    fbo->releas();

この間の ... に描画コードを記述すれば、よしなにOpenGLのレンダーターゲットがFBOのテクスチャーに描画してくれ、Qt5の描画エンジンがこれを最終的に画面に描画してくれます。

ここで、 teapot を描画している部分は、上記の ... の後半で、

      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      gluPerspective(30., GLdouble(width()) / GLdouble(height()), 0.1, 1000.);
   
      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
      float ex = 5.f * std::sin(.0005f * timer_counter);
      float ey = 3.f * std::sin(.0005f * timer_counter);
      float ez = 5.f * std::cos(.0005f * timer_counter);
      gluLookAt( ex, ey, ez
               , 0., 0., 0.
               , 0., 1., 0.
               );

      glEnable(GL_LIGHT0);
      glEnable(GL_LIGHTING);
   
      glViewport(0, 0, width(), height());
   
      glutWireTeapot(0.8);

の部分だけです。そのままOpenGLの描画コードです。今回は GLSL を記述したりせず、シンプルに、GLUとGLUTを使い teapot を描画しました。何やら計算させているのはカメラの視点をアニメーションさせる為のコードです。

ちなみに、GLSLを使いたい場合はこの部分にシェーダーオブジェクトの取り扱いを記述して glDrawArrays とかすれば良いのですが、その際もQt5では、 QOpenGLBuffer 、 QOpenGLShaderProgram など OpenGLの生の C API をラップした素敵なクラスが用意されています。

さて、では、 ... 部分の前半は何をしているかというと、

      QRectF drawRect(0, 0, width(), height());

      QOpenGLPaintDevice device(width(), height());
      QPainter painter;
   
      painter.begin(&device);
   
      painter.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing);
      painter.fillRect(drawRect, QColor::fromHslF(std::cos(.001f * timer_counter) * .5f + .5f, 1.f, .9f));
      painter.drawTiledPixmap(drawRect, QPixmap(":/qt-project.org/qmessagebox/images/qtlogo-64.png"));
   
      painter.setPen(QPen(Qt::black, 0));
      QFont font;
      font.setPointSize(28);
      painter.setFont(font);
      painter.drawText(drawRect, "QML2\nGLTeapot", QTextOption(Qt::AlignRight | Qt::AlignBottom));
   
      painter.end();

QPainter を用いてFBOに teapot 以外の背景を描いています。

2D描画、今回の例のように背景的なものを簡易的に描画処理したい場合にはOpenGLのコードとしてビルボードでうんぬん・・・しなくてもQPainterで描いてしまう事もできます。

今回はFBOを生成して初期状態として真っ黒のテクスチャーがあり、そこにQPainterで背景をいっぱいに描画しているので、OpenGLのコードでは glClear とか呼ばずに、QPainterが背景を描画した後のテクスチャーにそのまま重ね描きで teapot を描画しています。

ちなみに、コメントアウトしてありますが、

  //fbo->toImage().save("/tmp/hoge.png");

この様にするとQOpenGLFramebufferObjectのテクスチャーを簡単にファイルに書き出す事もできます。toImage()で取得しているオブジェクトはQImageなので、扱いやすいですね。

[2. QMLの仕組みで有効なアイテム名で登録できるようにする]

gl_teapot_plugin.h : https://github.com/usagi/qml2teapot/blob/master/gl_teapot_plugin.h

#pragma once
#include <QQmlExtensionPlugin>

class gl_teapot_plugin : public QQmlExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
 
public:
    void registerTypes(const char* uri);
};

gl_teapot_plugin.cxx : https://github.com/usagi/qml2teapot/blob/master/gl_teapot_plugin.cxx

#include "gl_teapot_plugin.h"
#include "gl_teapot.h"

#include <qqml.h>

void gl_teapot_plugin::registerTypes(const char *uri)
{
    // @uri net.WonderRabbitProject.tmp
    qmlRegisterType<gl_teapot>(uri, 1, 0, "GLTeapot");
}

qmldir : https://github.com/usagi/qml2teapot/blob/master/qmldir

module net.WonderRabbitProject.tmp
plugin gl_teapot

今回の様にQML2アイテムとして提供したいコードがsnake_caseで記述されている場合には注意が必要で、qmlRegisterTypeによりQML2のアイテムとしては"GLTeapot"という名称でgl_teapot型を登録しています。QML2のアイテム名は大文字始まりのCamelCaseで命名する決まりで、違反すると正常に動作しません。ちなみに小文字始まりのcamelCaseはQMLではプロパティーとして認識されるのでやはり動作しません。加えて、ハイフンやアンダースコアも使えません。

なお、ここまでで既にライブラリーのソースコードの準備としては完了していて、自分でコンパイラーを叩けば共有ライブラリーの生成も可能になっています。

[3. test.qml を用意する]

test.qml : https://github.com/usagi/qml2teapot/blob/master/test.qml

import QtQuick 2.0
import net.WonderRabbitProject.tmp 1.0

Row
{
  focus: true
  Keys.onEscapePressed: Qt.quit()

  GLTeapot
  {
    width: 320
    height: 240
  }
}

動作確認用のサンプルとしてESCを押すと終了する 320x240 の GLTeapot アイテムを扱うだけのQMLを用意しました。

[4. cmake ]

QtCreator/qmakeでビルドしても良いのですが、今回は CMakeLists.txt を記述して cmake します。

CMakeLists.txt : https://github.com/usagi/qml2teapot/blob/master/CMakeLists.txt

今回のポイントは、

set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)



find_package(Qt5Qml REQUIRED)
find_package(Qt5Quick REQUIRED)



qt5_use_modules(gl_teapot Qml Quick)

この辺りです。cmakeはQt5に対応、というかQt5の開発用パッケージがcmakeに対応してくれているので、上記の様な方法で qmake で .pro を書くのと同等以上にパワフルにビルドを記述する事ができます。

なお、 qmldir やビルドで生成する .so はQtのURIの仕組みに沿ったディレクトリーに設置してあげる必要があります。その仕掛けが CMakeLists.txt の後半に記述してあります。それについは 「cmakeで任意のコマンドの実行結果を変数に取得して使う方法」や「cmakeでQML 2 Extension Pluginをビルドする方法」も参考にどうぞ。

[5. build & run]

cmake/ninjaでビルドする例:

mkdir ../qml2teapot.build
cd ../qml2teapot.build
cmake -G Ninja ../qml2teapot
ninja

.qmlの実行はQt5/QML2からは qmlscene を使います。

qmlscene test.qml

もし、正常に動作しない場合には、環境変数 QML2_IMPORT_PATH を確認して見て下さい。それについては「Qt Quick 2.0: how to use the original QML2 plugin on a .qml / qmlscene」を参考にどうぞ。






[xxx.  ????]

さて、かくかくしかじか、今年も C++ Advent Calendar に参加させて頂きました。事前には Qt5/QML+Ogre+Bullet で簡単なゲームでも作って展示しようかと思っていたのですが、優先度の高いあれこれや体調不良やとつぜんの腓返りや今年初のまとまった雪(積雪30cmくらい)などで、気付いてみれば Qt5/QML2 で Ogre はサンプルを稼働できた程度、 Bullet の組み込みはスケジュール的に無理な状況になってしまいました(;´∀`)

そんなわけで、今回は Qt5/QML2 で OpenGL による独自描画系を持ったアイテムを提供する方法について、古典的な glu と glut を用いた teapot のサンプルを作成し、紹介してみました。案外、日本語、英語問わず、Qt5のFBO周りの制御については解説や質疑応答がまだまだ盛んとは言えない様です。似たような事を遊んでみたい方の参考になったら嬉しいな。

・・・でもね、実はこのコード、C++ Advent Calendarの公開日になって急遽用意したバギーな状態だったりもします。 glutWireTeapot だとよほどよく見ないとおかしな点には気付かないと思いますが、 glutSolidTeapot にすると・・・。

(;´∀`)…のちほど、バグ修正加筆記事かきます。ごめんなさいー。

(追記: 2013-12-15)
バグ修正加筆記事書きました→「Qt5/QML2でteapot: まともにglutSolidTeapotできるよう修正しました

参考:
  1. Qt Project: Docs>Qt 5.1>qtquick>QQuickItem Class | QtQuick 5.1#Custom Scene Graph Items
  2. StackOverflow: Rendering custom opengl in qt5's qtquick 2.0
  3. <peppe>'s weblog: Using FBOs instead of pBuffers in Qt 5
  4. 床井研究室: フレームバッファオブジェクトの使い方あげいん
  5. HORDE 3D: Tutorial - Setup Horde with Qt5 & QtQuick 2.1
  6. www.slis.tsukuba.ac.jp/~fujisawa.makoto.fu: OpenGL -FBO
  7. github: mltframework / shotcut / src / glwidget.cpp
  8. habrahabr.ru: Использование кадрового буфера в Qt 5
  9. OpenGL: Framebuffer Object Examples
  10. WisdomSoft(旧): システムとAPI / OpenGL入門 / 光と反射

0 件のコメント:

コメントを投稿