Skip to content

Commit 32d1172

Browse files
authored
Add examples (#1525)
Add examples from https://github.com/BabylonJS/BabylonNativeExamples into this repo to simplify future CI testing.
1 parent fe9fdb8 commit 32d1172

20 files changed

+1333
-0
lines changed

Apps/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
if((WIN32 AND NOT WINDOWS_STORE) AND GRAPHICS_API STREQUAL D3D11)
2+
add_subdirectory(HeadlessScreenshotApp)
3+
add_subdirectory(StyleTransferApp)
4+
endif()
5+
16
if(NOT ANDROID)
27
add_subdirectory(Playground)
38
endif()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
FetchContent_Declare(DirectXTK
2+
GIT_REPOSITORY https://github.com/microsoft/DirectXTK.git
3+
GIT_TAG e7d9b64ca6bb8977d4e3006727deaf57a7c1de84)
4+
5+
FetchContent_MakeAvailable(CMakeExtensions)
6+
7+
set(BUILD_TESTING OFF)
8+
set(BUILD_TOOLS OFF)
9+
set(BUILD_XAUDIO_WIN8 OFF)
10+
set(DIRECTX_ARCH ${CMAKE_VS_PLATFORM_NAME})
11+
12+
FetchContent_MakeAvailable_With_Message(DirectXTK)
13+
14+
set_property(TARGET DirectXTK PROPERTY UNITY_BUILD false)
15+
set_property(TARGET DirectXTK PROPERTY FOLDER Apps/Dependencies)
16+
17+
set(BABYLON_SCRIPTS
18+
"../node_modules/babylonjs/babylon.max.js"
19+
"../node_modules/babylonjs-loaders/babylonjs.loaders.js")
20+
21+
set(SCRIPTS
22+
"Scripts/index.js")
23+
24+
set(SOURCES
25+
"Win32/RenderDoc.h"
26+
"Win32/RenderDoc.cpp"
27+
"Win32/App.cpp")
28+
29+
add_executable(HeadlessScreenshotApp ${BABYLON_SCRIPTS} ${SCRIPTS} ${SOURCES})
30+
31+
target_compile_definitions(HeadlessScreenshotApp
32+
PRIVATE UNICODE
33+
PRIVATE _UNICODE)
34+
35+
target_link_libraries(HeadlessScreenshotApp
36+
PRIVATE AppRuntime
37+
PRIVATE Console
38+
PRIVATE DirectXTK
39+
PRIVATE ExternalTexture
40+
PRIVATE NativeEngine
41+
PRIVATE ScriptLoader
42+
PRIVATE Window
43+
PRIVATE XMLHttpRequest)
44+
45+
foreach(SCRIPT ${BABYLON_SCRIPTS} ${SCRIPTS})
46+
get_filename_component(SCRIPT_NAME "${SCRIPT}" NAME)
47+
add_custom_command(
48+
OUTPUT "${CMAKE_CFG_INTDIR}/Scripts/${SCRIPT_NAME}"
49+
COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/${SCRIPT}" "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/Scripts/${SCRIPT_NAME}"
50+
COMMENT "Copying ${SCRIPT_NAME}"
51+
MAIN_DEPENDENCY "${CMAKE_CURRENT_SOURCE_DIR}/${SCRIPT}")
52+
endforeach()
53+
54+
set_property(TARGET HeadlessScreenshotApp PROPERTY FOLDER Apps)
55+
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SCRIPTS})
56+
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/../node_modules PREFIX Scripts FILES ${BABYLON_SCRIPTS})
57+
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})

Apps/HeadlessScreenshotApp/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# HeadlessScreenshotApp
2+
3+
This app is an example of a headless Windows console application that takes screenshots of 3D assets.
4+
5+
See the [medium article](https://babylonjs.medium.com/babylon-native-in-a-headless-environment-868409b8b1cf) for more information.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/// <reference path="../../node_modules/babylonjs/babylon.module.d.ts" />
2+
/// <reference path="../../node_modules/babylonjs-loaders/babylonjs.loaders.module.d.ts" />
3+
4+
let engine = null;
5+
let scene = null;
6+
let outputTexture = null;
7+
let rootMesh = null;
8+
9+
/**
10+
* Sets up the engine, scene, and output texture.
11+
*/
12+
function startup(nativeTexture, width, height) {
13+
// Create a new native engine.
14+
engine = new BABYLON.NativeEngine();
15+
16+
// Create a scene with a white background.
17+
scene = new BABYLON.Scene(engine);
18+
scene.clearColor.set(1, 1, 1, 1);
19+
20+
// Create an environment so that reflections look good.
21+
scene.createDefaultEnvironment({ createSkybox: false, createGround: false });
22+
23+
// Wrap the input native texture in a render target texture for the output
24+
// render target of the camera used in `loadAndRenderAssetAsync` below.
25+
// Note that the properties (width, height, samples, etc.) must match the
26+
// native texture created in `App.cpp`.
27+
outputTexture = new BABYLON.RenderTargetTexture(
28+
"outputTexture",
29+
{
30+
width: width,
31+
height: height,
32+
},
33+
scene,
34+
{
35+
colorAttachment: engine.wrapNativeTexture(nativeTexture),
36+
generateDepthBuffer: true,
37+
generateStencilBuffer: true,
38+
samples: 4,
39+
}
40+
);
41+
}
42+
43+
/**
44+
* Loads and renders an asset given its URL.
45+
*/
46+
async function loadAndRenderAssetAsync(url) {
47+
// Dispose the previous asset if present.
48+
if (rootMesh) {
49+
rootMesh.dispose();
50+
}
51+
52+
// Load the asset from the input URL.
53+
const result = await BABYLON.SceneLoader.ImportMeshAsync(null, url, undefined, scene);
54+
rootMesh = result.meshes[0];
55+
56+
// Create a default camera that looks at the asset from a specific angle
57+
// and outputs to the render target created in `startup` above.
58+
scene.createDefaultCamera(true, true);
59+
scene.activeCamera.alpha = 2;
60+
scene.activeCamera.beta = 1.25;
61+
scene.activeCamera.outputRenderTarget = outputTexture;
62+
63+
// Enable ACES tone mapping in image processing configuration.
64+
scene.imageProcessingConfiguration.toneMappingEnabled = true;
65+
scene.imageProcessingConfiguration.toneMappingType = BABYLON.ImageProcessingConfiguration.TONEMAPPING_ACES;
66+
67+
// Wait until the scene is ready before rendering the frame.
68+
await scene.whenReadyAsync();
69+
70+
// Render one frame.
71+
scene.render();
72+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#include <Babylon/AppRuntime.h>
2+
#include <Babylon/Graphics/Device.h>
3+
#include <Babylon/ScriptLoader.h>
4+
#include <Babylon/Plugins/ExternalTexture.h>
5+
#include <Babylon/Plugins/NativeEngine.h>
6+
#include <Babylon/Polyfills/Console.h>
7+
#include <Babylon/Polyfills/Window.h>
8+
#include <Babylon/Polyfills/XMLHttpRequest.h>
9+
10+
#include <winrt/base.h>
11+
12+
#include <WICTextureLoader.h>
13+
#include <ScreenGrab.h>
14+
#include <wincodec.h>
15+
16+
#include <filesystem>
17+
#include <iostream>
18+
19+
#include "RenderDoc.h"
20+
21+
namespace
22+
{
23+
constexpr const uint32_t WIDTH = 1024;
24+
constexpr const uint32_t HEIGHT = 1024;
25+
26+
std::filesystem::path GetModulePath()
27+
{
28+
WCHAR modulePath[4096];
29+
DWORD result = GetModuleFileNameW(nullptr, modulePath, ARRAYSIZE(modulePath));
30+
winrt::check_bool(result != 0 && result != std::size(modulePath));
31+
return std::filesystem::path{modulePath}.parent_path();
32+
}
33+
34+
winrt::com_ptr<ID3D11Device> CreateD3DDevice()
35+
{
36+
winrt::com_ptr<ID3D11Device> d3dDevice{};
37+
uint32_t flags = D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT;
38+
winrt::check_hresult(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, nullptr, 0, D3D11_SDK_VERSION, d3dDevice.put(), nullptr, nullptr));
39+
return d3dDevice;
40+
}
41+
42+
winrt::com_ptr<ID3D11Texture2D> CreateD3DRenderTargetTexture(ID3D11Device* d3dDevice)
43+
{
44+
D3D11_TEXTURE2D_DESC desc{};
45+
desc.Width = WIDTH;
46+
desc.Height = HEIGHT;
47+
desc.MipLevels = 1;
48+
desc.ArraySize = 1;
49+
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
50+
desc.SampleDesc = {4, 0};
51+
desc.Usage = D3D11_USAGE_DEFAULT;
52+
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
53+
desc.CPUAccessFlags = 0;
54+
desc.MiscFlags = 0;
55+
56+
winrt::com_ptr<ID3D11Texture2D> texture;
57+
winrt::check_hresult(d3dDevice->CreateTexture2D(&desc, nullptr, texture.put()));
58+
return texture;
59+
}
60+
61+
Babylon::Graphics::Device CreateGraphicsDevice(ID3D11Device* d3dDevice)
62+
{
63+
Babylon::Graphics::Configuration config{};
64+
config.Device = d3dDevice;
65+
config.Width = WIDTH;
66+
config.Height = HEIGHT;
67+
return Babylon::Graphics::Device(config);
68+
}
69+
70+
void CatchAndLogError(Napi::Promise jsPromise)
71+
{
72+
auto jsOnRejected = Napi::Function::New(jsPromise.Env(), [](const Napi::CallbackInfo& info) {
73+
auto console = info.Env().Global().Get("console").As<Napi::Object>();
74+
console.Get("error").As<Napi::Function>().Call(console, {info[0]});
75+
std::exit(1);
76+
});
77+
78+
jsPromise.Get("catch").As<Napi::Function>().Call(jsPromise, {jsOnRejected});
79+
}
80+
}
81+
82+
int main()
83+
{
84+
// Initialize RenderDoc.
85+
RenderDoc::Init();
86+
87+
// Create a DirectX device.
88+
auto d3dDevice = CreateD3DDevice();
89+
90+
// Get the immediate context for DirectXTK when saving the texture.
91+
winrt::com_ptr<ID3D11DeviceContext> d3dDeviceContext;
92+
d3dDevice->GetImmediateContext(d3dDeviceContext.put());
93+
94+
// Create the Babylon Native graphics device and update.
95+
auto device = CreateGraphicsDevice(d3dDevice.get());
96+
auto deviceUpdate = device.GetUpdate("update");
97+
98+
// Start rendering a frame to unblock the JavaScript from queuing graphics
99+
// commands.
100+
device.StartRenderingCurrentFrame();
101+
deviceUpdate.Start();
102+
103+
// Create a Babylon Native application runtime which hosts a JavaScript
104+
// engine on a new thread.
105+
Babylon::AppRuntime runtime{};
106+
runtime.Dispatch([&device](Napi::Env env) {
107+
// Add the Babylon Native graphics device to the JavaScript environment.
108+
device.AddToJavaScript(env);
109+
110+
// Initialize the console polyfill.
111+
Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto) {
112+
std::cout << message;
113+
});
114+
115+
// Initialize the window, XMLHttpRequest, and NativeEngine polyfills.
116+
Babylon::Polyfills::Window::Initialize(env);
117+
Babylon::Polyfills::XMLHttpRequest::Initialize(env);
118+
Babylon::Plugins::NativeEngine::Initialize(env);
119+
});
120+
121+
// Load the scripts for Babylon.js core and loaders plus this app's index.js.
122+
Babylon::ScriptLoader loader{runtime};
123+
loader.LoadScript("app:///Scripts/babylon.max.js");
124+
loader.LoadScript("app:///Scripts/babylonjs.loaders.js");
125+
loader.LoadScript("app:///Scripts/index.js");
126+
127+
// Create a render target texture for the output.
128+
winrt::com_ptr<ID3D11Texture2D> outputTexture = CreateD3DRenderTargetTexture(d3dDevice.get());
129+
130+
std::promise<void> addToContext{};
131+
std::promise<void> startup{};
132+
133+
// Create an external texture for the render target texture and pass it to
134+
// the `startup` JavaScript function.
135+
loader.Dispatch([externalTexture = Babylon::Plugins::ExternalTexture{outputTexture.get()}, &addToContext, &startup](Napi::Env env) {
136+
auto jsPromise = externalTexture.AddToContextAsync(env);
137+
addToContext.set_value();
138+
139+
auto jsOnFulfilled = Napi::Function::New(env, [&startup](const Napi::CallbackInfo& info) {
140+
auto nativeTexture = info[0];
141+
info.Env().Global().Get("startup").As<Napi::Function>().Call(
142+
{
143+
nativeTexture,
144+
Napi::Value::From(info.Env(), WIDTH),
145+
Napi::Value::From(info.Env(), HEIGHT),
146+
});
147+
startup.set_value();
148+
});
149+
150+
jsPromise = jsPromise.Get("then").As<Napi::Function>().Call(jsPromise, {jsOnFulfilled}).As<Napi::Promise>();
151+
152+
CatchAndLogError(jsPromise);
153+
});
154+
155+
// Wait for `AddToContextAsync` to be called.
156+
addToContext.get_future().wait();
157+
158+
// Render a frame so that `AddToContextAsync` will complete.
159+
deviceUpdate.Finish();
160+
device.FinishRenderingCurrentFrame();
161+
162+
// Wait for `startup` to finish.
163+
startup.get_future().wait();
164+
165+
struct Asset
166+
{
167+
const char* Name;
168+
const char* Url;
169+
};
170+
171+
std::array<Asset, 3> assets = {
172+
Asset{"BoomBox", "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoomBox/glTF/BoomBox.gltf"},
173+
Asset{"GlamVelvetSofa", "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/GlamVelvetSofa/glTF/GlamVelvetSofa.gltf"},
174+
Asset{"MaterialsVariantsShoe", "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/MaterialsVariantsShoe/glTF/MaterialsVariantsShoe.gltf"},
175+
};
176+
177+
for (const auto& asset : assets)
178+
{
179+
// Tell RenderDoc to start capturing.
180+
RenderDoc::StartFrameCapture(d3dDevice.get());
181+
182+
// Start rendering a frame to unblock the JavaScript again.
183+
device.StartRenderingCurrentFrame();
184+
deviceUpdate.Start();
185+
186+
std::promise<void> loadAndRenderAsset{};
187+
188+
// Call `loadAndRenderAssetAsync` with the asset URL.
189+
loader.Dispatch([&loadAndRenderAsset, &asset](Napi::Env env) {
190+
std::cout << "Loading " << asset.Name << std::endl;
191+
192+
auto jsPromise = env.Global().Get("loadAndRenderAssetAsync").As<Napi::Function>().Call({Napi::String::From(env, asset.Url)}).As<Napi::Promise>();
193+
194+
auto jsOnFulfilled = Napi::Function::New(env, [&loadAndRenderAsset](const Napi::CallbackInfo&) {
195+
loadAndRenderAsset.set_value();
196+
});
197+
198+
jsPromise = jsPromise.Get("then").As<Napi::Function>().Call(jsPromise, {jsOnFulfilled}).As<Napi::Promise>();
199+
200+
CatchAndLogError(jsPromise);
201+
});
202+
203+
// Wait for the function to complete.
204+
loadAndRenderAsset.get_future().wait();
205+
206+
// Finish rendering the frame.
207+
deviceUpdate.Finish();
208+
device.FinishRenderingCurrentFrame();
209+
210+
// Tell RenderDoc to stop capturing.
211+
RenderDoc::StopFrameCapture(d3dDevice.get());
212+
213+
// Save the texture into an PNG next to the executable.
214+
auto filePath = GetModulePath() / asset.Name;
215+
filePath.concat(".png");
216+
std::cout << "Writing " << filePath.string() << std::endl;
217+
218+
// See https://github.com/Microsoft/DirectXTK/wiki/ScreenGrab#srgb-vs-linear-color-space
219+
winrt::check_hresult(DirectX::SaveWICTextureToFile(d3dDeviceContext.get(), outputTexture.get(), GUID_ContainerFormatPng, filePath.c_str(), nullptr, nullptr, true));
220+
}
221+
222+
return 0;
223+
}

0 commit comments

Comments
 (0)