Qt 3D レンダー・フレームグラフ

Qt 3D Renderアスペクトは、レンダリングアルゴリズムを完全にデータ駆動型にすることを可能にする。制御するデータ構造はフレームグラフとして知られています。Qt 3D ECS (Entity Component System)が、エンティティやコンポーネントのツリーからシーンを構築することで、いわゆるScenegraphを定義できるのと同様に、フレームグラフもツリー構造ですが、異なる目的で使用されます。すなわち、シーンがどのようにレンダリングされるかをコントロールすることです。

1つのフレームをレンダリングする間に、3Dレンダラーはおそらく何度もステートを変更します。これらの状態変更の回数と性質は、シーン内にどのマテリアル(シェーダー、メッシュジオメトリ、テクスチャ、ユニフォーム変数)があるかだけでなく、どの高レベルレンダリングスキームを使用しているかによっても異なります。

たとえば、従来の単純なフォワードレンダリングスキームを使用するのと、ディファードレンダリングアプローチを使用するのとでは、大きく異なります。反射、シャドウ、複数のビューポート、および早期の Z フィルパスなどのその他の機能はすべて、レンダラーがフレームの過程で設定する必要があるステートと、それらのステートの変更が発生する必要があるタイミングを変更します。

比較対象として、Qt Quick 2 シーンを描画するQt Quick 2 scenegraph レンダラーは、プリミティブのバッチ処理や、不透明アイテムのレンダリングの後に透明アイテムのレンダリングを行うように C++ でハードワイヤされています。Qt Quick 2の場合、これですべての要件をカバーできるので、まったく問題ありません。上に挙げた例のいくつかを見ればわかるように、利用可能なレンダリング方法が多数あることを考えると、このようなハードワイヤードのレンダラーは、一般的な3Dシーンでは柔軟性に欠ける可能性が高い。あるいは、このようなケースをすべてカバーできるほど柔軟なレンダラーができたとしても、汎用的すぎるためにパフォーマンスが低下する可能性が高い。さらに悪いことに、より多くのレンダリング手法が常に研究されている。そのため、柔軟で拡張性があり、かつ使用と保守が簡単なアプローチが必要だった。フレームグラフの登場です!

フレームグラフの各ノードは、レンダラーがシーンをレンダリングするために使用するコンフィギュレーションの一部を定義します。フレームグラフツリー内のノードの位置によって、そのノードをルートとするサブツリーが、いつ、どこで、レンダリングパイプラインのアクティブなコンフィギュレーションになるかが決まります。後で説明するように、レンダラーは、フレームの各ポイントでレンダリングアルゴリズムに必要なステートを構築するために、このツリーをトラバースします。

画面上に単純な立方体をレンダリングしたいだけなら、明らかにこれはやりすぎだと思うかもしれない。しかし、少し複雑なシーンを作り始めると、これは便利です。よくあるケースのために、Qt 3D 、すぐに使えるフレームグラフの例がいくつか用意されています。

いくつかの例と結果のフレームグラフを紹介することで、フレームグラフのコンセプトの柔軟性を示します。

エンティティ(Entity)とコンポーネント(Components)で構成されるシーングラフ(Scenegraph)とは異なり、フレームグラフ(Framegraph)は、Qt3DRender::QFrameGraphNode のサブクラスであるネストされたノード(Node)のみで構成されることに注意してください。これは、フレームグラフ(Framegraph)のノードは、仮想世界のシミュレートされたオブジェクトではなく、サポート情報であるためです。

最初のシンプルなフレームグラフを構築する方法をすぐに説明しますが、その前に利用可能なフレームグラフノードを紹介します。また、Scenegraphのツリーと同様に、QMLとC++のAPIは1対1の互換性があります。読みやすさと簡潔さのために、この記事ではQML APIを選択しました。

フレームグラフの優れた点は、これらの単純なノードタイプを組み合わせることで、毛むくじゃらの低レベルのC/C++レンダリングコードに一切触れることなく、特定のニーズに合わせてレンダラーを設定できることです。

フレームグラフのルール

正しく機能するフレームグラフツリーを構築するためには、フレームグラフツリーのトラバース方法と、Qt 3D レンダラへのフィード方法について、いくつかのルールを知っておく必要があります。

フレームグラフの設定

Qt 3D FrameGraphツリーは、QRenderSettingsコンポーネントのactiveFrameGraphプロパティに割り当てられなければなりません。これが、レンダラーのアクティブフレームグラフになります。もちろん、これはQMLプロパティバインディングなので、アクティブフレームグラフ(またはその一部)は、実行時にその場で変更することができます。例えば、屋内と屋外のシーンで異なるレンダリングアプローチを使用したい場合や、何らかの特殊効果を有効または無効にしたい場合などです。

Entity {
    id: sceneRoot
    components: RenderSettings {
         activeFrameGraph: ... // FrameGraph tree
    }
}

注意: activeFrameGraphは、QMLのFrameGraphコンポーネントのデフォルトプロパティです。

Entity {
    id: sceneRoot
    components: RenderSettings {
         ... // FrameGraph tree
    }
}

フレームグラフの使用方法

  • Qt 3D (英語)レンダラーは、フレームグラフツリーを深さ方向から順に走査します。トラバーサルは深さ優先なので、ノードを定義する順番が重要であることに注意してください。
  • レンダラーがフレームグラフのリーフノードに到達すると、リーフノードからルートノードまでのパスで指定されたすべての状態を収集します。これにより、フレームのセクションをレンダリングするために使用されるステートが定義されます。Qt 3D の内部構造に興味があれば、このステートのコレクションをRenderViewと呼びます。
  • RenderView に含まれるコンフィギュレーションが与えられると、レンダラーはレンダリングされるシーングラフの全ての Entity を集め、それらからRenderCommands のセットを構築し、RenderView と関連付ける。
  • RenderViewとRenderCommandsのセットは、OpenGLに提出するために渡される。
  • これをフレームグラフの各リーフノードで繰り返すと、フレームが完成し、レンダラーがQOpenGLContext::swapBuffers ()を呼び出してフレームを表示します。

フレームグラフは、Qt 3D レンダラーを設定するためのデータ駆動型メソッドです。データ駆動型であるため、実行時に設定を変更することができ、C++以外の開発者やデザイナーがフレームの構造を変更したり、何千行もの定型コードを記述することなく新しいレンダリングアプローチを試したりすることができます。

フレームグラフの例

フレーム・グラフ・ツリーを記述する際に守るべきルールがわかったところで、いくつかの例を見て、それらを分解します。

単純なフォワード・レンダラー

フォワード・レンダリングとは、OpenGLを伝統的な方法で使用し、バックバッファに一度に1つのオブジェクトをシェーディングしながら直接レンダリングすることです。これは、中間Gバッファにレンダリングするディファードレンダリングとは対照的です。以下は、順方向レンダリングに使用できる単純なFrameGraphです:

Viewport {
     normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
     property alias camera: cameraSelector.camera

     ClearBuffers {
          buffers: ClearBuffers.ColorDepthBuffer

          CameraSelector {
               id: cameraSelector
          }
     }
}

見ての通り、このツリーは1つのリーフを持ち、下図に示すように合計3つのノードで構成されています。

上で定義したルールを使用すると、このFrameGraphツリーから、次のような構成のRenderViewが1つ生成されます:

  • リーフノード -> RenderView
    • 画面全体を埋めるビューポート(ネストされたビューポートを簡単にサポートするために、正規化された座標を使用する)
    • カラーバッファとデプスバッファはクリアされるように設定されています。
    • 露出されたカメラプロパティで指定されたカメラ

複数の異なるFrameGraphツリーが、同じレンダリング結果を生成できます。リーフからルートまで収集される状態が同じである限り、結果も同じになります。その結果、リーフノードの数が少なくなり、全体としてRenderViewの数が少なくなります。

Viewport {
     normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
     property alias camera: cameraSelector.camera

     CameraSelector {
          id: cameraSelector

          ClearBuffers {
               buffers: ClearBuffers.ColorDepthBuffer
          }
     }
}
CameraSelector {
      Viewport {
           normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)

           ClearBuffers {
                buffers: ClearBuffers.ColorDepthBuffer
           }
      }
}

マルチビューポートのフレームグラフ

ウィンドウの4つの象限に4つの仮想カメラの視点からScenegraphをレンダリングする、少し複雑な例に移りましょう。これは、3D CAD やモデリングツールの一般的な構成で、カーレースゲームや CCTV カメラディスプレイのバックミラーをレンダリングするのに役立ちます。

Viewport {
     id: mainViewport
     normalizedRect: 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 {
          id: topRightViewport
          normalizedRect: Qt.rect(0.5, 0, 0.5, 0.5)
          CameraSelector { id: cameraSelectorTopRightViewport }
     }

     Viewport {
          id: bottomLeftViewport
          normalizedRect: Qt.rect(0, 0.5, 0.5, 0.5)
          CameraSelector { id: cameraSelectorBottomLeftViewport }
     }

     Viewport {
          id: bottomRightViewport
          normalizedRect: Qt.rect(0.5, 0.5, 0.5, 0.5)
          CameraSelector { id: cameraSelectorBottomRightViewport }
     }
}

このツリーはもう少し複雑で、葉が5枚あります。先ほどと同じルールに従って、FrameGraphから5つのRenderViewオブジェクトを構築します。以下の図は、最初の2つのRenderViewの構築を示しています。残りのRenderViewは、他のサブツリーがあるだけで、2番目の図と非常に似ています。

作成されたRenderViewは以下の通りです:

  • RenderView (1)
    • フルスクリーンビューポートを定義
    • カラーバッファとデプスバッファがクリアされるように設定されている
  • レンダービュー(2)
    • フルスクリーン・ビューポートが定義されている
    • 定義されたサブビューポート(レンダリングビューポートは親ビューポートに対して相対的に拡大縮小される)
    • CameraSelector 指定された
  • レンダービュー (3)
    • 定義されたフルスクリーン・ビューポート
    • 定義されたサブビューポート(レンダリングビューポートは親ビューポートに対して相対的に拡大縮小される)
    • CameraSelector 指定
  • レンダービュー (4)
    • 定義されたフルスクリーン・ビューポート
    • 定義されたサブビューポート(レンダリングビューポートは親ビューポートに対して相対的に拡大縮小される)
    • CameraSelector 指定
  • レンダービュー (5)
    • 定義されたフルスクリーン・ビューポート
    • 定義されたサブビューポート(レンダリングビューポートは親ビューポートに対して相対的に拡大縮小される)
    • CameraSelector 指定

ただし、この場合は順番が重要です。もし、ClearBuffers ノードが最初ではなく最後になると、注意深くレンダリングされた直後にすべてがクリアされてしまうという単純な理由で、黒い画面になってしまいます。同じような理由で、FrameGraphのルートとして使用することはできません。それは、各ビューポートの画面全体をクリアする呼び出しになるからです。

FrameGraphの宣言順序は重要ですが、Qt 3D 、RenderViewの状態が有効である間に送信されるRenderCommandのセットを生成する目的で、各RenderViewが他のRenderViewから独立しているため、各RenderViewを並列に処理することができます。

Qt 3D は、利用可能なコアの数に応じて自然にスケールアップする並列性へのタスクベースのアプローチを使用しています。これは、前の例の次の図に示されています。

RenderViewのRenderCommandは多くのコアで並列に生成することができ、専用のOpenGLサブミッションスレッドで正しい順序でRenderViewをサブミットするように注意しさえすれば、結果のシーンは正しくレンダリングされます。

ディファードレンダラー

レンダリングに関して言えば、ディファードレンダリングはフォワードレンダリングとはレンダラー構成が異なります。各メッシュを描画し、シェーダー効果を適用してシェーディングする代わりに、ディファードレンダリングは2レンダーパス方式を採用します。

まず、シーン内のすべてのメッシュが同じシェーダを使って描画され、通常、各フラグメントに対して少なくとも4つの値が出力されます:

  • ワールド法線ベクトル
  • ワールド法線ベクトル
  • 深度
  • ワールド位置ベクトル

これらの値はそれぞれテクスチャに保存されます。法線、色、深度、位置のテクスチャは、Gバッファと呼ばれるものを形成する。最初のパスでは画面上には何も描画されず、後で使用できるようにGバッファに描画されます。

すべてのメッシュが描画されると、Gバッファは現在カメラから見えるすべてのメッシュで満たされます。2番目のレンダーパスは、Gバッファのテクスチャから法線、色、位置の値を読み取り、フルスクリーン・クアッドに色を出力することによって、最終的なカラーシェーディングを施したシーンをバックバッファにレンダリングするために使用されます。

このテクニックの利点は、複雑なエフェクトに必要な重いコンピューティングパワーが、2回目のパスの間、実際にカメラで見られている要素にのみ使用されることです。すべてのメッシュはシンプルなシェーダーで描画されるため、最初のパスでは処理能力はそれほどかかりません。したがって、遅延レンダリングは、シェーディングとライティングをシーン内のオブジェクトの数から切り離し、代わりにスクリーン(およびGバッファ)の解像度に結びつけます。これは、追加のGPUメモリ使用量を犠牲にして、大量のダイナミックライトを使用できるため、多くのゲームで使用されているテクニックです。

Viewport {
    id: root
    normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)

    property GBuffer gBuffer
    property alias camera: sceneCameraSelector.camera
    property alias sceneLayer: sceneLayerFilter.layers
    property alias screenQuadLayer: screenQuadLayerFilter.layers

    RenderSurfaceSelector {

        CameraSelector {
            id: sceneCameraSelector

            // Fill G-Buffer
            LayerFilter {
                id: sceneLayerFilter
                RenderTargetSelector {
                    id: gBufferTargetSelector
                    target: gBuffer

                    ClearBuffers {
                        buffers: ClearBuffers.ColorDepthBuffer

                        RenderPassFilter {
                            id: geometryPass
                            matchAny: FilterKey {
                                name: "pass"
                                value: "geometry"
                            }
                        }
                    }
                }
            }

            TechniqueFilter {
                parameters: [
                    Parameter { name: "color"; value: gBuffer.color },
                    Parameter { name: "position"; value: gBuffer.position },
                    Parameter { name: "normal"; value: gBuffer.normal },
                    Parameter { name: "depth"; value: gBuffer.depth }
                ]

                RenderStateSet {
                    // Render FullScreen Quad
                    renderStates: [
                        BlendEquation { blendFunction: BlendEquation.Add },
                        BlendEquationArguments {
                            sourceRgb: BlendEquationArguments.SourceAlpha
                            destinationRgb: BlendEquationArguments.DestinationColor
                        }
                    ]

                    LayerFilter {
                        id: screenQuadLayerFilter
                        ClearBuffers {
                            buffers: ClearBuffers.ColorDepthBuffer
                            RenderPassFilter {
                                matchAny: FilterKey {
                                    name: "pass"
                                    value: "final"
                                }
                                parameters: Parameter {
                                    name: "winSize"
                                    value: Qt.size(1024, 768)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

(上記のコードはqt3d/tests/manual/deferred-renderer-qmlから引用しています)。

グラフィック的には、結果のフレームグラフは次のようになります:

RenderViewは以下のようになります:

  • RenderView (1)
    • 使用するカメラの指定
    • 画面全体を埋めるビューポートを定義します。
    • レイヤーコンポーネントsceneLayerのすべてのEntityを選択します。
    • gBuffer 、アクティブなレンダーターゲットとして設定します。
    • 現在バインドされているレンダーターゲット(gBuffer )の色と深度をクリアします。
    • の注釈に一致する Material と Technique を持つシーン内の Entities のみを選択します。RenderPassFilter
  • レンダービュー (2)
    • 画面全体を埋めるビューポートを定義します。
    • レイヤコンポーネントscreenQuadLayerのすべてのEntityを選択します。
    • 現在バインドされているフレームバッファ(スクリーン)のカラーバッファとデプスバッファをクリアします。
    • のアノテーションに一致するマテリアルとテクニックを持つシーン内のエンティティのみを選択します。RenderPassFilter

フレームグラフのその他の利点

FrameGraphツリーは完全にデータドリブンであり、実行時に動的に変更できるので、次のことができます:

  • 異なるプラットフォームやハードウェアに対して異なるフレームグラフツリーを持ち、実行時に最適なものを選択できる。
  • シーンにビジュアル・デバッグを簡単に追加し、有効にする。
  • シーンの特定の領域でレンダリングする内容に応じて、異なるFrameGraphツリーを使用する。
  • Qt 3D の内部を変更することなく、新しいレンダリング技術を実装。

まとめ

FrameGraph と、それを構成するノードタイプを紹介しました。そして、FrameGraph の構築ルールと、Qt 3D エンジンが裏でどのように FrameGraph を使用するかを説明するために、いくつかの例を説明しました。ここまでで、あなたは、FrameGraphの概要とその使用方法(おそらく、フォワード・レンダラーに初期のz-fillパスを追加する)について、かなり理解できたはずです。また、FrameGraph は、Qt 3D が提供するレンダラーやマテリアルに縛られないように、あなたが使用するためのツールであることを常に念頭に置く必要があります。

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.