Babylon Native无头渲染

当我们说渲染时,我们通常谈论的是应用程序以 60fps 的速度渲染,无论是游戏还是其他使用 GPU 的应用程序。 然而,在其他场景中,我们可能希望使用 GPU 来运行根本不显示任何内容的进程,例如处理视频、操作图像或渲染 3D 资源,也许所有这些都在服务器上运行。 在本文中,我将描述如何使用 Babylon Native 来实现此类场景。 具体来说,我将展示一个示例,说明如何使用 Babylon Native 在 Windows 上使用 DirectX 11 捕获 3D 资源的屏幕截图。

免责声明:本示例中使用的 API 合约可能会发生变化,因为核心团队仍在研究正确的 API 合约形式。

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;
}

此代码并不罕见,但可以对其进行调整以使用 WARP,例如,如果环境没有 GPU。

接下来,我们将使用这个 DirectX 设备创建一个 Babylon Native 图形设备。

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;
}

我们必须指定宽度和高度(本例中为 1024x1024),因为 Babylon Native 设备不像通常那样与窗口或视图关联。

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;
}

然后,我们通过名为ExternalTexture 的Babylon Native 插件向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)来确保正确的顺序。

ExternalTexture 文档中所述, ExternalTexture::AddToContextAsync 函数要求图形设备在完成之前渲染一帧。 addToContext future 将等待,直到调用  AddToContextAsync,并且 FinishRenderingCurrentFrame 将渲染一帧以允许 AddToContextAsync 完成。

2、JavaScript(第 1 部分)

接下来,我们将回顾JavaScript端的第一部分(启动)。 忽略典型的 Babylon.js 引擎和场景设置,此函数采用一个名为 nativeTexture 的参数,它是来自 AddToContextAsync 结果的纹理。 然后使用 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::StartFrameCaptureRenderDoc::StopFrameCapture 的调用了吗? 这些将告诉 RenderDoc 开始和停止捕获帧,因为 RenderDoc 不知道帧何时开始或停止,因为我们不在典型的渲染情况下。 我们可以通过取消 RenderDoc.h 中一行的注释来打开 RenderDoc 捕获。 使用 RenderDoc 对于调试 GPU 问题非常有用。

6、结束语

我希望这能让你了解如何在无头环境中使用 Babylon Native。 这不是典型的场景,但它是使用其他技术实现更困难或更昂贵的场景。 我们将继续努力让Babylon Native在尽可能多的场景中发挥作用。


原文链接:Babylon Native in a Headless Environment

BimAnt翻译整理,转载请标明出处