レンダリングというと、ゲームであろうと GPU を使用する別のアプリケーションであろうと、通常は 60 fps でレンダリングするアプリケーションのことを指します。ただし、他のシナリオでは、GPU を使用して、ビデオの処理、画像の操作、3D アセットのレンダリングなど、何も表示しないプロセス (おそらくすべてサーバー上で実行される) を実行したい場合があります。この記事では、Babylon Native を使用してそのようなシナリオを実装する方法について説明します。具体的には、Babylon Native を使用して、Windows 上の DirectX 11 を使用して 3D アセットのスクリーンショットをキャプチャする方法の例を示します。
免責事項: この例で使用されている API コントラクトは、コア チームが正しい API コントラクト フォームの開発中であるため、変更される可能性があります。
推奨: NSDT エディタを使用して、プログラム可能な 3D シーンをすばやく構築します
1. コンソールアプリケーション
サンプル リポジトリはここにあります。CMake を使用して Windows 用の Visual Studio プロジェクトを生成します。Babylon Native と DirectXTK の依存関係はサブモジュールを通じて組み込まれ、CMake で使用されます。DirectXTK 依存関係は、DirectX テクスチャを PNG ファイルに保存するためにのみ使用されます。
アプリケーションのコアは、App.cpp というファイルと、index.js 内のそれに相当する JavaScript です。ネイティブコード側から始めて、いくつか詳しく見てみましょう。
1.1 DirectX グラフィックス デバイスの作成
まず、スタンドアロンの DirectX デバイスを作成する必要があります。
winrt::com_ptr<ID3D11Device> CreateD3DDevice()
{
winrt::com_ptr<ID3D11Device> d3dDevice{};
uint32_t flags = D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT;
winrt::check_hresult(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, nullptr, 0, D3D11_SDK_VERSION, d3dDevice.put(), nullptr, nullptr));
return d3dDevice;
}
このコードは珍しいものではありませんが、たとえば環境に GPU がない場合は、WARP を使用するように適合させることができます。
次に、この DirectX デバイスを使用して、Babylon ネイティブ グラフィックス デバイスを作成します。
std::unique_ptr<Babylon::Graphics::Device> CreateGraphicsDevice(ID3D11Device* d3dDevice)
{
Babylon::Graphics::DeviceConfiguration config{d3dDevice};
std::unique_ptr<Babylon::Graphics::Device> device = Babylon::Graphics::Device::Create(config);
device->UpdateSize(WIDTH, HEIGHT);
return device;
}
Babylon Native デバイスは通常のようにウィンドウまたはビューに関連付けられていないため、幅と高さ (この場合は 1024x1024) を指定する必要があります。
1.2 JavaScript ホスティング環境の作成
もちろん、JavaScript ホスティング環境も作成する必要があります。この場合、Chakra (Windows のデフォルト) を使用して、Babylon.js コアとローダー モジュール、および前述の JavaScript ロジックが存在する Index.js をロードします。その後、フレームのレンダリングも開始します。これにより、JavaScript がグラフィックス コマンドをキューに入れることがなくなります。
auto runtime = std::make_unique<Babylon::AppRuntime>();
runtime->Dispatch([&device](Napi::Env env)
{
device->AddToJavaScript(env);
Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto)
{
std::cout << message;
});
Babylon::Polyfills::Window::Initialize(env);
Babylon::Polyfills::XMLHttpRequest::Initialize(env);
Babylon::Plugins::NativeEngine::Initialize(env);
});
Babylon::ScriptLoader loader{*runtime};
loader.LoadScript("app:///Scripts/babylon.max.js");
loader.LoadScript("app:///Scripts/babylonjs.loaders.js");
loader.LoadScript("app:///Scripts/index.js");
device->StartRenderingCurrentFrame();
deviceUpdate->Start();
Visual Studio で Chakra を使用すると、デバッガーを追加できるため便利です。JavaScript コードの任意の場所にステートメントを追加すると、Visual Studio インスタント デバッガーがダイアログ ボックスを通じて JavaScript をデバッグするよう求めます。アプリケーションが正しく動作するには、デバッグ構成で実行する必要があることに注意してください。
1.3 出力テクスチャ
また、Babylon.js カメラの OutputRenderTarget の出力レンダー ターゲット テクスチャを作成する必要もあります。まず、DirectX レンダー ターゲット テクスチャを作成します。
winrt::com_ptr<ID3D11Texture2D> CreateD3DRenderTargetTexture(ID3D11Device* d3dDevice)
{
D3D11_TEXTURE2D_DESC desc{};
desc.Width = WIDTH;
desc.Height = HEIGHT;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc = {1, 0};
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = 0;
desc.MiscFlags = 0;
winrt::com_ptr<ID3D11Texture2D> texture;
winrt::check_hresult(d3dDevice->CreateTexture2D(&desc, nullptr, texture.put()));
return texture;
}
次に、外部テクスチャと呼ばれる Babylon ネイティブ プラグインを通じてネイティブ テクスチャを JavaScript に公開します。
std::promise<void> addToContext{};
std::promise<void> startup{};
loader.Dispatch([externalTexture = Babylon::Plugins::ExternalTexture{outputTexture.get()}, &addToContext, &startup](Napi::Env env)
{
auto jsPromise = externalTexture.AddToContextAsync(env);
addToContext.set_value();
jsPromise.Get("then").As<Napi::Function>().Call(jsPromise,
{
Napi::Function::New(env, [&startup](const Napi::CallbackInfo& info)
{
auto nativeTexture = info[0];
info.Env().Global().Get("startup").As<Napi::Function>().Call(
{
nativeTexture,
Napi::Value::From(info.Env(), WIDTH),
Napi::Value::From(info.Env(), HEIGHT),
});
startup.set_value();
})
});
});
addToContext.get_future().wait();
deviceUpdate->Finish();
device->FinishRenderingCurrentFrame();
startup.get_future().wait();
これは通常のレンダリング アプリケーションではなく、個々のフレームを明示的にレンダリングしているため、正しい順序を保証するために同期された構造 (この場合は std::promise) も必要であることに注意してください。
外部テクスチャのドキュメントで説明されているように、外部テクスチャ::AddToContextAsync 関数では、グラフィック デバイスが完了する前に 1 フレームをレンダリングする必要があります。addToContext フューチャーは、AddToContextAsync が呼び出されるまで待機し、FinishRenderingCurrentFrame がフレームをレンダリングして、AddToContextAsync が完了できるようにします。
2. JavaScript (パート 1)
次に、JavaScript 側の最初の部分 (起動) を確認します。一般的な Babylon.js エンジンとシーンのセットアップを無視して、この関数は、AddToContextAsync の結果からのテクスチャである、nativeTexture と呼ばれるパラメーターを受け取ります。次に、このパラメータは、wrapNativeTexture を使用してラップされ、Babylon.js レンダー ターゲット テクスチャにカラー アタッチメントとして追加されます。使い方については後ほど説明します。
function startup(nativeTexture, width, height) {
engine = new BABYLON.NativeEngine();
scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color3.White();
scene.createDefaultEnvironment({ createSkybox: false, createGround: false });
outputTexture = new BABYLON.RenderTargetTexture(
"outputTexture",
{
width: width,
height: height
},
scene,
{
colorAttachment: engine.wrapNativeTexture(nativeTexture),
generateDepthBuffer: true,
generateStencilBuffer: true
}
);
}
2.1 glTF アセット
ネイティブ コードに戻り、glTF リソースをロードしてスクリーンショットをキャプチャする準備が整いました。
struct Asset
{
const char* Name;
const char* Url;
};
std::array<Asset, 3> assets =
{
Asset{"BoomBox", "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoomBox/glTF/BoomBox.gltf"},
Asset{"GlamVelvetSofa", "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/GlamVelvetSofa/glTF/GlamVelvetSofa.gltf"},
Asset{"MaterialsVariantsShoe", "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/MaterialsVariantsShoe/glTF/MaterialsVariantsShoe.gltf"},
};
for (const auto& asset : assets)
{
RenderDoc::StartFrameCapture(d3dDevice.get());
device->StartRenderingCurrentFrame();
deviceUpdate->Start();
std::promise<void> loadAndRenderAsset{};
loader.Dispatch([&loadAndRenderAsset, &asset](Napi::Env env)
{
std::cout << "Loading " << asset.Name << std::endl;
auto jsPromise = env.Global().Get("loadAndRenderAssetAsync").As<Napi::Function>().Call(
{
Napi::String::From(env, asset.Url)
}).As<Napi::Promise>();
jsPromise.Get("then").As<Napi::Function>().Call(jsPromise,
{
Napi::Function::New(env, [&loadAndRenderAsset](const Napi::CallbackInfo&)
{
loadAndRenderAsset.set_value();
})
});
});
loadAndRenderAsset.get_future().wait();
deviceUpdate->Finish();
device->FinishRenderingCurrentFrame();
RenderDoc::StopFrameCapture(d3dDevice.get());
auto filePath = GetModulePath() / asset.Name;
filePath.concat(".png");
std::cout << "Writing " << filePath.string() << std::endl;
// See https://github.com/Microsoft/DirectXTK/wiki/ScreenGrab#srgb-vs-linear-color-space
winrt::check_hresult(DirectX::SaveWICTextureToFile(context.get(), outputTexture.get(), GUID_ContainerFormatPng, filePath.c_str(), nullptr, nullptr, true));
}
長く見えるかもしれませんが、それほど複雑ではありません。各アセットをループし、JavaScript 関数loadAndRenderAssetAsync を呼び出し、完了するのを待って、PNG をディスクに保存します。
3. JavaScript (パート 2)
JavaScript 側のloadAndRenderAssetAsync 関数は、glTF リソースをインポートし、カメラをセットアップし、シーンの準備が完了するのを待って、単一のフレームをレンダリングします。これは、Babylon.js を使用した Web アプリケーションで起こることと似ているはずです。
async function loadAndRenderAssetAsync(url) {
if (rootMesh) {
rootMesh.dispose();
}
const result = await BABYLON.SceneLoader.ImportMeshAsync(null, url, undefined, scene);
rootMesh = result.meshes[0];
scene.createDefaultCamera(true, true);
scene.activeCamera.alpha = 2;
scene.activeCamera.beta = 1.25;
scene.activeCamera.outputRenderTarget = outputTexture;
await scene.whenReadyAsync();
scene.render();
}
カメラの出力レンダー ターゲットには以前の出力レンダー ターゲット テクスチャが割り当てられるため、シーンはデフォルトのバック バッファ (もちろんこのコンテキストには存在しません) の代わりにこの出力テクスチャにレンダリングされます。これにより、先ほど設定したネイティブ DirectX レンダー ターゲット テクスチャに直接レンダリングされます。
4. 結果
ConsoleApp を構築して実行する例を以下に示します。
3 つの PNG ファイルを出力します。
5. RenderDoc を使用してデバッグする
後もう一つ!ヘルパー関数が RenderDoc::StartFrameCapture と RenderDoc::StopFrameCapture を呼び出していることに注目してください。これらは、RenderDoc にフレームのキャプチャを開始および停止するように指示します。これは、一般的なレンダリング状況ではないため、RenderDoc はフレームがいつ開始または停止するかを認識しないためです。RenderDoc.h 内の行のコメントを解除すると、RenderDoc キャプチャをオンにできます。RenderDoc の使用は、GPU の問題のデバッグに役立ちます。
6. 結論
これで、ヘッドレス環境で Babylon Native を使用する方法が理解できれば幸いです。これは一般的なシナリオではありませんが、他のテクノロジを使用して実装するのがより困難または高価になるシナリオです。今後も、Babylon Native をできるだけ多くのシーンで活用できるよう、努力してまいります。