Appearance
In this approach, you'll code an interactive WebAssembly application that responds to user actions, such as moving sliders on the web page using C++! You'll also enable mouse and keyboard interaction, set up an HTML page with a canvas element, and connect this canvas to a vtkRenderWindow
and vtkRenderWindowInteractor
for rendering and interaction.
Write native code in C++
In the following code, a vtkConeSource
generates polygonal geometry, which is rendered by a vtkPolyDataMapper
. We also wrap the C++ functions void setConeResolution(int)
and void setConeHeight(double)
to JavaScript and connect them to separate sliders that allow adjusting the cone's resolution and height, respectively.
cpp
extern "C" {
EMSCRIPTEN_KEEPALIVE void setConeResolution(int resolution) {
if (coneSource) {
coneSource->SetResolution(resolution);
} else {
std::cerr << "Error: Cone source is not initialized." << std::endl;
}
}
}
cpp
void setConeHeight(double height) {
if (coneSource) {
coneSource->SetHeight(height);
} else {
std::cerr << "Error: Cone source is not initialized." << std::endl;
}
}
void render() {
if (renderWindow) {
renderWindow->Render();
} else {
std::cerr << "Error: Render window is not initialized." << std::endl;
}
}
EMSCRIPTEN_BINDINGS(cone_module) {
emscripten::function("setConeHeight", &setConeHeight);
emscripten::function("render", &render);
}
cpp
int coneResolution = 6;
std::string canvasSelector = "#canvas";
for (int i = 0; i < argc; ++i) {
if (argv[i] && !strcmp(argv[i], "--help")) {
std::cout
<< "Usage: " << argv[0]
<< " [options]\n"
"Options:\n"
" --help: Show this help message\n"
" --resolution: Set the resolution of the cone (default is 6)\n"
" --canvas-selector: Set the selector for canvas (default is "
"#canvas)\n";
return 0;
} else if (argv[i] && !strcmp(argv[i], "--resolution")) {
// Handle resolution option
if (++i < argc) {
coneResolution = atoi(argv[i]);
}
} else if (argv[i] && !strcmp(argv[i], "--canvas-selector")) {
if (++i < argc) {
canvasSelector = argv[i];
}
}
}
cpp
// Create pipeline
coneSource = vtkConeSource::New();
coneSource->SetResolution(coneResolution);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(coneSource->GetOutputPort());
vtkNew<vtkActor> actor;
actor->SetMapper(mapper);
actor->GetProperty()->SetEdgeVisibility(1);
actor->GetProperty()->SetEdgeColor(1, 0, 1);
// Create a renderer, render window, and interactor
vtkNew<vtkRenderer> renderer;
renderer->SetBackground(0.2, 0.3, 0.4);
renderer->AddActor(actor);
renderWindow = vtkRenderWindow::New();
renderWindow->AddRenderer(renderer);
vtkNew<vtkRenderWindowInteractor> renderWindowInteractor;
renderWindowInteractor->SetRenderWindow(renderWindow);
renderer->ResetCamera();
cpp
if (auto *wasmInteractor = vtkWebAssemblyRenderWindowInteractor::SafeDownCast(
renderWindowInteractor)) {
// If using WebAssembly, set the canvas selector
wasmInteractor->SetCanvasSelector(canvasSelector.c_str());
}
// If your canvas has id="canvas", this if block is not needed.
// you can set it here if your canvas has a different id.
if (auto *webGLRenderWindow =
vtkWebAssemblyOpenGLRenderWindow::SafeDownCast(renderWindow)) {
// If using WebAssembly, set the canvas selector
webGLRenderWindow->SetCanvasSelector(canvasSelector.c_str());
} else if (auto *webGPURenderWindow =
vtkWebAssemblyWebGPURenderWindow::SafeDownCast(renderWindow)) {
// If using WebGPU, set the canvas selector
webGPURenderWindow->SetCanvasSelector(canvasSelector.c_str());
} else {
// If not using WebAssembly or WebGPU, print an error message
std::cerr << "Error: Unsupported render window type. "
<< "Please use a WebAssembly or WebGPU render window."
<< std::endl;
return EXIT_FAILURE;
}
cpp
// Clean up the cone source when the interactor is deleted
vtkNew<vtkCallbackCommand> onDeleteCallback;
onDeleteCallback->SetCallback(
[](vtkObject *caller, unsigned long, void *, void *) {
if (coneSource != nullptr) {
coneSource->Delete();
coneSource = nullptr;
}
if (renderWindow != nullptr) {
renderWindow->Delete();
renderWindow = nullptr;
}
});
renderWindowInteractor->AddObserver(vtkCommand::DeleteEvent,
onDeleteCallback);
cpp
// Start event loop
renderWindow->Render();
renderWindowInteractor->Start();
cpp
#include "vtkActor.h"
#include "vtkCallbackCommand.h"
#include "vtkConeSource.h"
#include "vtkNew.h"
#include "vtkPolyData.h"
#include "vtkPolyDataMapper.h"
#include "vtkProperty.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkRenderer.h"
#include "vtkWebAssemblyOpenGLRenderWindow.h"
#include "vtkWebAssemblyRenderWindowInteractor.h"
#include "vtkWebAssemblyWebGPURenderWindow.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
#include <emscripten/bind.h>
namespace {
vtkConeSource *coneSource = nullptr;
vtkRenderWindow *renderWindow = nullptr;
} // namespace
// region emscriptenKeepalive
extern "C" {
EMSCRIPTEN_KEEPALIVE void setConeResolution(int resolution) {
if (coneSource) {
coneSource->SetResolution(resolution);
} else {
std::cerr << "Error: Cone source is not initialized." << std::endl;
}
}
}
// endregion emscriptenKeepalive
// region embindFunctions
void setConeHeight(double height) {
if (coneSource) {
coneSource->SetHeight(height);
} else {
std::cerr << "Error: Cone source is not initialized." << std::endl;
}
}
void render() {
if (renderWindow) {
renderWindow->Render();
} else {
std::cerr << "Error: Render window is not initialized." << std::endl;
}
}
EMSCRIPTEN_BINDINGS(cone_module) {
emscripten::function("setConeHeight", &setConeHeight);
emscripten::function("render", &render);
}
// endregion embindFunctions
int main(int argc, char *argv[]) {
// region args
int coneResolution = 6;
std::string canvasSelector = "#canvas";
for (int i = 0; i < argc; ++i) {
if (argv[i] && !strcmp(argv[i], "--help")) {
std::cout
<< "Usage: " << argv[0]
<< " [options]\n"
"Options:\n"
" --help: Show this help message\n"
" --resolution: Set the resolution of the cone (default is 6)\n"
" --canvas-selector: Set the selector for canvas (default is "
"#canvas)\n";
return 0;
} else if (argv[i] && !strcmp(argv[i], "--resolution")) {
// Handle resolution option
if (++i < argc) {
coneResolution = atoi(argv[i]);
}
} else if (argv[i] && !strcmp(argv[i], "--canvas-selector")) {
if (++i < argc) {
canvasSelector = argv[i];
}
}
}
// endregion args
// region vtk
// Create pipeline
coneSource = vtkConeSource::New();
coneSource->SetResolution(coneResolution);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(coneSource->GetOutputPort());
vtkNew<vtkActor> actor;
actor->SetMapper(mapper);
actor->GetProperty()->SetEdgeVisibility(1);
actor->GetProperty()->SetEdgeColor(1, 0, 1);
// Create a renderer, render window, and interactor
vtkNew<vtkRenderer> renderer;
renderer->SetBackground(0.2, 0.3, 0.4);
renderer->AddActor(actor);
renderWindow = vtkRenderWindow::New();
renderWindow->AddRenderer(renderer);
vtkNew<vtkRenderWindowInteractor> renderWindowInteractor;
renderWindowInteractor->SetRenderWindow(renderWindow);
renderer->ResetCamera();
// endregion vtk
// region bindCanvas
if (auto *wasmInteractor = vtkWebAssemblyRenderWindowInteractor::SafeDownCast(
renderWindowInteractor)) {
// If using WebAssembly, set the canvas selector
wasmInteractor->SetCanvasSelector(canvasSelector.c_str());
}
// If your canvas has id="canvas", this if block is not needed.
// you can set it here if your canvas has a different id.
if (auto *webGLRenderWindow =
vtkWebAssemblyOpenGLRenderWindow::SafeDownCast(renderWindow)) {
// If using WebAssembly, set the canvas selector
webGLRenderWindow->SetCanvasSelector(canvasSelector.c_str());
} else if (auto *webGPURenderWindow =
vtkWebAssemblyWebGPURenderWindow::SafeDownCast(renderWindow)) {
// If using WebGPU, set the canvas selector
webGPURenderWindow->SetCanvasSelector(canvasSelector.c_str());
} else {
// If not using WebAssembly or WebGPU, print an error message
std::cerr << "Error: Unsupported render window type. "
<< "Please use a WebAssembly or WebGPU render window."
<< std::endl;
return EXIT_FAILURE;
}
// endregion bindCanvas
// region coneSourceLifecycle
// Clean up the cone source when the interactor is deleted
vtkNew<vtkCallbackCommand> onDeleteCallback;
onDeleteCallback->SetCallback(
[](vtkObject *caller, unsigned long, void *, void *) {
if (coneSource != nullptr) {
coneSource->Delete();
coneSource = nullptr;
}
if (renderWindow != nullptr) {
renderWindow->Delete();
renderWindow = nullptr;
}
});
renderWindowInteractor->AddObserver(vtkCommand::DeleteEvent,
onDeleteCallback);
// endregion coneSourceLifecycle
// region interactor
// Start event loop
renderWindow->Render();
renderWindowInteractor->Start();
// endregion interactor
return EXIT_SUCCESS;
}
Explaination: The referenced sections from ../../../examples/cpp/cone/main.cpp
demonstrate the following:
Directly expose C functions
Native functions which take or return simple C data types can be wrapped conveniently with an
EMSCRIPTEN_KEEPALIVE
macro. This macro makes the function available in JavaScript with a prefix_
. Here, a C symbolsetConeResolution
becomes_setConeResolution
.WARNING
Symbols that are wrapped using
EMSCRIPTEN_KEEPALIVE
must have C-style linkage (extern "C"
), otherwise the symbol name will be mangled like__Z17setConeResolutioni
which is no fun!Bind C++ functions with embind
You can wrap any C++ function to JavaScript using the
emscripten::function
construct. Member functions of classes can be wrapped withemscripten::class
. Emscripten does the to and fro conversion for methods that take or return complex types likestd::string
. You can learn more about this style of wrapping at emscripten.org/docs.Argument parsing
Command-line arguments allow configuration of the application at startup. This is similar to how you would parse arguments in desktop applications. In WASM, arguments are passed via the
arguments
key to the wasm module object.VTK setup
Initialize the VTK pipeline by creating a cone source, mapper, actor, renderer, and render window.
Bind canvas
Connect the HTML canvas element to the VTK render window. This enables rendering output in the browser.
DANGER
A number of things can go wrong in this step. You will see an error in the console if the binding is performed too late (after a VTK render call), or when the canvas selector has a spelling mistake.
jsvtkWebAssemblyOpenGLRenderWindow (0x1d3c80): Error (0) initializing WebGL2. vtkWebAssemblyOpenGLRenderWindow (0x1d3c80): Failed to create Emscripten opengl context Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'getParameter') at emscriptenWebGLGet (main.js:1:12409402) at _glGetIntegerv (main.js:1:12411165) at 023050d2:0xc1103 at 023050d2:0x4d5c4b at 023050d2:0x4a73b9 at 023050d2:0x17d647 at 023050d2:0x4d3d4a at 023050d2:0x7108c8 at callMain (main.js:1:12453949) at doRun (main.js:1:12454368)
Cleanup globals
We ensure that global objects are cleaned up when the interactor gets deleted by doing cleanup in a callback function that observes the
DeleteEvent
on the render window interactor. This application uses globals so that wrapped functions likesetConeResolution
andrender
can access thevtkConeSource
object andvtkRenderWindow
object respectively.Start interactor
Sets up and starts the VTK render window interactor, enabling mouse and keyboard interaction with the rendered scene.
The full code combines these steps to create a minimal interactive 3D visualization in the browser using VTK, C++, and Embind.
Interface to the web with HTML
The HTML provides a canvas with id
"vtk-wasm-canvas". It also passes the resolution of cone and the canvas selector argument strings to the main wasm program using the arguments
key in the Module
dictionary. The main.js
script is generated by emscripten. It checks whether a variable named Module
is defined in the global scope and imports the configuration like program arguments, canvas, etc from the Module
object. UI callbacks can access the wasm module using the Module
variable.
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div>
<input type="range" min="3" max="128" value="8" onchange="Module._setConeResolution(this.value); Module.render();"
style="position: absolute; top: 1rem; right: 1rem; z-index: 10;" />
<input type="range" min="1.0" max="2.0" value="1.0" step="0.1" onchange="Module.setConeHeight(this.value); Module.render();"
style="position: absolute; top: 1rem; left: 1rem; z-index: 10;" />
</div>
<canvas id="vtk-wasm-canvas" tabindex="-1" onclick="focus()"></canvas>
<script type="text/javascript">
var Module = {
arguments: [
"--resolution", "8",
"--canvas-selector", "#vtk-wasm-canvas"],
canvas: () => {
return document.querySelector("#vtk-wasm-canvas");
},
};
</script>
<script type="text/javascript" src="./main.js"></script>
</body>
</html>
TIP
You can also use the -sMODULARIZE
+ -sEXPORT_NAME=createModule
emscripten link options which offer a different approach to instantiating the wasm runtime. That approach lets you call a function createModule({...options})
that returns a Promise resolving with a handle to the wasm runtime. See emscripten.org/docs for more information.
Build system
Finally, the CMake code creates a WASM executable by compiling the C++ source code and linking with VTK libraries.
cmake
cmake_minimum_required(VERSION 3.29)
project(cone_main LANGUAGES C CXX)
# -----------------------------------------------------------------------------
# EMSCRIPTEN only
# -----------------------------------------------------------------------------
if (NOT EMSCRIPTEN)
message("Skipping example: This needs to run inside an Emscripten build environment")
return ()
endif ()
# -----------------------------------------------------------------------------
# Handle VTK dependency
# -----------------------------------------------------------------------------
find_package(VTK REQUIRED)
if (NOT VTK_FOUND)
message("Skipping example: ${VTK_NOT_FOUND_MESSAGE}")
return ()
endif ()
# -----------------------------------------------------------------------------
# Compile example code
# -----------------------------------------------------------------------------
add_executable(main main.cpp)
target_link_libraries(main PRIVATE ${VTK_LIBRARIES})
# -----------------------------------------------------------------------------
# Optimizations
# -----------------------------------------------------------------------------
set(emscripten_optimizations)
set(emscripten_debug_options)
# Set a default build type if none was specified
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message(STATUS "Setting build type to 'Debug' as none was specified.")
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE)
# Set the possible values of build type for cmake-gui
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
"MinSizeRel" "RelWithDebInfo")
endif()
if (CMAKE_BUILD_TYPE STREQUAL "Release")
set(wasm_optimize "BEST")
set(wasm_debuginfo "NONE")
elseif (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
set(wasm_optimize "SMALLEST_WITH_CLOSURE")
set(wasm_debuginfo "NONE")
elseif (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
set(wasm_optimize "MORE")
set(wasm_debuginfo "PROFILE")
elseif (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(wasm_optimize "NO_OPTIMIZATION")
set(wasm_debuginfo "DEBUG_NATIVE")
endif ()
set(wasm_optimize_NO_OPTIMIZATION "-O0")
set(wasm_optimize_LITTLE "-O1")
set(wasm_optimize_MORE "-O2")
set(wasm_optimize_BEST "-O3")
set(wasm_optimize_SMALLEST "-Os")
set(wasm_optimize_SMALLEST_WITH_CLOSURE "-Oz")
set(wasm_optimize_SMALLEST_WITH_CLOSURE_link "--closure=1")
set(emscripten_link_options
"-lembind"
"-sALLOW_MEMORY_GROWTH=1"
"-sSINGLE_FILE=1")
if (DEFINED "wasm_optimize_${wasm_optimize}")
list(APPEND emscripten_optimizations
${wasm_optimize_${wasm_optimize}})
list(APPEND emscripten_link_options
${wasm_optimize_${wasm_optimize}_link})
else ()
message (FATAL_ERROR "Unrecognized value for wasm_optimize=${wasm_optimize}")
endif ()
set(wasm_debuginfo_NONE "-g0")
set(wasm_debuginfo_READABLE_JS "-g1")
set(wasm_debuginfo_PROFILE "-g2")
set(wasm_debuginfo_DEBUG_NATIVE "-g3")
set(wasm_debuginfo_DEBUG_NATIVE_link "-sASSERTIONS=1")
if (DEFINED "wasm_debuginfo_${wasm_debuginfo}")
list(APPEND emscripten_debug_options
${wasm_debuginfo_${wasm_debuginfo}})
list(APPEND emscripten_link_options
${wasm_debuginfo_${wasm_debuginfo}_link})
else ()
message (FATAL_ERROR "Unrecognized value for wasm_debuginfo=${wasm_debuginfo}")
endif ()
target_compile_options(main
PRIVATE
${emscripten_compile_options}
${emscripten_optimizations}
${emscripten_debug_options})
target_link_options(main
PRIVATE
${emscripten_link_options}
${emscripten_optimizations}
${emscripten_debug_options})
set(CMAKE_NINJA_FORCE_RESPONSE_FILE "ON" CACHE BOOL "Force Ninja to use response files.")
# -----------------------------------------------------------------------------
# VTK modules initialization
# -----------------------------------------------------------------------------
vtk_module_autoinit(
TARGETS main
MODULES ${VTK_LIBRARIES}
)
# Output the main.js file to the docs/public/demo/cpp-app-1 directory
# so that it can be used in the documentation.
set_target_properties(main PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../../../docs/public/demo/cpp-app-1"
)