Qt 3D Render Framegraph筆記

Qt 3D Render Framegraph筆記

はじめに

はじめまして、
去年から応用技術部に所属された新入社員の何(カ)と申します。

初めての投稿の今回は、案件で初めて触った開発環境Qtについて、特にQt3D Rendererについての内容を紹介させていただきます。

まずQtとは?

https://www.qt.io/

The Qt CompanyとQt Projectによって開発されているクロスプラットフォームアプリケーションフレームワークです。Qt3Dはその中で3Dのリアルタイムレンダリングを提供するモジュールとなります。

市場で一般的に扱われるリアルタイムレンダリングエンジン(リアルタイムだと大体ゲームエンジンのほうになる、UE4/Unityなど)に比べたら使用者数が少なく、高度の描画にはあまり向いてないかもしれませんが、開発のニーズに合わせて、基盤から構築より色々開発時間が短縮されるメリットもあります。

2020年年末リリースしたばかりのQt 6からはRHI(Rendering Hardware Interface)へ転換し、他のグラフィックスAPI(DirectX、Vulkan、 Metal)が対応できましたが、Qt 6以前では、OpenGLのみを使用するレンダラーとなります。

今回の紹介はQt 5ベースとなります。基本の使い方としては、大体UnityのEntity Component Systemのように、ツリーからEntityとComponentの組み合わせでオブジェクトの描画を決めます。何を描画する場合でも、UnityのようにEdtiorからObjectを追加し、その部分をc++で簡易に必要なcomponentを登録して描画できるので、Unityに慣れている方は初めて触っても、大体すぐ描画できると思います。

ただこの前に、Renderer自体の設定はどうなるのか、特に元々Editorもないので各設定はどうすればよいのか、例えばDefered Render/Multiple Viewports/pre-depth passなど高度なシーンを描画するためには、あらかじめQt3DのFrameGraphの各設定になれる必要があります。

この目的でQtドキュメントの翻訳、内容抜粋とネタ紹介での記事を書きます。オブジェクトごとの描画前に、Renderer自体の構築に先に触れておきましょう。

ドキュメントの原文は以下のリンク:

https://doc.qt.io/qt-5/qt3drender-framegraph.html

原文は基本QMLの紹介となるので、C++より省略されるものと違う部分が結構あり、C++の構成から紹介すると、構造的には原文のQML階層では理解しやすいと思います。

本投稿は以下の構成でお話しします。

  • 基本ルール紹介
  • ベースのSimple Forward Renderer
  • 複数ウインドウのMultiple Viewport Forward Renderer
  • ネタとなるQFrameGraphNode Class

また、構造や機能の使い方は以下の公式gitで確認できます。

https://github.com/qt/qt3d/tree/dev/tests/manual

基本ルール

まず基礎のwindowですが、c++側ではQt3DWindowクラス、もしくはこちらから派生したクラスをメインウインドウにします。内部にsceneRootを設定でき、ツリー構造でベースノードとなります。
activeFrameGraphがシーンの有効FrameGraphとなり、デフォルト設定以外にも自分でQt3DRender::QFrameGraphNodeからベースのRendererを作ってQt3DWindowのactiveFrameGraphに指定することもできます。

//QMLの基本設定:
Entity {
  id: sceneRoot
  components: RenderSettings {
    activeFrameGraph: ... // FrameGraph tree
  }
} 

//C++の基本設定:
Qt3DExtras::Qt3DWindow view;
Qt3DCore::QEntity *rootEntity = new Qt3DCore::QEntity();
view.setRootEntity(rootEntity);//後のカメラ設定など
Qt3DRender::QCamera *basicCamera = view.camera();
基本設定

Framegraphの流れ:

・rootNodeに登録するFrameGraphNode(Entityノードではなく、あくまでもFrameGraphNodeを指す)、depth-first searchという探索で、最初の木から子のないノードまでという探索、という概念でどういう描画ステートを作るのか、その構造が大事。

・Qt3DRender::QFrameGraphNode継承からのQLayer、QRenderSurfaceSelector、QViewPortなど、設定の親子関係から探索でひとつまとめる描画ステートをRenderViewと呼ぶ。

・複数RenderViewそれぞれに対応するEntityをまとめてRenderCommandsを作り、最終OpenGLへ送る。

Simple Forward Renderer

Forward Rendererでは、ひとつのオブジェクトを完全描画してから次のオブジェクト描画へ進む仕組みのRendererです。レンダリングを触る方々も一番最初に触る構造で、相対的にもシンプルになります。

//例1
Viewport {
  normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
  property alias camera: cameraSelector.camera 
  ClearBuffers {
    buffers: ClearBuffers.ColorDepthBuffer 
    CameraSelector {
     id: cameraSelector
    }
  }
}
QML構成

このQMLのツリー構造から各Nodeの親子関係がわかりやすいと思います。

それに同じ出力結果で違う構造でも問題なく、例えば

//例2
Viewport {
  normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
  property alias camera: cameraSelector.camera 
  CameraSelector {
    id: cameraSelector 
    ClearBuffers {
     buffers: ClearBuffers.ColorDepthBuffer
    }
  }
} 
//例3
CameraSelector {
  Viewport {
    normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
     ClearBuffers {
      buffers: ClearBuffers.ColorDepthBuffer
    }
  }
}
QML構成


変更が少ないQt3DRender::QFrameGraphNodeを親に設定すべきです。RenderState変更の減少より効率的な描画を求められます。

C++例:

Qt3DRender::QViewport* viewport = new Qt3DRender::QViewport(root);
Qt3DRender::QCameraSelector* cameraSelector = new Qt3DRender::QCameraSelector(viewport);
Qt3DRender::QCamera* camera = new Qt3DRender::QCamera(cameraSelector);

上記のように初期化の親node変更などもいけます。

簡単な構造では、それほど親子関係を気にしなくても理想の出力ができますが、複雑になるとこの関係に注意が必要です。

次から複雑な設定へ移行します。

A Multi Viewport FrameGraph

よくある一画面に4つのViewportの階層構成は以下のようにとなります。

Viewport {
  id: mainViewportnormalizedRect: Qt.rect(0, 0, 1, 1)
  property alias Camera: cameraSelectorTopLeftViewport.camera
  property alias Camera: cameraSelectorTopRightViewport.camera
  property alias Camera: cameraSelectorBottomLeftViewport.camera
  property alias Camera: cameraSelectorBottomRightViewport.camera
  ClearBuffers {
    buffers: ClearBuffers.ColorDepthBuffer
  }
   Viewport {
    id: topLeftViewport
    normalizedRect: Qt.rect(0, 0, 0.5, 0.5)
    CameraSelector {
     id: cameraSelectorTopLeftViewport
    }
  } 
  //上のViewportのように残りのviewportを追加
  .......
}
Qt公式サイト(https://doc.qt.io/qt-5/qt3drender-framegraph.html)より転載
Qt公式サイト(https://doc.qt.io/qt-5/qt3drender-framegraph.html)より転載

RenderViewを確認してみましょう。

この状態で上記のRenderViewを作ります。

  • RenderView (1)
    • 全画面Viewportを指定
    • 色と深度バッファをクリア
  • RenderView (2)
    • 全画面Viewportを指定
    • サブViewport(左上)
    • カメラセレクトを指定
  • RenderView (3)//345は上記(2)のように

順位に注意しましょう。最初にClearBufferが発生し、Viewport間では使バッファクリアが発生しません。ColorのClearBufferを中へ挟むと、前のViewport画面がクリアされます。

Qt公式サイト(https://doc.qt.io/qt-5/qt3drender-framegraph.html)より転載

RenderViewごとに対する複数のRenderCommandを並列に生成することが可能で、RenderViewのsubmit順位が正しければ出力順が正確になります。

もっと複雑な設定で、viewportごとにlayerを作り、そのlayerの間ではclear bufferを用意し、レイヤごとにそれぞれ違う表現を出すことができます(例えば2D/3Dのカメラ投影を分けるとか)。

C++例:

auto renderSurfaceSelector = new Qt3DRender::QRenderSurfaceSelector(root);
 //ここからどんどんレイヤを追加する 

//レイヤでEntityを分ける
auto objectsLayerFilter = new Qt3DRender::QLayerFilter(renderSurfaceSelector);
auto objectLayer = new Qt3DRender::QLayer(objectsLayerFilter);
objectsLayerFilter->addLayer(objectLayer);

 //レイヤに対するViewportとカメラの設定
auto viewport = new Qt3DRender::QViewport(objectLayer);
auto cameraSelector = new Qt3DRender::QCameraSelector(viewport);
auto camera = new Qt3DRender::QCamera(cameraSelector);cameraSelector->setCamera(camera); 

//レイヤへ移る時のバッファクリアなど
auto clearBuffers = new Qt3DRender::QClearBuffers(camera);
clearBuffers->setBuffers(Qt3DRender::QClearBuffers::DepthBuffer);

これでlayer作成の間に、バッファクリアするのか、Blend機能を追加するのか、深度テストをするのかなど、色々状態を決められます。

またDeferredRenderの紹介も原文にあります。qmlとcppの紹介が結構長いため、興味がある方は原文CPPのSampleを参考してください。

描画に関連する一部Qtクラスの紹介と小ネタ、注意点:

https://doc.qt.io/qt-5/qt3drender-qframegraphnode.html

QFrameGraphNodeから継承のすべてのクラスは上記から確認できます。

実際の開発中に使ったQt3DRenderのクラスの内容と注意点を、一部こちらへまとめます。

Qt3DRender::QSortPolicy

  • DrawCallに影響するクラス。シーン内のEntity描画順をソートする(描画ステート変更コスト順、Z値順など)。
  • 公式ドキュメントではQSortPolicyが設定されない場合、entity hierarchyの順位からの呼び出しとなる。これが誤解されがちだと思う。そもそもQLayer指定などよりEntityが分けられ、レイヤごとから呼び出しとなり、同じレイヤとしても必ず登録順と同じではない。動的にソートされる可能性があるので、もしオブジェクト登録などを確保したい場合、ニーズに合わせてQSortPolicyを使って、entityに何か差分をもたらしたほうが無難。

Qt3DRender::QLayerFilterとQt3DRender::QLayer

  • 名前的には誤解される可能性もあるので、LayerFilterとLayerは一対多の関係で、LayerFilterに複数のLayerを登録できる。LayerはEntityに対する所属となる。
  • LayerFilterが先の指定で、qtがLayerFilter実例からグループを分ける。同じQLayerFilterでの複数Layerの順位がどうなるのかドキュメントに特に言及されておらず、それぞれを確実に分けるためQLayerFilterとQLayerが1対1の関係で、複数作成するのが無難。

Qt3DRender::QRenderPassFilter

  • 上記と同じく、qt大体Filter付きのクラスが上位関係と考えられたほうがよいが、QRenderPassとは親子関係でもない。
  • 設定された場合、QRenderPassとのQFilterKeyがマッチした場合のみの描画パスが実行される。

Qt3DRender::QRenderStateSet

  • RenderPassではマテリアルに対する各パラメータ、テクスチャの設定の代わりに、QRenderStateSetが全体の設定となる(基本レイヤごとの設定となる)。
  • こちらにグラフィックパイプラインの設定、cullmode/alphatest/depthtest/blendなどを、関数setRenderState()で追加したらひとつの描画ステートとなる。

まとめ:

基本的なFrameGraphとNode構成、シンプルなことから複雑なことまで、使い手となるクラスを色々紹介し、内製の各機能をツールという考えで、デフォルトの描画設定に拘らず、ニーズに合わせて使ったら、色々特殊な描画が実現できます。

もっと特殊な実装例を紹介しようと思っていたのですが、原文の抜粋と紹介だけでも結構長くなってしまうので、今回はここまでにします。また機会があればQt3Dだけではなく、他のモジュール機能とネタを紹介したいと思います。ここまでお読みいただきありがとうございます。

この記事をシェアする