From f0066f12d440f660386a2362fd058384c8e03d29 Mon Sep 17 00:00:00 2001 From: Jaswant Panchumarti <jaswant.panchumarti@kitware.com> Date: Fri, 14 Feb 2025 13:28:34 -0500 Subject: [PATCH] Add MultipleCanvases in emscripten examples --- .../Cxx/MultipleCanvases/CMakeLists.txt | 70 ++++++++ .../Cxx/MultipleCanvases/MultipleCanvases.cxx | 130 ++++++++++++++ .../Emscripten/Cxx/MultipleCanvases/README.md | 27 +++ .../Cxx/MultipleCanvases/index.html | 158 ++++++++++++++++++ 4 files changed, 385 insertions(+) create mode 100644 Examples/Emscripten/Cxx/MultipleCanvases/CMakeLists.txt create mode 100644 Examples/Emscripten/Cxx/MultipleCanvases/MultipleCanvases.cxx create mode 100644 Examples/Emscripten/Cxx/MultipleCanvases/README.md create mode 100644 Examples/Emscripten/Cxx/MultipleCanvases/index.html diff --git a/Examples/Emscripten/Cxx/MultipleCanvases/CMakeLists.txt b/Examples/Emscripten/Cxx/MultipleCanvases/CMakeLists.txt new file mode 100644 index 00000000000..d9177663b78 --- /dev/null +++ b/Examples/Emscripten/Cxx/MultipleCanvases/CMakeLists.txt @@ -0,0 +1,70 @@ +cmake_minimum_required(VERSION 3.13) +project(MultipleCanvases) + +# ----------------------------------------------------------------------------- +# 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 + COMPONENTS + FiltersSources # VTK pipeline + InteractionStyle # Mouse handling + RenderingOpenGL2 # For Rendering + RenderingUI # For window +) + +if (NOT VTK_FOUND) + message("Skipping example: ${VTK_NOT_FOUND_MESSAGE}") + return () +endif () + +# ----------------------------------------------------------------------------- +# Compile example code +# ----------------------------------------------------------------------------- + +add_executable(MultipleCanvases MultipleCanvases.cxx) +target_link_libraries(MultipleCanvases PRIVATE ${VTK_LIBRARIES}) + +# ----------------------------------------------------------------------------- +# WebAssembly build options +# ----------------------------------------------------------------------------- +set(emscripten_link_options) +list(APPEND emscripten_link_options + "-sALLOW_MEMORY_GROWTH=1" + "-sSINGLE_FILE=1" + "-sEXPORTED_RUNTIME_METHODS=['ENV']" # ENV holds the environment variables accessible by C getenv + "--shell-file=${CMAKE_CURRENT_SOURCE_DIR}/index.html" +) +if (CMAKE_SIZEOF_VOID_P EQUAL "8") + list(APPEND emscripten_link_options + "-sMAXIMUM_MEMORY=16GB") +else () + list(APPEND emscripten_link_options + "-sMAXIMUM_MEMORY=4GB") +endif () +target_link_options(MultipleCanvases + PUBLIC + ${emscripten_link_options} +) +set_target_properties(MultipleCanvases +PROPERTIES + OUTPUT_NAME "index" + SUFFIX ".html") + +# ----------------------------------------------------------------------------- +# VTK modules initialization +# ----------------------------------------------------------------------------- + +vtk_module_autoinit( + TARGETS MultipleCanvases + MODULES ${VTK_LIBRARIES} +) diff --git a/Examples/Emscripten/Cxx/MultipleCanvases/MultipleCanvases.cxx b/Examples/Emscripten/Cxx/MultipleCanvases/MultipleCanvases.cxx new file mode 100644 index 00000000000..0c224a7810d --- /dev/null +++ b/Examples/Emscripten/Cxx/MultipleCanvases/MultipleCanvases.cxx @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause + +#include "vtkActor.h" +#include "vtkArrowSource.h" +#include "vtkCompositePolyDataMapper.h" +#include "vtkConeSource.h" +#include "vtkCubeSource.h" +#include "vtkCylinderSource.h" +#include "vtkDiskSource.h" +#include "vtkPartitionedDataSetCollectionSource.h" +#include "vtkPlatonicSolidSource.h" +#include "vtkRegularPolygonSource.h" +#include "vtkRenderWindow.h" +#include "vtkRenderWindowInteractor.h" +#include "vtkRenderer.h" +#include "vtkSmartPointer.h" +#include "vtkSphereSource.h" +#include "vtkSuperquadricSource.h" +#include "vtkWebAssemblyOpenGLRenderWindow.h" +#include "vtkWebAssemblyRenderWindowInteractor.h" + +#include <emscripten/emscripten.h> + +#include <array> +#include <cstdlib> +#include <iostream> +#include <random> +#include <vector> + +namespace +{ +std::vector<vtkSmartPointer<vtkRenderWindowInteractor>> interactors; +} + +int main(int argc, char* argv[]) +{ + vtkRenderWindowInteractor::InteractorManagesTheEventLoop = false; + if (argc < 2) + { + std::cerr << "Usage: MultipleCanvases <numCanvases>\n"; + } + const int numCanvases = std::atoi(argv[1]); + std::array<vtkSmartPointer<vtkAlgorithm>, 10> sources; + std::size_t i = 0; + sources[i++] = { vtk::TakeSmartPointer(vtkArrowSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkConeSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkCubeSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkCylinderSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkDiskSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkPartitionedDataSetCollectionSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkPlatonicSolidSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkRegularPolygonSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkSphereSource::New()) }; + sources[i++] = { vtk::TakeSmartPointer(vtkSuperquadricSource::New()) }; + + for (int iCanvas = 0; iCanvas < numCanvases; ++iCanvas) + { + const std::string canvasId = "canvas" + std::to_string(iCanvas); + const std::string canvasSelector = "#" + canvasId; + vtkNew<vtkRenderWindow> renderWindow; + vtkNew<vtkRenderWindowInteractor> interactor; + vtkNew<vtkRenderer> renderer; + vtkNew<vtkActor> actor; + vtkNew<vtkCompositePolyDataMapper> mapper; + + auto& source = sources[iCanvas % sources.size()]; + mapper->SetInputConnection(source->GetOutputPort()); + mapper->ScalarVisibilityOff(); + actor->SetMapper(mapper); + renderer->AddActor(actor); + + if (auto* wasmInteractor = vtkWebAssemblyRenderWindowInteractor::SafeDownCast(interactor)) + { + wasmInteractor->SetCanvasSelector(canvasSelector.c_str()); + } + if (auto* wasmGLWindow = vtkWebAssemblyOpenGLRenderWindow::SafeDownCast(renderWindow)) + { + wasmGLWindow->SetCanvasSelector(canvasSelector.c_str()); + } + renderer->SetBackground(0.3, 0.3, 0.3); + renderWindow->AddRenderer(renderer); + interactor->SetRenderWindow(renderWindow); + interactors.emplace_back(interactor); + // clang-format off +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" + MAIN_THREAD_EM_ASM({addCanvas($0, $1, $2); }, canvasId.c_str(), source->GetClassName(), iCanvas); +#pragma clang diagnostic pop + // clang-format on + renderer->ResetCamera(); + renderWindow->Render(); + } + return 0; +} + +extern "C" +{ + EMSCRIPTEN_KEEPALIVE void startEventLoop(int iCanvas) + { + if (static_cast<std::size_t>(iCanvas) < interactors.size()) + { + interactors[iCanvas]->Start(); + } + } + + EMSCRIPTEN_KEEPALIVE void updateSize(int iCanvas, int width, int height) + { + if (static_cast<std::size_t>(iCanvas) < interactors.size()) + { + interactors[iCanvas]->UpdateSize(width, height); + } + } + + EMSCRIPTEN_KEEPALIVE void render(int iCanvas, int width, int height) + { + if (static_cast<std::size_t>(iCanvas) < interactors.size()) + { + interactors[iCanvas]->GetRenderWindow()->Render(); + } + } + + EMSCRIPTEN_KEEPALIVE void stopEventLoop(int iCanvas) + { + if (static_cast<std::size_t>(iCanvas) < interactors.size()) + { + interactors[iCanvas]->TerminateApp(); + } + } +} diff --git a/Examples/Emscripten/Cxx/MultipleCanvases/README.md b/Examples/Emscripten/Cxx/MultipleCanvases/README.md new file mode 100644 index 00000000000..9e55c93c1b0 --- /dev/null +++ b/Examples/Emscripten/Cxx/MultipleCanvases/README.md @@ -0,0 +1,27 @@ +# Build +This example illustrates how to utilize multiple canvases in a single webassembly module. The key functions are: +1. `vtkWebAssemblyRenderWindowInteractor::SetCanvasSelector()` +2. `vtkWebAssemblyOpenGLRenderWindow::SetCanvasSelector()` + +In WebGL, there can be atmost 16 contexts. As a result, the max. +number of canvases are limited. This example does not yet support webgpu, it will need slight +refactoring to make it compatible. + +``` +emcmake cmake \ + -G Ninja \ + -S /path/to/vtk/Examples/Emscripten/Cxx/MultipleCanvases \ + -B out/build \ + -DVTK_DIR=/path/to/where/vtk/wasm/was/built + +cmake --build out/build +``` + +# Serve and test generated code + +``` +cd out/build +python3 -m http.server 8000 +``` + +Open your browser to http://localhost:8000 diff --git a/Examples/Emscripten/Cxx/MultipleCanvases/index.html b/Examples/Emscripten/Cxx/MultipleCanvases/index.html new file mode 100644 index 00000000000..fcf78c90855 --- /dev/null +++ b/Examples/Emscripten/Cxx/MultipleCanvases/index.html @@ -0,0 +1,158 @@ +<!DOCTYPE html> +<html lang="en-us"> + +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + :root { + color-scheme: light dark; + } + + html, + body { + /*remove default margin*/ + margin: 0; + /* make body fill the browser */ + height: 100%; + } + + #outer { + width: 100%; + height: 100%; + display: block; + overflow: auto; + } + + .canvas { + display: inline-block; + padding: 1em; + background: #888; + margin: 1em; + } + + .size0>canvas { + width: 300px; + height: 300px; + } + + .size1>canvas { + width: 400px; + height: 300px; + } + + .size2>canvas { + width: 300px; + height: 400px; + } + + .size3>canvas { + width: 400px; + height: 400px; + } + </style> +</head> + +<body> + <div id="outer"> + </div> + <script> + const numCanvases = 16; + /** + * Lock canvas size for VTK rendering unit tests. These settings make the canvas ignore + * resize events from the parent HTML element. + */ + function lockCanvasSize(wasmRuntime) { + try { + if (typeof wasmRuntime._setDefaultExpandVTKCanvasToContainer !== 'undefined') { + wasmRuntime._setDefaultExpandVTKCanvasToContainer(false); + } + if (typeof wasmRuntime._setDefaultInstallHTMLResizeObserver !== 'undefined') { + wasmRuntime._setDefaultInstallHTMLResizeObserver(false); + } + } catch (_e) {} + } + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const canvas = entry.target; + const width = entry.contentBoxSize[0].inlineSize; + const height = entry.contentBoxSize[0].blockSize; + const scaledWidth = Math.max( + 1, + width * window.devicePixelRatio + 0.5 + ); + const scaledHeight = Math.max( + 1, + height * window.devicePixelRatio + 0.5 + ); + Module._updateSize(canvas.internalIndex, scaledWidth, scaledHeight); + Module._render(canvas.internalIndex); + } + }); + + function rand(min, max) { + if (min === undefined) { + max = 1; + min = 0; + } else if (max === undefined) { + max = min; + min = 0; + } + return Math.random() * (max - min) + min; + } + + function addCanvas(canvasId, sourceName, iCanvas) { + canvasId = maybeCStringToJsString(canvasId); + sourceName = maybeCStringToJsString(sourceName); + const outerElem = document.body.querySelector("#outer"); + // making this + // <div class="product size?"> + // <canvas></canvas> + // <div>Canvas#: ?</div> + // </div> + const newCanvas = document.createElement('canvas'); + newCanvas.id = canvasId; + newCanvas.oncontextmenu = (event) => event.preventDefault(); + newCanvas.tabIndex = -1; + newCanvas.internalIndex = iCanvas; + resizeObserver.observe(newCanvas); + + const container = document.createElement('div'); + container.className = `canvas size${Math.floor(rand(1, 4))}`; + + const description = document.createElement('div'); + description.textContent = `Canvas #: ${iCanvas + 1} ${sourceName}`; + + container.onmouseenter = () => { + Module._startEventLoop(iCanvas); + newCanvas.focus(); + }; + container.onmouseleave = () => { + Module._stopEventLoop(iCanvas); + }; + container.appendChild(newCanvas); + container.appendChild(description); + outerElem.appendChild(container); + setTimeout((i) => Module._render(i), 1000, iCanvas); + } + + var vtkWasmRuntime = null; + var Module = { + arguments: [`${numCanvases}`], + preRun: (runtime) => { + // cache the VTK wasm runtime instance. + vtkWasmRuntime = runtime; + vtkWasmRuntime.ENV.VTK_GRAPHICS_BACKEND = "WEBGPU"; + }, + onRuntimeInitialized: () => { + if (vtkWasmRuntime !== null) { + lockCanvasSize(vtkWasmRuntime); + } + }, + }; + </script> + {{{ SCRIPT }}} +</body> + +</html> -- GitLab