NVIDIA Falcor を使ってみる

NVIDIA Falcor とは

D3D12 をバックエンドとした、リアルタイムレンダリングのフレームワークです。 昨今のゲームエンジンほどの手厚い機能はありませんが、レンダリングAPIの抽象化レイヤーが軽量なのでカスタマイズが容易です。 抽象化レイヤーは、ラスタライズのみならず DXR をサポートしているのが大きな特長で、DXR を使って何か試してみたいときには特に有用なフレームワークになります。 また、DepthPre パスや、GBuffer パス、SVGF パスなどが初めから用意されているので、Raster と RT のハイブリッドレンダリングをテストしたい場合や、RT のデノイザー開発にも対応できます。 また、今回は紹介しませんが、CUDA を使った処理もサポートされているので、必要に応じてこちらも使ってみると面白いかもしれません。

Getting Started

早速ですが、環境をセットアップします。とはいっても、基本的には GitHub のリポジトリを Clone してビルドするだけです。
https://github.com/nvidiagameworks/falcor

2020/11現在の推奨されるビルド環境は以下の通りです

  • Windows 10 version 1809 or newer
  • Visual Studio 2019
  • Microsoft Windows SDK version 1903 (10.0.18362.1)

また、必須ではありませんが以下のパッケージや機材を準備することをおすすめします

  • GeForce RTXシリーズ (もしくはDXRをサポートしたGPU)
  • Windows 10 Graphics Tools (WindowsのOptional Featureからインストールするパッケージ。DebugLayerのために必要)
  • NVAPI (NVAPIによる拡張機能を使用したい場合)

GitのリポジトリをClone

git clone --recursive -b master git@github.com:NVIDIAGameWorks/Falcor.git

NVAPIのインストール

NVAPIを使用する方は、 ここより NVAPI の最新パッケージをダウンロードして、Externals/.packman/nvapi に配置します。 ダウンロードの際にNVIDIAの開発者登録アカウントが必要になります。 次にFalcorConfig.hを開き、_ENABLE_NVAPI を1に設定します。

#define _ENABLE_NVAPI 1 // Set this to 1 to enable NVIDIA specific DX extensions. Make sure you have the NVAPI package in your 'Externals' directory. View the readme for more information.

FalcorTest プロジェクトをビルド

Tools/FalcorTest をビルドすると、Falcor (Falcor本体。レンダリングのバックエンドになるDLL) と、FalcorTest(各種単機能テストのレンダリングが書かれたアプリ)がビルドされます。 実行すると、各種機能がテストされて結果がコンソールに表示されます。詳細なテストではありませんが、まずここで自分の使いたい機能が正しく動作しているかチェックしておきましょう。 幾つかの機能は無条件にスキップされるようになっているので、適宜ソースを書き換えてテストしましょう。

たとえば、ShaderModel6.5 のサポートを確認したければ、このように書き換えます。

#if 0
    GPU_TEST(ShaderModel6_4, "Requires shader model 6.4") { test(ctx, "6_4"); }
    GPU_TEST(ShaderModel6_5, "Requires shader model 6.5") { test(ctx, "6_5"); }
#else
    GPU_TEST(ShaderModel6_4) { test(ctx, "6_4"); }
    GPU_TEST(ShaderModel6_5) { test(ctx, "6_5"); }
#endif

TraceRayInline は、2020/11現在は、Falcor内にはシェーダーのコンパイルが通るかのテストコードしかありませんが、とりあえずテストすることが出来ます。

#if 0
    GPU_TEST(testTraceRayInlineAPI, "Requires shader model 6.5")
#else
    GPU_TEST(testTraceRayInlineAPI)
#endif
    {
        // We don't actually run the program, just make sure it compiles.
        ctx.createProgram("Tests/Slang/TraceRayInline.cs.slang", "testTraceRayInlineAPI", Program::DefineList(), Shader::CompilerFlags::None, "6_5");
    }

サンプルコードに目を通す

ProjectTemplate

Project Template

Samples/ProjectTemplate をビルドして実行すると、ウィンドウが開き、緑の画面とGUIが表示されます。チュートリアルではよくあるお約束の RenderTarget をクリアしているだけのプログラムになります。 プログラム本体はほんの数行で、Guiにボタンを追加するのと画面を緑色でクリアするコードが記述されているだけです。D3D12のAPIは抽象化されており、Render Target のクリアはこの一行で行われます。

    pRenderContext->clearFbo(pTargetFbo.get(), clearColor, 1.0f, 0, FboAttachmentType::All);

RenderingContext は、メンバーに LowLevelContextData クラスを保持しており、これが D3D12 の CommandAllocator / CommandList / CommandQueue などを保持しています。 D3D12Device はグローバル変数としてアクセス可能で、一通りのAPIの抽象化レイヤーが提供されていますが、ネイティブ API へのアクセスも簡単なものになっています。 一方で、Guiのクラスを覗くと、Dear ImGui がGUIのバックエンドとして使われているのが分かります。

ShaderToy

ShaderToy

Samples/ShaderToy プロジェクトをビルドすると、フルスクリーンの PixelShader のパスで、シェーダー Samples/ShaderToy/Toy.ps.slang が実行されます。 初期化処理の OnLoad で幾つかのステートを作成していますが全部不要な処理です。初期化処理で実際に必要とされる処理は下記の一行のみです。

// Load shaders
mpMainPass = FullScreenPass::create("Samples/ShaderToy/Toy.ps.slang");

描画時の onFrameRender では、初期化した mpMainPass のConstant Buffer の値を設定して execute を呼び出して描画しているだけです。

mpMainPass["ToyCB"]["iResolution"] = float2(width, height);
mpMainPass["ToyCB"]["iGlobalTime"] = (float)gpFramework->getGlobalClock().getTime();
mpMainPass->execute(pRenderContext, pTargetFbo);

このように FullScreenPass クラスを使うと、ポストプロセス処理などで使うフルスクリーンのPixelShaderパスを簡単に構築することができます。Falcor には、FullScreenPass 以外にも極めて基本的な Dispatch や Draw を呼び出す機能がクラスとして標準で用意されています。これらは、Falcor プロジェクトの RenderGraph/BasePasses にあるので、興味があれば参照してください。

ModelViewer

Samples/ModelViewerをビルドして実行します。Gui の Load Model をクリックして、Falcor/Media/teapot.obj を読みます。(他にも、Media/Arcade/Arcade.fbxも読み込むことができます) Falcor のアセット読み込みのバックエンドは、 assimp を使用しています。ライトとカメラの設定が無いので、初期状態ではカメラはteapotの中にありますし、レンダリングが真っ黒ですが、ASWDでカメラを動かせば、とりあえずジオメトリが読み込まれていることが分かります。

ModelViewer

ModelViewer.ps.slang をチェックすると、単純にシーン上のライトをイテレーションして、マテリアルを評価しています。したがって、ライトさえ配置されていれば正しくレンダリングされるように書かれています。なので、ModelViewer.cpp の、SceneBuilder がファイルを読み終わった後の箇所で、以下の様にライトの数を確認してライトが無ければ DirectionalLight を追加するように処理を書き加えて実行します。

if (pBuilder->getLightCount() == 0) {
    // no lights.
    pBuilder->addLight(DirectionalLight::create());
}

これで、シーン上にライトが無い場合はデフォルトのDirectionalLightが追加されるので、ライティングの様子が見れるようになりました。

ModelViewer with a light

次に、Media/Arcade/Arcade.fscene を読み込んでみます。fscene は、Json で記述された Falcorのシーン格納形式で、ライトとカメラの定義が含まれているので、特にソースコードを変更することなくライティングの様子を見ることができます。Arcade.fscene はリファレンスとして Arcade.fbx と Light.fbx を参照して、内部にライトとカメラの定義を保持しています。

ModelViewer fscene

ModelViewer のプログラムを見てみると、初期化時に WireFrame描画用の RasterStateと、幾つかのCullModeの RasterStatreを作っています。DepthStencilStateも幾つか作っています。これらはGui上で選択して使用する事ができます。 シェーダープログラムは、アプリケーション側は PixelShader のみ指定して、頂点シェーダーと DrawCall の呼び出しは、Falcor 側の mpScene->render() に任されています。

ModelViewer wire frame

HelloDXR

Samples/HelloDXRをビルドして実行します。先ほどの Arcade/Arcade.fscene が起動時から読み込まれています。

Hello DXR (RTX ON)
Hello DXR (RTX OFF)

Ray Traceのチェックボックスで、DXRの有効/無効を切り替える事ができ、影と反射の効果の有無が違いとして見て取れます。 ソースコードを見ると、DXRが無効な時のレンダリングは、Falcorの組み込みレンダリングパスのRasterScenePassが使用されています。設定されているピクセルシェーダーは、ModelViewerとほぼ同じで、Falcorの組み込みシェーディング関数を使用しています。つまり、先の ModelViewer のサンプルプログラムは、RasterState の変更をしないのであれば、RasterScenePassを使って一行で記述できるという事です。

    mpRasterPass = RasterScenePass::create(mpScene, "Samples/HelloDXR/HelloDXR.ps.slang", "", "main");
    .
    .
    .
    mpRasterPass->renderScene(pRenderContext, pTargetFbo);

DXR側は、HelloDXR.rt.slangに記述されている各シェーダーをShaderLibraryとしてコンパイルして、RtProgram::Descを使ってHitGroupの定義を行っています。

    RtProgram::Desc rtProgDesc;
    rtProgDesc.addShaderLibrary("Samples/HelloDXR/HelloDXR.rt.slang").setRayGen("rayGen");
    rtProgDesc.addHitGroup(0, "primaryClosestHit", "primaryAnyHit").addMiss(0, "primaryMiss");
    rtProgDesc.addHitGroup(1, "", "shadowAnyHit").addMiss(1, "shadowMiss");
    rtProgDesc.addDefines(mpScene->getSceneDefines());
    rtProgDesc.setMaxTraceRecursionDepth(3); // 1 for calling TraceRay from RayGen, 1 for calling it from the primary-ray ClosestHitShader for reflections, 1 for reflection ray tracing a shadow ray

    mpRaytraceProgram = RtProgram::create(rtProgDesc);
    mpRtVars = RtProgramVars::create(mpRaytraceProgram, mpScene);
    mpRaytraceProgram->setScene(mpScene);

Closest Hitのシェーディングの関数は、Rasterで使用しているものと同一で、prepareShadigData()でサーフェースの情報を取得して、envalMaterial()でシェーディングを行っています。 いる。ただし、ライトの評価の際に checkLightHit() で ShadowRay を飛ばして遮蔽のチェックをしています。また、リフレクションの Ray を飛ばしており、GGXのアルファ値(Roughness相当のパラメーター)で適当にブレンディングしています。(PBRではない) そのため、DXRを有効にした場合は、影と反射が追加されたレンダリングになります。

Mogwaiに目を通す

注意

ビルドの際にコンパイルエラー等は発生しませんでしたが、実行時にPythonのBindingを設定している個所で例外が発生しました。この問題は、VisualStudioをアップデートすることで解決しました。動作確認は Microsoft Visual Studio Professional 2019 - Version 16.7.7 を用いて行いました。同様の問題が発生した場合は、VSのバージョンをチェックすると良いかもしれません。

Mogwai

Falcorは Pybind11を使って、RenderGraph を Python で記述するための機構を持っています。Mogwaiはその機構を使ったフレームワークと呼べると思います。 ビルドすると、Pythonで記述されたいくつかの RenderGraph が、実行ファイルの出力先の Data フォルダに用意されます。 起動後に File->Load Scriptで Data/FowardRenderer.py を読み込みます。これで Foward Rendering の RenderGraph が読み込まれて、使用できるようになります。 次にFile->Load Sceneで、Falcor/Media/Arcade/Arcade.fsceneを読み込みます。 すると、サンプルの Model Viewerと同様にレンダリングが確認できると思います。しかし、ModelViewerとは異なり、先ほど読み込んだ Pythonのスクリプトに記述された RenderGraph によってレンダリングの動作が定義されています。

ここで、File->LoadScript で、MinimalPathTracer.py を読み込むことで、パストレーシングのRenderGraphを、追加で読み込むことができます。 HUD上でRenderGraphを切り替えると、MinimalPathTracerにレンダリングパスを切り替えることができます。このように、Mogwaiを使うと、python で記述された複数の RenderGraph を動的に切り替える事ができます。これはレンダリングの比較評価等を行う際に有用だと言えると思います。

RenderGraph を ForwardRenderer に戻してEditボタンを押すと、RenderGraph の編集ができます。 基本的には FowardRenderer.py に書かれている内容そのままですが、RenderGraph を視覚的に確認してエディットすることもできます。ただ、オマケ的な要素が強く、実際の RenderGraph の構築では、Pythonスクリプトを直接編集したほうが早いです。

Mogwai Render Graph

RenderGraphのノード

RenderGraph の個々のノードは、Falcor の RenderPass クラスを継承したクラスです。 たとえば DepthPrePass ノードは DepthPass.dll として事前にビルドされており、これが ForwardRenderer.py の中でインポートされて、他のノードと接続されることでレンダリングパスが構築されています。他にも、GBuffer描画や、CascadedShadowMap, SSAO, Antialias, Tonemapping, SkyBox, それからレイトレーシング用に、MinimalPathTracer, AccumulatePass, SVGFPassなど、他にも幾つかの有用なノードが用意されています。これら RenderGraph のノードは個々のDLLとしてコンパイルされています。そのためのプロジェクトは、Falcor ソリューション内の RenderPasses フォルダに配置されているので確認してみてください。

Mogwai Render Graph

DepthPassに目を通す

RenderGraph の一つのノードの例として、DepthPass ノードを見てみます。DepthPass ノードは RenderPass クラスを継承して実装され、DepthPass.dllとして配置され、Falcor のレンダリングに使用されます。 ヘッダーファイルを見ると、Gui の描画やリフレクションをサポートする関数、シーン情報にアクセスをするための setScene があります。

    static SharedPtr create(RenderContext* pRenderContext = nullptr, const Dictionary& dict = {});

    virtual RenderPassReflection reflect(const CompileData& compileData) override;
    virtual void execute(RenderContext* pContext, const RenderData& renderData) override;
    virtual void setScene(RenderContext* pRenderContext, const Scene::SharedPtr& pScene) override;
    virtual void renderUI(Gui::Widgets& widget) override;
    virtual Dictionary getScriptingDictionary() override;
    virtual std::string getDesc() override { return kDesc; }

reflect()を見ると、レンダリングの出力として、2DのDepthStencilを出力することが分かります。入力は無いので、setSceneで設定されたシーンから取得できるカメラとジオメトリを用いてレンダリングすることが想像できます。 DepthBuffer のフォーマットはクラス内部に保持されて、Guiによる設定を介して変更できるようです。その結果がリフレクションの出力ピンに反映されます。

RenderPassReflection DepthPass::reflect(const CompileData& compileData)
{
    RenderPassReflection reflector;
    reflector.addOutput(kDepth, "Depth-buffer").bindFlags(Resource::BindFlags::DepthStencil).format(mDepthFormat).texture2D(0, 0, 0);
    return reflector;
}

描画解像度の設定は無く、DLLの外部で用意されたDepthStencilバッファをBindしてレンダリングする仕組みになっています。シェーダーは、Slangで記述されたピクセルシェーダーが一つだけあります。 中を確認すると、prepareShadingDataを呼び出しています。これは、アルファが完全に透明だった場合に、この関数のなかでDiscardが呼ばれるためです。

void main(VSOut vOut, uint triangleIndex : SV_PrimitiveID) : SV_TARGET
{
    // Calling prepareShadingData() to discard pixels that fail alpha test. The pixel shader has no other side effects.
    float3 viewDir = normalize(gScene.camera.getPosition() - vOut.posW);
    prepareShadingData(vOut, triangleIndex, viewDir);
}

ちなみに、描画解像度の設定は、Falcor の RenderGraph 側にあります。RenderGraphCompiler::allocateResources() が RenderGraph の変更や描画解像度変更をトリガーとして、使用されている出力ピンのリソースを ResourceCache に登録します。その時に、各種 RnederTarget の解像度の設定が行われます。

実際に RenderGraph のノードを実装するならば、ぜひ Docs/ Tutorials をご一読する事をお勧めします。

最後に

ビルドして実行するだけではいまいち概要がつかみにくい Falcor ですが、リポジトリの ドキュメントを読めば、この記事に書いてあることを含めて記載があります。ソースコードの規模もあまり大きくないので、全体の把握も比較的容易だと思います。アセットもFBX形式が読み込めるので、手元のアセットでテストしたい場合なども比較的短時間でセットアップできるのではないでしょうか。今回は紹介していませんが、 ORCAをはじめ、CC-BY等のオープンライセンスの元で公開されているアセットなどもあるので、これらを活用してレンダリングのテストを行う事も可能だと思います。

今回は、簡単にですが Falcor を触ってみました。この手の軽量なレンダリングフレームワークというのは、自作含めて多数あると思いますが、RasterとDXRをシームレスにサポートしている軽量フレームワークはあまり見かけません。 そういう意味ではユニークな存在かもしれません。

shikihuiku
shikihuiku

リアルタイムレンダリングが好き

Related