diff --git a/.gitignore b/.gitignore index 142a5560..8cefb6b3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ !/plugins /plugins/* !/plugins/example_plugin +!/plugins/gesture_plugin +!/plugins/storyboard_plugin doc/*.html diff --git a/GesturePlugin-System.png b/GesturePlugin-System.png new file mode 100644 index 00000000..e95b44ef Binary files /dev/null and b/GesturePlugin-System.png differ diff --git a/GesturePlugin-UI.png b/GesturePlugin-UI.png new file mode 100644 index 00000000..a1ba284b Binary files /dev/null and b/GesturePlugin-UI.png differ diff --git a/README.md b/README.md index 66c77de1..4d70b64d 100644 --- a/README.md +++ b/README.md @@ -1,168 +1,185 @@ -# OSPRay Studio +# Immersive OSPRay Studio +> This project is part of a larger project called [Immersive OSPray](https://github.com/jungwhonam/ImmersiveOSPRay). -This is release v0.11.1 of Intel® OSPRay Studio. It is released under the -Apache 2.0 license. +We extend [OSPRay v2.10.0](https://github.com/ospray/ospray/releases/tag/v2.11.0) to display a single, coherent 3D virtual environment on tiled display walls and use gesture-based interaction techniques to navigate the environment. We provide another mode of running the application with the ability to open multiple windows and coordinate these windows (see [MULTIWINDOWS Mode](#multiwindows-mode)). We provide gestured-based interaction techniques by integrating a separate server application sending tracking user data to a plugin to the rendering application (see [Gesture Plugin](#gesture-plugin)). -Visit [**OSPRay Studio**](http://www.ospray.org/ospray_studio) -(http://www.ospray.org/ospray_studio) for more information. - -See [what's -new](https://github.com/ospray/ospray_studio/blob/master/CHANGELOG.md) -in this release. - -## Overview - -Intel OSPRay Studio is an open source and interactive visualization and -ray tracing application that leverages [Intel OSPRay](https://www.ospray.org) -as its core rendering engine. It can be used to load complex scenes requiring -high fidelity rendering or very large scenes requiring supercomputing resources. - -The main control structure is a *scene graph* which allows users to -create an abstract scene in a *directed acyclical graph* manner. Scenes -can either be imported or created using scene graph nodes and structure -support. The scenes can then be rendered either with OSPRay's pathtracer -or scivis renderer. - -More information can be found in the [**high-level feature -description**](https://github.com/ospray/ospray_studio/blob/master/FEATURES.md). - -Building OSPRay Studio -======================== - -CMake Superbuild ----------------- +# Build and Run +## CMake configuration and build +``` +git clone https://github.com/jungwhonam/ospray_studio.git +cd ospray_studio -### Required dependencies for superbuild +git checkout v0.12.0-alpha.x -- [CMake](https://www.cmake.org) (v3.15+) and any C++14 compiler +mkdir build +cd build +mkdir release -For convenience, OSPRay Studio provides a CMake Superbuild script which will -pull down its dependencies i.e. GLFW, OSPRay, rkcommon and TBB. It builds OSPRay -Studio without OpemImageIO and OpenEXR support. `stb_image` is used for all -image operations by default instead. +cmake -S .. \ +-B release \ +-DCMAKE_BUILD_TYPE=Release \ +-DUSE_PYSG=OFF \ +-DUSE_MPI=ON \ +-DBUILD_PLUGINS=ON \ +-DBUILD_PLUGIN_GESTURE=ON \ +-Dospray_DIR="/Users/jnam/Documents/Test/ospray/build/release/install/ospray/lib/cmake/ospray-2.10.0" -To use the superbuild run with: +cmake --build release -- -j 5 -``` sh -mkdir build -cd build -cmake .. -cmake --build . +cmake --install release ``` +OSPRay Studio needs to be built with ```-DUSE_MPI=ON```, ```-DBUILD_PLUGINS=ON```, and ```-BUILD_PLUGIN_GESTURE=ON``` in CMake. Also, we need to use [OSPRay we have customized](https://github.com/jungwhonam/ospray/tree/v2.11.0-alpha.x). After building the OSPRay, set ```ospray_DIR``` so CMake can locate OSPRay, e.g., ```/Users/jnam/Documents/GitHub/ospray/build/install/ospray/lib/cmake/ospray-2.10.0```. -For other full set of options, run: - -``` sh -ccmake .. +## Run the application +``` +mpirun -n 3 \ +./ospStudio \ +multiwindows \ +--mpi \ +--displayConfig config/display_settings.json \ +--scene multilevel_hierarchy \ +--plugin gesture \ +--plugin:gesture:config config/tracking_settings.json ``` -or +```multiwindows```: This option activates our custom mode. -``` sh -cmake-gui .. -``` +```--mpi```: This option enables the OSPRay Studio's built-in MPI support, which is a required dependency of our custom mode. -Standard CMake build --------------------- +````--displayConfig config/display_settings.json````: The JSON configuration file contains information about off-axis projection cameras and windows. Information in the file is used to position and scale windows. See [Display Configuration JSON File](https://github.com/jungwhonam/ConfigurationGenerator#display-configuration-json-file) for details on the JSON file. -For standard cmake process turn off cmake option `OSPRAY_INSTALL` and provide -following required dependencies with their respective cmake options as will be -listed in OS-specific building process below. +```--scene multilevel_hierarchy```: This option starts the application with the scene opened (optional). -### Required dependencies +```--plugin gesture```: This option starts the application with the gesture plugin. -- [CMake](https://www.cmake.org) (v3.15+) and any C++14 compiler -- Intel [OSPRay](https://www.github.com/ospray/ospray) (v2.10.0) and its - dependencies - OSPRay Studio builds on top of OSPRay. Instructions on - building OSPRay are provided - [here](http://www.ospray.org/downloads.html#building-and-finding-ospray). - OSPRay and OSPRay Studio have the following common dependencies which Studio - can hence leverage from an OSPRay build. - - Intel oneAPI Rendering Toolkit common library - [rkcommon](https://www.github.com/ospray/rkcommon) (v1.10.0) - - Intel [Threading Building Blocks](https://www.threadingbuildingblocks.org/) -- OpenGL and [GLFW](https://www.glfw.org) (v3.3.4) - for the windowing environment +```--plugin:gesture:config config/tracking_settings.json```: The JSON configuration file contains information about the gesture tracking server and user tracking data. Gesture Plugin uses this file. See [Implementation details](#implementation-details) for details on the JSON file. +> See [example-config](/example-config/) for example JSON files. -### Optional Dependencies +# MULTIWINDOWS Mode +OSPRay Studio provides different modes of running the application. We added another mode called ```MULTIWINDOWS```; the mode is similar to the default ```GUI``` mode with these additional features: 1) Position and scale windows based on MPI ranks and 2) Synchronize MPI processes. -- Intel [Open Image Denoise](https://openimagedenoise.github.io) - (v1.4.3 or - newer) for denoising frames. To use with OSPRay Studio, OSPRay must be built - with `-DBUILD_OIDN=ON` in CMake. -- [OpenVDB](https://www.openvdb.org/) to support loading VDB formatted volume files. -- [OpenImageIO](http://openimageio.org/) and [OpenEXR](https://www.openexr.com/) - (pre-3.x versions) to support images in a variety of file formats. Set `OPENIMAGEIO_ROOT` - and `OPENEXR_ROOT` to the respective install directories to use these libraries. - (tested with OpenImageIO v2.3.16 and OpenEXR v2.5.8) -- [Python] (3.9.7) (https://python.org) for python bindings +> See ```app/MultiWindows.cpp```. To implement the mode, we copied and modified ```app/MainWindow.cpp```. -### Building on Linux and macOS +### 1. Position and scale windows based on MPI ranks +This new OSPRay Studio mode takes a command line option, ```--displayConfig```, which points to a JSON configuration file that specifies windows and off-axis cameras. At the start of the application, the JSON file is loaded, and values get stored in a JSON object ```nlohmann::ordered_json configDisplay```. Positioning and scaling GLFW windows are done in a constructor. -- Follow OSPRay's build instructions to install it, which will also - fulfill most other required dependencies. Set the following - environment variables to easily locate OSPRay and - rkcommon during CMake. +> See [Display Configuration JSON File](https://github.com/jungwhonam/ConfigurationGenerator#display-configuration-json-file) for details on the JSON file. - +> See ```void MultiWindows::addToCommandLine(std::shared_ptr app)``` for implementation. - ``` bash - export ospray_DIR = ${OSPRAY_INSTALL_LOCATION} - export rkcommon_DIR = ${RKCOMMON_INSTALL_LOCATION} - export TBB_DIR = ${TBB_INSTALL_LOCATION} - ``` +### 2. Synchronize MPI processes +We take additional steps to run these multiple processes in a synchronized fashion. After processing user inputs, the master process updates values in a sharing object. +> Currently we only synchronize a camera location and a closing status across processes. +``` +while (true) { + + ... + + // poll and process events + glfwPollEvents(); + if (sg::sgMpiRank() == 0) { + // poll and process events from the server + for (auto &p : pluginPanels) + p->process("update"); + + // update the shared state + sharedState.camChanged = true; + sharedState.transform = arcballCamera->getTransform(); + sharedState.quit = glfwWindowShouldClose(glfwWindow) || g_quitNextFrame; + } + MPI_Barrier(MPI_COMM_WORLD); +} +``` - Alternatively, [CMAKE_PREFIX_PATH](https://cmake.org/cmake/help/latest/variable/CMAKE_PREFIX_PATH.html) - can be set to find the OSPRay install and other dependencies. +Then, at the beginning of the next frame, the object is broadcast to other processes, and each process updates its objects and application states based on the shared values. -- Clone OSPRay Studio +``` +while (true) { + MPI_Bcast(&sharedState, sizeof(sharedState), MPI_BYTE, 0, MPI_COMM_WORLD); + + { // process changes in the shared state + if (sharedState.quit) { + break; + } + + if (sharedState.camChanged) { + auto camera = frame->child("camera").nodeAs(); + camera->child("transform").setValue(sharedState.transform); + camera->child("topLeft").setValue(xfmPoint(sharedState.transform, topLeftLocal)); + camera->child("botLeft").setValue(xfmPoint(sharedState.transform, botLeftLocal)); + camera->child("botRight").setValue(xfmPoint(sharedState.transform, botRightLocal)); + + sharedState.camChanged = false; + } + } + + ... +} +``` - ``` bash - git clone https://github.com/ospray/ospray_studio/ - ``` +Also, to ensure windows display rendering results simultaneously, processes wait for others to complete the rendering processes before swapping buffers. This is done by calling ```waitOnOSPRayFrame()``` and ```MPI_Barrier(...)``` before ```glfwSwapBuffers(...)```. -- Create build directory and change directory to it (we recommend - keeping a separate build directory) - ``` bash - cd ospray_studio - mkdir build - cd build - ``` -- Then run the typical CMake routine +# Gesture Plugin +
+ + + +
+ + + +
+
- ``` bash - cmake -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang ... # or use ccmake - make -j `nproc` # or cmake --build . - ``` +The plugin handles the connection with [Gesture Tracking Server](https://github.com/jungwhonam/GestureTrackingServer), computes gestures from received data, and keeps track of the latest state. When the plugin receives a message from the server, it derives additional information from the body tracking data. The underlying scene is not updated immediately; OSPRay Studio initiates updating the scene from the latest tracking data. When the application is in the phase of processing user inputs, e.g., key-pressed events, it calls a poll event method from the plugin to get the latest tracking result and uses the result to update corresponding 3D objects, e.g., changing camera locations. -- To run OSPRay Studio, make sure `LD_LIBRARY_PATH` (on Linux) or - `DYLD_LIBRARY_PATH` (on macOS) contains all dependencies. For - example, +> See codes under ```plugins/gesture_plugin/tracker```. - ``` bash - export LD_LIBRARY_PATH=${OSPRAY_INSTALL}/lib64:...:$LD_LIBRARY_PATH - # then run! - ./ospStudio - ``` +## GUI +The plugin panel can be opended by clicking ```Plugins/Gesture Panel``` in the menu. As shown in the left figure, the top pane shows information about the server and has a button for starting and closing the connection. ``Configuration`` pane provides options to modify body tracking data received from the server. ``Save`` button saves the current values to a JSON file (the application reads the values at the start). ``Status`` pane shows important updates, e.g., indicating whether a server is connected. -### Building on Windows +## Implementation details + +At the start, the information about the socket, e.g., IP address and port number, is read from a JSON configuration file. The file also contains information about how to process the user tracking data. -Use CMake (cmake-gui) to configure and generate a Microsoft Visual -Studio solution file for OSPRay Studio. +``` +{ + "ipAddress": "127.0.0.1", + "portNumber": 8888, + + "scaleOffset": [0.001, -0.001, -0.001], + "translationOffset": [0.0, -0.1, 1.19], + "confidenceLevelThreshold": 1, + "leaningAngleThreshold": 1.0, + "leaningDirScaleFactor": [1.0, 1.0, 1.0] +} +``` +- the first two key/value pairs are information about the gesture tracking server. +- ```multiplyBy``` is multiplied to position values of joints. This process is needed as Kinect and OSPRay are in two different coordinate systems. +- ```positionOffset``` are used to offset the sensor's center. The offset is applied to calibrate the sensor and displays. +- ```leaningAngleThreshold``` is a threshhold for activating the flying mode. When a user's body is leaning more than the angle, the flying mode is activated. -- Specify the source folder and the build directory in CMake -- Specify `ospray_DIR`, `rkcommon_DIR` CMake - variables for the respective install locations -- Click 'Configure' and select the appropriate generator (we recommend - using at least Visual Studio 15 2017) -- Select x64 as an optional parameter for the generator (32-bit builds - are not supported) -- Click 'Generate' to create `ospray_studio.sln`. Open this in Visual - Studio and compile + +`async-sockets` receives body tracking data from the server. The implementation is based on [async-sockets](https://github.com/eminfedar/async-sockets-cpp) (using the version from the last commit on 2/21/2022). -You can optionally use the CMake command line: + +```TrackingManager``` manages a socket connection and keeps track of the latest data from the server. In addition to providing methods for starting and closing the connection, the class keeps track of the latest data received from the server. The data can be accessed by calling ```pollState()```. When the method is called, the object that stores the latest information becomes empty, indicating the data has been used. In the plugin, the method is called in ```process(std::string key)```. -``` pwsh -cmake --build . --config Release --target install ``` +void PanelGesture::process(std::string key) { + if (key == "update") { + TrackingState state = trackingManager->pollState(); + if (state.mode == INTERACTION_FLYING) { + context->arcballCamera->move(state.leaningDir); + } + } + else if (key == "start") { + trackingManager->start(); + } +} +``` + +The manager class also figures out current gestures. When a message is received from the server, ```updateState(std::string message)``` is called to process the message. We compute a leaning direction and the current gesture mode in our current implementation. diff --git a/app/ArcballCamera.cpp b/app/ArcballCamera.cpp index 750f9496..df6e07ed 100644 --- a/app/ArcballCamera.cpp +++ b/app/ArcballCamera.cpp @@ -83,6 +83,11 @@ void ArcballCamera::pan(const vec2f &delta) updateCamera(); } +void ArcballCamera::move(const rkcommon::math::vec3f &dir) { + translation = AffineSpace3f::translate(-dir) * translation; + updateCamera(); +} + vec3f ArcballCamera::eyePos() const { return cameraToWorld.p; diff --git a/app/ArcballCamera.h b/app/ArcballCamera.h index e4a8305a..386a8922 100644 --- a/app/ArcballCamera.h +++ b/app/ArcballCamera.h @@ -113,6 +113,7 @@ class ArcballCamera void zoom(float amount); void dolly(float amount); void pan(const vec2f &delta); + void move(const vec3f &dir); vec3f eyePos() const; vec3f center() const; diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 148d5524..92822547 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(ospStudio Batch.cpp TimeSeriesWindow.cpp AnimationManager.cpp + MultiWindows.cpp $<$:Benchmark.cpp> @@ -80,12 +81,26 @@ install(TARGETS ospStudio if(OSPRAY_INSTALL) get_target_property(OSPRAY_LIBNAME ospray::ospray IMPORTED_LOCATION_RELEASE) + if (CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + get_target_property(OSPRAY_LIBNAME ospray::ospray IMPORTED_LOCATION_DEBUG) + endif() string(REGEX MATCH "^.*/" _sharedlib_glob "${OSPRAY_LIBNAME}") string(APPEND _sharedlib_glob "*${CMAKE_SHARED_LIBRARY_SUFFIX}*") # message(STATUS "_sharedlib_glob: ${_sharedlib_glob}") file(GLOB _sharedlibs LIST_DIRECTORIES false "${_sharedlib_glob}") # message(STATUS "_sharedlibs: ${_sharedlibs}") + + # get *_debug*.dylib* files from the tbb's lib directory + if (CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + get_target_property(TBB_LIBNAME TBB::tbb IMPORTED_LOCATION_DEBUG) + string(REGEX MATCH "^.*/" _tbbLib_glob "${TBB_LIBNAME}") + string(APPEND _tbbLib_glob "*_debug*${CMAKE_SHARED_LIBRARY_SUFFIX}*") + file(GLOB tbb_sharedlibs LIST_DIRECTORIES false "${_tbbLib_glob}") + list(APPEND _sharedlibs ${tbb_sharedlibs}) + unset(_tbbLib_glob) + unset(tbb_sharedlibs) + endif() if(WIN32) install(FILES diff --git a/app/MultiWindows.cpp b/app/MultiWindows.cpp new file mode 100644 index 00000000..cfe8d79d --- /dev/null +++ b/app/MultiWindows.cpp @@ -0,0 +1,3092 @@ +// copied MainWindow.cpp and used "devel" namespace + +#include "MultiWindows.h" +// imgui +#include "imgui.h" +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl2.h" +#include "Proggy.h" +// std +#include +#include +#include +// ospray_sg +#include "sg/camera/Camera.h" +#include "sg/exporter/Exporter.h" +#include "sg/fb/FrameBuffer.h" +#include "sg/generator/Generator.h" +#include "sg/renderer/Renderer.h" +#include "sg/scene/World.h" +#include "sg/scene/lights/LightsManager.h" +#include "sg/visitors/Commit.h" +#include "sg/visitors/PrintNodes.h" +#include "sg/visitors/Search.h" +#include "sg/visitors/SetParamByNode.h" +#include "sg/visitors/CollectTransferFunctions.h" +#include "sg/scene/volume/Volume.h" +#include "sg/Math.h" +#include "sg/Mpi.h" +// rkcommon +#include "rkcommon/math/rkmath.h" +#include "rkcommon/os/FileName.h" +#include "rkcommon/utility/SaveImage.h" +#include "rkcommon/utility/getEnvVar.h" +#include "rkcommon/utility/DataView.h" + +// json +#include "sg/JSONDefs.h" + +#include +#include +// widgets +#include "widgets/AdvancedMaterialEditor.h" +#include "widgets/FileBrowserWidget.h" +#include "widgets/ListBoxWidget.h" +#include "widgets/SearchWidget.h" +#include "widgets/TransferFunctionWidget.h" +#include "widgets/PieMenu.h" +#include "widgets/Guizmo.h" + +// CLI +#include + +using namespace ospray_studio; +using namespace ospray; + +namespace devel { + +static ImGuiWindowFlags g_imguiWindowFlags = ImGuiWindowFlags_AlwaysAutoResize; + +static bool g_quitNextFrame = false; +static bool g_saveNextFrame = false; +static bool g_animatingPath = false; +static bool g_clearSceneConfirm = false; + +static const std::vector g_scenes = {"tutorial_scene", + "sphere", + "particle_volume", + "random_spheres", + "wavelet", + "wavelet_slices", + "torus_volume", + "unstructured_volume", + "multilevel_hierarchy"}; + +#ifdef USE_MPI +static const std::vector g_renderers = { + "scivis", "pathtracer", "ao", "debug", "mpiRaycast"}; +#else +static const std::vector g_renderers = { + "scivis", "pathtracer", "ao", "debug"}; +#endif + +// list of cameras imported with the scene definition +static CameraMap g_sceneCameras; + +static const std::vector g_debugRendererTypes = {"eyeLight", + "primID", + "geomID", + "instID", + "Ng", + "Ns", + "backfacing_Ng", + "backfacing_Ns", + "dPds", + "dPdt", + "volume"}; + +static const std::vector g_lightTypes = {"ambient", + "cylinder", + "distant", + "hdri", + "sphere", + "spot", + "sunSky", + "quad"}; + +static +void offaxisStereoCamera(vec3f LL, vec3f LR, vec3f UR, vec3f eye, + vec3f &dirOUT, vec3f &upOUT, + float &fovyOUT, float &aspectOUT, + vec2f &imageStartOUT, vec2f &imageEndOUT) +{ + vec3f X = (LR-LL)/length(LR-LL); + vec3f Y = (UR-LR)/length(UR-LR); + vec3f Z = cross(X,Y); + + dirOUT = -Z; + upOUT = Y; + + // eye position relative to screen/wall + vec3f eyeP = eye-LL; + + // distance from eye to screen/wall + float dist = dot(eyeP,Z); + + float left = dot(eyeP,X); + float right = length(LR-LL)-left; + float bottom = dot(eyeP,Y); + float top = length(UR-LR)-bottom; + + float newWidth = left g_camPath; // interpolated path through cameraStack +int g_camSelectedStackIndex = 0; +int g_camCurrentPathIndex = 0; +float g_camPathSpeed = 5; // defined in hundredths (e.g. 10 = 10 * 0.01 = 0.1) +const int g_camPathPause = 2; // _seconds_ to pause for at end of path +int g_rotationConstraint = -1; + +const double CAM_MOVERATE = + 10.0; // TODO: the constant should be scene dependent or user changeable +double g_camMoveX = 0.0; +double g_camMoveY = 0.0; +double g_camMoveZ = 0.0; +double g_camMoveA = 0.0; +double g_camMoveE = 0.0; +double g_camMoveR = 0.0; + +bool g_syncStates = false; + +float lockAspectRatio = 0.0; + +sg::NodePtr g_copiedMat = nullptr; + +std::string quatToString(quaternionf &q) +{ + std::stringstream ss; + ss << q; + return ss.str(); +} + +bool rendererUI_callback(void *, int index, const char **out_text) +{ + *out_text = g_renderers[index].c_str(); + return true; +} + +bool debugTypeUI_callback(void *, int index, const char **out_text) +{ + *out_text = g_debugRendererTypes[index].c_str(); + return true; +} + +bool lightTypeUI_callback(void *, int index, const char **out_text) +{ + *out_text = g_lightTypes[index].c_str(); + return true; +} + +bool cameraUI_callback(void *, int index, const char **out_text) +{ + static std::string outText; + outText = std::to_string(index) + ": " + g_sceneCameras.at_index(index).first; + *out_text = outText.c_str(); + return true; +} + +bool stringVec_callback(void *data, int index, const char **out_text) +{ + *out_text = ((std::string *)data)[index].c_str(); + return true; +} + +std::string vec3fToString(const vec3f &v) +{ + std::stringstream ss; + ss << v; + return ss.str(); +} + +// MultiWindows definitions /////////////////////////////////////////////// + +void error_callback(int error, const char *desc) +{ + std::cerr << "error " << error << ": " << desc << std::endl; +} + +SharedState::SharedState() : quit(false), camChanged(false) { } + +MultiWindows *MultiWindows::activeWindow = nullptr; + +MultiWindows::MultiWindows(StudioCommon &_common) + : StudioContext(_common, StudioMode::GUI), windowSize(_common.defaultSize), scene("") +{ + parseCommandLine(); + + pluginManager = std::make_shared(); + if (activeWindow != nullptr) { + throw std::runtime_error("Cannot create more than one MultiWindows!"); + } + + // Default saved image baseName (cmdline --image to override) + optImageName = "studio"; + optSPP = 1; // Default SamplesPerPixel in interactive mode is one. + + animationWidget = std::shared_ptr( + new AnimationWidget("Animation Controls", animationManager)); + + activeWindow = this; + + glfwSetErrorCallback(error_callback); + + // initialize GLFW + if (!glfwInit()) { + throw std::runtime_error("Failed to initialize GLFW!"); + } + + // set the window size and show/hide the frame + windowSize.x = configDisplay[sg::sgMpiRank()]["screenWidth"]; + windowSize.y = configDisplay[sg::sgMpiRank()]["screenHeight"]; + optResolution.x = windowSize.x; + optResolution.y = windowSize.y; + glfwWindowHint(GLFW_DECORATED, sg::sgMpiRank() == 0 ? GLFW_TRUE : GLFW_FALSE); + + glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE); + + // get primary monitor's display scaling + GLFWmonitor *primaryMonitor = glfwGetPrimaryMonitor(); + if (primaryMonitor) + glfwGetMonitorContentScale(primaryMonitor, &contentScale.x, &contentScale.y); + + // create GLFW window + glfwWindow = glfwCreateWindow( + windowSize.x, windowSize.y, "OSPRay Studio", nullptr, nullptr); + + if (!glfwWindow) { + glfwTerminate(); + throw std::runtime_error("Failed to create GLFW window!"); + } + + glfwMakeContextCurrent(glfwWindow); + + // Determine whether GL framebuffer has float format + // and set format used by glTexImage2D correctly. + if (glfwGetWindowAttrib(glfwWindow, GLFW_CONTEXT_VERSION_MAJOR) < 3) { + gl_rgb_format = GL_RGB16; + gl_rgba_format = GL_RGBA16; + } else { + gl_rgb_format = GL_RGB32F; + gl_rgba_format = GL_RGBA32F; + } + + // set the window position + int numOfMonitors; + GLFWmonitor** monitors = glfwGetMonitors(&numOfMonitors); + int displayIndex = configDisplay[sg::sgMpiRank()]["display"]; + if (numOfMonitors <= displayIndex) { + throw std::runtime_error("The display index should be less than numOfMonitors: " + std::to_string(numOfMonitors)); + } + int xVirtual, yVirtual; + glfwGetMonitorPos(monitors[displayIndex], &xVirtual, &yVirtual); + int x = (int) configDisplay[sg::sgMpiRank()]["screenX"] + xVirtual; + int y = (int) configDisplay[sg::sgMpiRank()]["screenY"] + yVirtual; + glfwSetWindowPos(glfwWindow, x, y); + + // update three corners of the image plane + { + topLeftLocal = configDisplay[sg::sgMpiRank()]["topLeft"].get(); + botLeftLocal = configDisplay[sg::sgMpiRank()]["botLeft"].get(); + botRightLocal = configDisplay[sg::sgMpiRank()]["botRight"].get(); + vec3f eyePos = configDisplay[sg::sgMpiRank()]["eye"].get(); + vec4f mullion { + configDisplay[sg::sgMpiRank()]["mullionLeft"], + configDisplay[sg::sgMpiRank()]["mullionRight"], + configDisplay[sg::sgMpiRank()]["mullionTop"], + configDisplay[sg::sgMpiRank()]["mullionBottom"]}; + + // use mullion values to update the three corners + vec3f tl = topLeftLocal, bl = botLeftLocal, br = botRightLocal; + + float mullionLeft = mullion[0]; + botLeftLocal += normalize(br - bl) * mullionLeft; + topLeftLocal += normalize(br - bl) * mullionLeft; + + float mullionRight = mullion[1]; + botRightLocal += normalize(bl - br) * mullionRight; + + float mullionTop = mullion[2]; + topLeftLocal += normalize(bl - tl) * mullionTop; + + float mullionBottom = mullion[3]; + botLeftLocal += normalize(tl - bl) * mullionBottom; + botRightLocal += normalize(tl - bl) * mullionBottom; + } + + // further configure GLFW window based on rank + if (sg::sgMpiRank() == 0) { + glfwSetWindowAspectRatio(glfwWindow, windowSize.x, windowSize.y); + + glfwSetKeyCallback( + glfwWindow, [](GLFWwindow *, int key, int, int action, int mod) { + auto &io = ImGui::GetIO(); + if (!io.WantCaptureKeyboard) + if (action == GLFW_PRESS) { + switch (key) { + case GLFW_KEY_UP: + if (mod != GLFW_MOD_ALT) + g_camMoveZ = -CAM_MOVERATE; + else + g_camMoveY = -CAM_MOVERATE; + break; + case GLFW_KEY_DOWN: + if (mod != GLFW_MOD_ALT) + g_camMoveZ = CAM_MOVERATE; + else + g_camMoveY = CAM_MOVERATE; + break; + case GLFW_KEY_LEFT: + g_camMoveX = -CAM_MOVERATE; + break; + case GLFW_KEY_RIGHT: + g_camMoveX = CAM_MOVERATE; + break; + case GLFW_KEY_W: + g_camMoveE = CAM_MOVERATE; + break; + case GLFW_KEY_S: + if (mod != GLFW_MOD_CONTROL) + g_camMoveE = -CAM_MOVERATE; + else + g_saveNextFrame = true; + break; + case GLFW_KEY_A: + if (mod != GLFW_MOD_ALT) + g_camMoveA = -CAM_MOVERATE; + else + g_camMoveR = -CAM_MOVERATE; + break; + case GLFW_KEY_D: + if (mod != GLFW_MOD_ALT) + g_camMoveA = CAM_MOVERATE; + else + g_camMoveR = CAM_MOVERATE; + break; + + case GLFW_KEY_X: + g_rotationConstraint = 0; + break; + case GLFW_KEY_Y: + g_rotationConstraint = 1; + break; + case GLFW_KEY_Z: + g_rotationConstraint = 2; + break; + + case GLFW_KEY_I: + activeWindow->centerOnEyePos(); + break; + + case GLFW_KEY_G: + activeWindow->showUi = !(activeWindow->showUi); + break; + case GLFW_KEY_Q: { + auto showMode = + rkcommon::utility::getEnvVar("OSPSTUDIO_SHOW_MODE"); + // Enforce "ctrl-Q" to make it more difficult to exit by mistake. + if (showMode && mod != GLFW_MOD_CONTROL) + std::cout << "Use ctrl-Q to exit\n"; + else + g_quitNextFrame = true; + } break; + case GLFW_KEY_P: + activeWindow->frame->traverse(); + break; + case GLFW_KEY_M: + activeWindow->baseMaterialRegistry->traverse(); + break; + case GLFW_KEY_L: + activeWindow->lightsManager->traverse(); + break; + case GLFW_KEY_B: + PRINT(activeWindow->frame->bounds()); + break; + case GLFW_KEY_V: + activeWindow->frame->child("camera").traverse(); + break; + case GLFW_KEY_R: + g_syncStates = true; + break; + case GLFW_KEY_SPACE: + if (activeWindow->cameraStack.size() >= 2) { + g_animatingPath = !g_animatingPath; + g_camCurrentPathIndex = 0; + if (g_animatingPath) { + g_camPath = buildPath( + activeWindow->cameraStack, g_camPathSpeed * 0.01); + } + } + activeWindow->animationWidget->togglePlay(); + break; + case GLFW_KEY_EQUAL: + activeWindow->pushLookMark(); + break; + case GLFW_KEY_MINUS: + activeWindow->popLookMark(); + break; + case GLFW_KEY_0: /* fallthrough */ + case GLFW_KEY_1: /* fallthrough */ + case GLFW_KEY_2: /* fallthrough */ + case GLFW_KEY_3: /* fallthrough */ + case GLFW_KEY_4: /* fallthrough */ + case GLFW_KEY_5: /* fallthrough */ + case GLFW_KEY_6: /* fallthrough */ + case GLFW_KEY_7: /* fallthrough */ + case GLFW_KEY_8: /* fallthrough */ + case GLFW_KEY_9: + activeWindow->setCameraSnapshot((key + 9 - GLFW_KEY_0) % 10); + break; + } + } + if (action == GLFW_RELEASE) { + switch (key) { + case GLFW_KEY_X: + case GLFW_KEY_Y: + case GLFW_KEY_Z: + g_rotationConstraint = -1; + break; + case GLFW_KEY_UP: + case GLFW_KEY_DOWN: + g_camMoveZ = 0; + g_camMoveY = 0; + break; + case GLFW_KEY_LEFT: + case GLFW_KEY_RIGHT: + g_camMoveX = 0; + break; + case GLFW_KEY_W: + case GLFW_KEY_S: + g_camMoveE = 0; + break; + case GLFW_KEY_A: + case GLFW_KEY_D: + g_camMoveA = 0; + g_camMoveR = 0; + break; + case GLFW_KEY_C: + for (auto &p : activeWindow->pluginPanels) + p->process("start"); + break; + } + } + }); + + // set GLFW callbacks + glfwSetFramebufferSizeCallback( + glfwWindow, [](GLFWwindow *, int newWidth, int newHeight) { + activeWindow->reshape(vec2i{newWidth, newHeight}); + }); + + glfwSetMouseButtonCallback(glfwWindow, [](GLFWwindow *win, int, int action, int) { + ImGuiIO &io = ImGui::GetIO(); + if (!activeWindow->showUi || !io.WantCaptureMouse) { + double x, y; + glfwGetCursorPos(win, &x, &y); + activeWindow->mouseButton(vec2f{float(x), float(y)}); + + activeWindow->getFrame()->setNavMode(action == GLFW_PRESS); + } + }); + + glfwSetScrollCallback(glfwWindow, [](GLFWwindow *, double x, double y) { + ImGuiIO &io = ImGui::GetIO(); + if (!activeWindow->showUi || !io.WantCaptureMouse) { + activeWindow->mouseWheel(vec2f{float(x), float(y)}); + } + }); + + glfwSetCursorPosCallback(glfwWindow, [](GLFWwindow *, double x, double y) { + ImGuiIO &io = ImGui::GetIO(); + if (!activeWindow->showUi || !io.WantCaptureMouse) { + activeWindow->motion(vec2f{float(x), float(y)}); + } + }); + } + + ImGui::CreateContext(); + + // Enable context for ImGui experimental viewports + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; + + ImGui_ImplGlfw_InitForOpenGL(glfwWindow, true); + ImGui_ImplOpenGL2_Init(); + + // Disable active viewports until users enables toggled in view menu + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_ViewportsEnable; + + // set ImGui font, scaled to display DPI + auto &io = ImGui::GetIO(); + auto scaleFactor = std::max(contentScale.x, contentScale.y); + auto scaledFontSize = fontSize * scaleFactor; + ImVec2 imScale(contentScale.x, contentScale.y); + + ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF( + ProggyClean_compressed_data, ProggyClean_compressed_size, scaledFontSize); + io.FontGlobalScale = 1.f / scaleFactor; + io.DisplayFramebufferScale = imScale; + + // set initial OpenGL state + glEnable(GL_TEXTURE_2D); + glDisable(GL_LIGHTING); + + // create OpenGL frame buffer texture + glGenTextures(1, &framebufferTexture); + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, framebufferTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + refreshScene(true); + + // set the initial state + sharedState.camChanged = false; + sharedState.camState = arcballCamera->getState(); + sharedState.sceneChanged = false; + sharedState.sceneStateSize = 0; + sharedState.sceneState = ""; + showUi = sg::sgMpiRank() == 0; + + // trigger window reshape events with current window size + glfwGetFramebufferSize(glfwWindow, &windowSize.x, &windowSize.y); + reshape(windowSize); +} + +MultiWindows::~MultiWindows() +{ + ImGui_ImplOpenGL2_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + glfwTerminate(); + pluginManager->removeAllPlugins(); + g_sceneCameras.clear(); + g_copiedMat = nullptr; + sg::clearAssets(); +} + +void MultiWindows::start() +{ + std::cerr << "MULTIWINDOWS mode\n"; + + // load plugins // + for (auto &p : studioCommon.pluginsToLoad) + pluginManager->loadPlugin(p); + + // create panels // + // doing this outside constructor to ensure shared_from_this() + // can wrap a valid weak_ptr (in constructor, not guaranteed) + + pluginManager->main(shared_from_this(), &pluginPanels); + // std::move(newPluginPanels.begin(), + // newPluginPanels.end(), + // std::back_inserter(pluginPanels)); + + std::ifstream cams("cams.json"); + if (cams) { + JSON j; + cams >> j; + cameraStack = j.get>(); + } + + if (parseCommandLine()) { + refreshRenderer(); + refreshScene(true); + mainLoop(); + } +} + +MultiWindows *MultiWindows::getActiveWindow() +{ + return activeWindow; +} + +std::shared_ptr MultiWindows::getFrame() +{ + return frame; +} + +void MultiWindows::registerDisplayCallback( + std::function callback) +{ + displayCallback = callback; +} + +void MultiWindows::registerKeyCallback(std::function callback) +{ + keyCallback = callback; +} + +void MultiWindows::registerImGuiCallback(std::function callback) +{ + uiCallback = callback; +} + +void MultiWindows::mainLoop() +{ + // continue until the user closes the window + startNewOSPRayFrame(); + + while (true) { + { // process changes in the shared state + MPI_Bcast(&sharedState.quit, 1, MPI_CXX_BOOL, 0, MPI_COMM_WORLD); + if (sharedState.quit) { + break; + } + + // sync the scene + MPI_Bcast(&sharedState.sceneChanged, 1, MPI_CXX_BOOL, 0, MPI_COMM_WORLD); + if (sharedState.sceneChanged) { + MPI_Bcast(&sharedState.sceneStateSize, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (sg::sgMpiRank() != 0) { + sharedState.sceneState.resize(sharedState.sceneStateSize); + } + + MPI_Bcast(const_cast(sharedState.sceneState.c_str()), sharedState.sceneStateSize, MPI_CHAR, 0, MPI_COMM_WORLD); + if (sg::sgMpiRank() != 0) { + try { + clearScene(); + sg::importScene(shared_from_this(), sharedState.sceneState); + // arcballCamera->setState(sharedState.camState); + // updateCamera(); + } catch (const std::exception &e) { + std::cerr << "Failed to sync the scene: '" << e.what() << "'!\n"; + std::cerr << " " << e.what() << std::endl; + } catch (...) { + std::cerr << "Failed to sync the scene!\n"; + } + } + + sharedState.sceneStateSize = 0; + sharedState.sceneState = ""; + sharedState.sceneChanged = false; + } + + // sync the camera + MPI_Bcast(&sharedState.camChanged, 1, MPI_CXX_BOOL, 0, MPI_COMM_WORLD); + if (sharedState.camChanged) { + MPI_Bcast(&sharedState.camState, sizeof(sharedState.camState), MPI_BYTE, 0, MPI_COMM_WORLD); + arcballCamera->setState(sharedState.camState); + updateCamera(); + sharedState.camChanged = false; + } + } + + ImGui_ImplOpenGL2_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + bool hasPausedRendering = false; + size_t numTasksExecuted; + do { + numTasksExecuted = 0; + + if (optDoAsyncTasking) { + numTasksExecuted += scheduler->background()->executeAllTasksAsync(); + } else { + numTasksExecuted += scheduler->background()->executeAllTasksSync(); + } + + numTasksExecuted += scheduler->studio()->executeAllTasksSync(); + + auto task = scheduler->ospray()->pop(); + if (task) { + if (!hasPausedRendering) { + hasPausedRendering = true; + + // if a task wants to modify ospray properties, then we need to cancel any + // currently rendering frame to make sure we don't get any segfaults + frame->pauseRendering = true; + frame->cancelFrame(); + frame->waitOnFrame(); + } + + numTasksExecuted += scheduler->ospray()->executeAllTasksSync(task); + } + } while (numTasksExecuted > 0); + + if (hasPausedRendering) { + hasPausedRendering = false; + + // after running ospray tasks, make sure to re-enable rendering and update + // the scene and camera for any newly added or modified objects in the + // scene + frame->pauseRendering = false; + refreshScene(true); + } + + display(); + + // poll and process events + glfwPollEvents(); + if (sg::sgMpiRank() == 0) { + for (auto &p : pluginPanels) + p->process("update"); + + sharedState.quit = glfwWindowShouldClose(glfwWindow) || g_quitNextFrame; + + sharedState.camChanged = true; + sharedState.camState = arcballCamera->getState(); + + if (g_syncStates) { + sharedState.sceneChanged = true; + sharedState.sceneState = getSceneState().dump(); + sharedState.sceneStateSize = sharedState.sceneState.size(); + g_syncStates = false; + } + } + MPI_Barrier(MPI_COMM_WORLD); + } + + waitOnOSPRayFrame(); +} + +void MultiWindows::reshape(const vec2i &newWindowSize) +{ + windowSize = newWindowSize; + vec2i fSize = windowSize; + if (lockAspectRatio) { + // Tell OSPRay to render the largest subset of the window that satisies the + // aspect ratio + float aspectCorrection = lockAspectRatio + * static_cast(newWindowSize.y) + / static_cast(newWindowSize.x); + if (aspectCorrection > 1.f) { + fSize.y /= aspectCorrection; + } else { + fSize.x *= aspectCorrection; + } + } + if (frame->child("camera").hasChild("aspect")) + frame->child("camera")["aspect"] = static_cast(fSize.x) / fSize.y; + + frame->child("windowSize") = fSize; + frame->currentAccum = 0; + + // reset OpenGL viewport and orthographic projection + glViewport(0, 0, windowSize.x, windowSize.y); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0.0, windowSize.x, 0.0, windowSize.y, -1.0, 1.0); + + // update camera + arcballCamera->updateWindowSize(windowSize); +} + +void MultiWindows::updateCamera() +{ + frame->currentAccum = 0; + auto camera = frame->child("camera").nodeAs(); + + if (cameraIdx || optCameraRange.lower) { + // switch to index/cameraRange specific scene camera + if (optCameraRange.lower) { + auto &newCamera = g_sceneCameras.at_index(optCameraRange.lower); + g_selectedSceneCamera = newCamera.second; + } else if (cameraIdx) { + auto &newCamera = g_sceneCameras.at_index(cameraIdx); + g_selectedSceneCamera = newCamera.second; + } + frame->remove("camera"); + frame->add(g_selectedSceneCamera); + // update camera pointer + camera = frame->child("camera").nodeAs(); + if (g_selectedSceneCamera->hasChild("aspect")) + lockAspectRatio = g_selectedSceneCamera->child("aspect").valueAs(); + reshape(windowSize); // resets aspect + cameraIdx = 0; // reset global-context cameraIndex + arcballCamera->updateCameraToWorld(affine3f{one}, one); + cameraView = nullptr; // only used for arcball/default + } else if (cameraView && *cameraView != affine3f{one}) { + // use camera settings from scene camera + if (cameraSettingsIdx) { + auto settingsCamera = g_sceneCameras.at_index(cameraSettingsIdx).second; + for (auto &c : settingsCamera->children()) { + if (c.first == "cameraId") { + camera->createChild("cameraSettingsId", "int", c.second->value()); + camera->child("cameraSettingsId").setSGNoUI(); + camera->child("cameraSettingsId").setSGOnly(); + } else if (c.first != "uniqueCameraName") { + if (camera->hasChild(c.first)) + camera->child(c.first) = c.second->value(); + else { + camera->createChild( + c.first, c.second->subType(), c.second->value()); + if (settingsCamera->child(c.first).sgOnly()) + camera->child(c.first).setSGOnly(); + } + } + } + if (settingsCamera->hasChild("aspect")) + lockAspectRatio = settingsCamera->child("aspect").valueAs(); + reshape(windowSize); // resets aspect + } + + auto worldToCamera = rcp(*cameraView); + LinearSpace3f R, S; + ospray::sg::getRSComponent(worldToCamera, R, S); + auto rotation = ospray::sg::getRotationQuaternion(R); + + arcballCamera->updateCameraToWorld(*cameraView, rotation); + cameraView = nullptr; + activeWindow->centerOnEyePos(); + } + + if (camera->hasChild("focusDistance") + && !camera->child("cameraId").valueAs()) { + float focusDistance = rkcommon::math::length( + camera->child("lookAt").valueAs() - arcballCamera->eyePos()); + if (camera->child("adjustAperture").valueAs()) { + float oldFocusDistance = camera->child("focusDistance").valueAs(); + if (!(isinf(oldFocusDistance) || isinf(focusDistance))) { + float apertureRadius = camera->child("apertureRadius").valueAs(); + camera->child("apertureRadius") + .setValue(apertureRadius * focusDistance / oldFocusDistance); + } + } + camera->child("focusDistance").setValue(focusDistance); + } + + { + affine3f t = arcballCamera->getTransform(); + vec3f tl = xfmPoint(t, topLeftLocal); + vec3f bl = xfmPoint(t, botLeftLocal); + vec3f br = xfmPoint(t, botRightLocal); + vec3f tr = (tl - bl) + br; + + vec3f eye = t.p; + vec3f dir, up; + float fovy, aspect; + vec2f imgStart, imgEnd; + + // LL, LR, UR + offaxisStereoCamera(bl, br, tr, eye, dir, up, fovy, aspect, imgStart, imgEnd); + + camera->child("fovy").setValue(fovy); + camera->child("aspect").setValue(aspect); + camera->child("position").setValue(eye); + camera->child("direction").setValue(dir); + camera->child("up").setValue(up); + camera->child("imageStart").setValue(imgStart); + camera->child("imageEnd").setValue(imgEnd); + } +} + +void MultiWindows::setCameraState(CameraState &cs) +{ + arcballCamera->setState(cs); +} + +void MultiWindows::centerOnEyePos() +{ + // Recenters camera at the eye position and zooms all the way in, like FPV + // Save current zoom level + preFPVZoom = arcballCamera->getZoomLevel(); + arcballCamera->setCenter(arcballCamera->eyePos()); + arcballCamera->setZoomLevel(0.f); +} + +void MultiWindows::pickCenterOfRotation(float x, float y) +{ + ospray::cpp::PickResult res; + auto &fb = frame->childAs("framebuffer"); + auto &r = frame->childAs("renderer"); + auto &c = frame->childAs("camera"); + auto &w = frame->childAs("world"); + + x = clamp(x / windowSize.x, 0.f, 1.f); + y = 1.f - clamp(y / windowSize.y, 0.f, 1.f); + res = fb.handle().pick(r, c, w, x, y); + if (res.hasHit) { + if (!(glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS)) { + // Constraining rotation around the up works pretty well. + arcballCamera->constrainedRotate(vec2f(0.5f,0.5f), vec2f(x,y), 1); + // Restore any preFPV zoom level, then clear it. + arcballCamera->setZoomLevel(preFPVZoom + arcballCamera->getZoomLevel()); + preFPVZoom = 0.f; + arcballCamera->setCenter(vec3f(res.worldPosition)); + } + c["lookAt"] = vec3f(res.worldPosition); + updateCamera(); + } +} + +void MultiWindows::keyboardMotion() +{ + if (!(g_camMoveX || g_camMoveY || g_camMoveZ || g_camMoveE || g_camMoveA + || g_camMoveR)) + return; + + auto sensitivity = maxMoveSpeed; + if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + sensitivity *= fineControl; + + // 6 degrees of freedom, four arrow keys? no problem. + double inOut = g_camMoveZ; + double leftRight = g_camMoveX; + double upDown = g_camMoveY; + double roll = g_camMoveR; + double elevation = g_camMoveE; + double azimuth = g_camMoveA; + + if (inOut) { + arcballCamera->dolly(inOut * sensitivity); + } + if (leftRight) { + arcballCamera->pan(vec2f(leftRight, 0) * sensitivity); + } + if (upDown) { + arcballCamera->pan(vec2f(0, upDown) * sensitivity); + } + if (elevation) { + arcballCamera->constrainedRotate( + vec2f(-.5, 0), vec2f(-.5, elevation * .005 * sensitivity), 0); + } + if (azimuth) { + arcballCamera->constrainedRotate( + vec2f(0, -.5), vec2f(azimuth * .005 * sensitivity, -0.5), 1); + } + if (roll) { + arcballCamera->constrainedRotate( + vec2f(-.5, 0), vec2f(-.5, roll * .005 * sensitivity), 2); + } + updateCamera(); +} + +void MultiWindows::changeToDefaultCamera() +{ + auto defaultCamera = g_sceneCameras["default"]; + auto sgSceneCamera = frame->child("camera").nodeAs(); + + for (auto &c : sgSceneCamera->children()) { + if (c.first == "cameraId") { + defaultCamera->createChild("cameraSettingsId", "int", c.second->value()); + defaultCamera->child("cameraSettingsId").setSGNoUI(); + defaultCamera->child("cameraSettingsId").setSGOnly(); + } else if (c.first != "uniqueCameraName") { + if (defaultCamera->hasChild(c.first)) + defaultCamera->child(c.first) = c.second->value(); + else { + defaultCamera->createChild( + c.first, c.second->subType(), c.second->value()); + if (sgSceneCamera->child(c.first).sgOnly()) + defaultCamera->child(c.first).setSGOnly(); + } + } + } + + frame->remove("camera"); + frame->add(defaultCamera); + frame->commit(); + + auto worldToCamera = rcp(sgSceneCamera->cameraToWorld); + LinearSpace3f R, S; + ospray::sg::getRSComponent(worldToCamera, R, S); + auto rotation = ospray::sg::getRotationQuaternion(R); + + arcballCamera->updateCameraToWorld(sgSceneCamera->cameraToWorld, rotation); + activeWindow->centerOnEyePos(); + updateCamera(); // to reflect new default camera properties in GUI +} + +void MultiWindows::motion(const vec2f &position) +{ + if (frame->pauseRendering) + return; + + const vec2f mouse = position * contentScale; + if (previousMouse != vec2f(-1)) { + const bool leftDown = + glfwGetMouseButton(glfwWindow, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; + const bool rightDown = + glfwGetMouseButton(glfwWindow, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS; + const bool middleDown = + glfwGetMouseButton(glfwWindow, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS; + const vec2f prev = previousMouse; + + bool cameraChanged = leftDown || rightDown || middleDown; + + // if cameraChanged then switch back to default-camera and use current scene + // SG camera state + if (cameraChanged && frame->child("camera").child("uniqueCameraName").valueAs() + != "default") + changeToDefaultCamera(); + + auto sensitivity = maxMoveSpeed; + if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + sensitivity *= fineControl; + + auto displaySize = windowSize * contentScale; + + const vec2f mouseFrom(clamp(prev.x * 2.f / displaySize.x - 1.f, -1.f, 1.f), + clamp(prev.y * 2.f / displaySize.y - 1.f, -1.f, 1.f)); + const vec2f mouseTo(clamp(mouse.x * 2.f / displaySize.x - 1.f, -1.f, 1.f), + clamp(mouse.y * 2.f / displaySize.y - 1.f, -1.f, 1.f)); + + if (leftDown) { + arcballCamera->constrainedRotate(mouseFrom, + lerp(sensitivity, mouseFrom, mouseTo), + g_rotationConstraint); + } else if (rightDown) { + if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) + arcballCamera->dolly((mouseTo - mouseFrom).y * sensitivity); + else + arcballCamera->zoom((mouseTo - mouseFrom).y * sensitivity); + } else if (middleDown) { + arcballCamera->pan((mouseTo - mouseFrom) * sensitivity); + } + + if (cameraChanged) + updateCamera(); + } + + previousMouse = mouse; +} + +void MultiWindows::mouseButton(const vec2f &position) +{ + if (frame->pauseRendering) + return; + + if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS + && glfwGetMouseButton(glfwWindow, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { + // when picking new center of rotation change to default camera first + if (frame->child("camera").child("uniqueCameraName").valueAs() + != "default") + changeToDefaultCamera(); + + vec2f scaledPosition = position * contentScale; + pickCenterOfRotation(scaledPosition.x, scaledPosition.y); + } +} + +void MultiWindows::mouseWheel(const vec2f &scroll) +{ + if (!scroll || frame->pauseRendering) + return; + + // scroll is +/- 1 for horizontal/vertical mouse-wheel motion + + auto sensitivity = maxMoveSpeed; + if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + sensitivity *= fineControl; + + if (scroll.y) { + auto &camera = frame->child("camera"); + if (camera.hasChild("fovy")) { + auto fovy = camera["fovy"].valueAs(); + fovy = std::min(180.f, std::max(0.f, fovy + scroll.y * sensitivity)); + camera["fovy"] = fovy; + updateCamera(); + } + } + + // XXX anything interesting to do with horizontal scroll wheel? + // Perhaps cycle through renderer types? Or toggle denoiser or navigation? +} + +void MultiWindows::display() +{ + static auto displayStart = std::chrono::high_resolution_clock::now(); + + if (optAutorotate) { + vec2f from(0.f, 0.f); + vec2f to(autorotateSpeed * 0.001f, 0.f); + arcballCamera->rotate(from, to); + updateCamera(); + } + + // Update animation controller if playing + if (animationWidget->isPlaying()) { + animationWidget->update(); + // use scene camera while playing animation + if (frame->child("camera").child("uniqueCameraName").valueAs() + == "default" && g_selectedSceneCamera) { + frame->remove("camera"); + frame->add(g_selectedSceneCamera); + } + } + + if (g_animatingPath) { + static int framesPaused = 0; + CameraState current = g_camPath[g_camCurrentPathIndex]; + arcballCamera->setState(current); + updateCamera(); + + // pause at the end of the path + if (g_camCurrentPathIndex == (int) g_camPath.size() - 1) { + framesPaused++; + int framesToWait = g_camPathPause * ImGui::GetIO().Framerate; + if (framesPaused > framesToWait) { + framesPaused = 0; + g_camCurrentPathIndex = 0; + } + } else { + g_camCurrentPathIndex++; + } + } + + keyboardMotion(); + + if (displayCallback) + displayCallback(this); + + updateTitleBar(); + + auto &frameBuffer = frame->childAs("framebuffer"); + fbSize = frameBuffer.child("size").valueAs(); + + waitOnOSPRayFrame(); + + if (frame->frameIsReady()) { + if (!frame->isCanceled()) { + // display frame rate in window title + auto displayEnd = std::chrono::high_resolution_clock::now(); + auto durationMilliseconds = + std::chrono::duration_cast( + displayEnd - displayStart); + + latestFPS = 1000.f / float(durationMilliseconds.count()); + + // map OSPRay framebuffer, update OpenGL texture with contents, then unmap + waitOnOSPRayFrame(); + + // Only enabled if they exist + optShowAlbedo &= frameBuffer.hasAlbedoChannel(); + optShowDepth &= frameBuffer.hasDepthChannel(); + + auto *mappedFB = (void *)frame->mapFrame(optShowDepth + ? OSP_FB_DEPTH + : (optShowAlbedo ? OSP_FB_ALBEDO : OSP_FB_COLOR)); + + // This needs to query the actual framebuffer format + const GLenum glType = + frameBuffer.isFloatFormat() ? GL_FLOAT : GL_UNSIGNED_BYTE; + + // Only create the copy if it's needed + float *pDepthCopy = nullptr; + if (optShowDepth) { + // Create a local copy and don't modify OSPRay buffer + const auto *mappedDepth = static_cast(mappedFB); + std::vector depthCopy( + mappedDepth, mappedDepth + fbSize.x * fbSize.y); + pDepthCopy = depthCopy.data(); + + // Scale OSPRay's 0 -> inf depth range to OpenGL 0 -> 1, ignoring all + // inf values + float minDepth = rkcommon::math::inf; + float maxDepth = rkcommon::math::neg_inf; + for (auto depth : depthCopy) { + if (isinf(depth)) + continue; + minDepth = std::min(minDepth, depth); + maxDepth = std::max(maxDepth, depth); + } + + const float rcpDepthRange = 1.f / (maxDepth - minDepth); + + // Inverted depth (1.0 -> 0.0) may be more meaningful + if (optShowDepthInvert) + std::transform(depthCopy.begin(), + depthCopy.end(), + depthCopy.begin(), + [&](float depth) { + return (1.f - (depth - minDepth) * rcpDepthRange); + }); + else + std::transform(depthCopy.begin(), + depthCopy.end(), + depthCopy.begin(), + [&](float depth) { return (depth - minDepth) * rcpDepthRange; }); + } + + glBindTexture(GL_TEXTURE_2D, framebufferTexture); + glTexImage2D(GL_TEXTURE_2D, + 0, + optShowAlbedo ? gl_rgb_format : gl_rgba_format, + fbSize.x, + fbSize.y, + 0, + optShowDepth ? GL_LUMINANCE : (optShowAlbedo ? GL_RGB : GL_RGBA), + glType, + optShowDepth ? pDepthCopy : mappedFB); + + frame->unmapFrame(mappedFB); + + // save frame to a file, if requested + if (g_saveNextFrame) { + saveCurrentFrame(); + g_saveNextFrame = false; + } + } + + // Start new frame and reset frame timing interval start + displayStart = std::chrono::high_resolution_clock::now(); + startNewOSPRayFrame(); + } + + // Allow OpenGL to show linear buffers as sRGB. + if (uiDisplays_sRGB && !frameBuffer.isSRGB()) + glEnable(GL_FRAMEBUFFER_SRGB); + + // clear current OpenGL color buffer + glClear(GL_COLOR_BUFFER_BIT); + + // render textured quad with OSPRay frame buffer contents + vec2f border(0.f); + if (lockAspectRatio) { + // when rendered aspect ratio doesn't match window, compute texture + // coordinates to center the display + float aspectCorrection = lockAspectRatio * static_cast(windowSize.y) + / static_cast(windowSize.x); + if (aspectCorrection > 1.f) { + border.y = 1.f - aspectCorrection; + } else { + border.x = 1.f - 1.f / aspectCorrection; + } + } + border *= 0.5f; + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + + glBegin(GL_QUADS); + + glTexCoord2f(border.x, border.y); + glVertex2f(0, 0); + + glTexCoord2f(border.x, 1.f - border.y); + glVertex2f(0, windowSize.y); + + glTexCoord2f(1.f - border.x, 1.f - border.y); + glVertex2f(windowSize.x, windowSize.y); + + glTexCoord2f(1.f - border.x, border.y); + glVertex2f(windowSize.x, 0); + + glEnd(); + + glDisable(GL_FRAMEBUFFER_SRGB); + + if (showUi) { + // Notify ImGui of the colorspace for color picker widgets + // (to match the colorspace of the framebuffer) + if (uiDisplays_sRGB || frameBuffer.isSRGB()) + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_IsSRGB; + + buildUI(); + ImGui::Render(); + ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData()); + + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_IsSRGB; + } else { + ImGui::EndFrame(); + } + + // Update and Render additional Platform Windows + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + GLFWwindow *backup_current_context = glfwGetCurrentContext(); + ImGui::UpdatePlatformWindows(); + ImGui::RenderPlatformWindowsDefault(); + glfwMakeContextCurrent(backup_current_context); + } + + // wait for other ranks to reach this point before swapping the buffer + MPI_Barrier(MPI_COMM_WORLD); + + // swap buffers + glfwSwapBuffers(glfwWindow); +} + +void MultiWindows::startNewOSPRayFrame() +{ + frame->startNewFrame(); +} + +void MultiWindows::waitOnOSPRayFrame() +{ + frame->waitOnFrame(); +} + +void MultiWindows::updateTitleBar() +{ + std::stringstream windowTitle; + windowTitle << "OSPRay Studio: "; + + auto &fb = frame->childAs("framebuffer"); + auto &v = frame->childAs("renderer")["varianceThreshold"]; + auto varianceThreshold = v.valueAs(); + + if (frame->pauseRendering) { + windowTitle << "rendering paused"; + } else if (frame->accumLimitReached()) { + windowTitle << "accumulation limit reached"; + } else if (fb.variance() < varianceThreshold) { + windowTitle << "variance threshold reached"; + } else { + windowTitle << std::setprecision(3) << latestFPS << " fps"; + if (latestFPS < 2.f) { + float progress = frame->frameProgress(); + windowTitle << " | "; + int barWidth = 20; + std::string progBar; + progBar.resize(barWidth + 2); + auto start = progBar.begin() + 1; + auto end = start + progress * barWidth; + std::fill(start, end, '='); + std::fill(end, progBar.end(), '_'); + *end = '>'; + progBar.front() = '['; + progBar.back() = ']'; + windowTitle << progBar; + } + } + + // Set indicator in the title bar for frame modified + windowTitle << (frame->isModified() ? "*" : ""); + + glfwSetWindowTitle(glfwWindow, windowTitle.str().c_str()); +} + +GLFWwindow *MultiWindows::getGLFWWindow() +{ + return glfwWindow; +} + +void MultiWindows::buildUI() +{ + // build main menu and options + buildMainMenu(); + + // build window UIs as needed + buildWindows(); + + if (uiCallback) { + uiCallback(); + } + + for (auto &p : pluginPanels) + if (p->isShown()) + p->buildUI(ImGui::GetCurrentContext()); +} + +void MultiWindows::refreshRenderer() +{ + // Change renderer if current type doesn't match requested + auto currentType = frame->childAs("renderer").child("type") + .valueAs(); + if (currentType != optRendererTypeStr) + frame->createChild("renderer", "renderer_" + optRendererTypeStr); + + auto &r = frame->childAs("renderer"); + r["pixelFilter"] = (int)optPF; + r["backgroundColor"] = optBackGroundColor; + r["pixelSamples"] = optSPP; + r["varianceThreshold"] = optVariance; + if (r.hasChild("maxContribution") && maxContribution < (float)math::inf) + r["maxContribution"] = maxContribution; + + // Re-add the backplate on renderer change + if (backPlateTexture != "") { + auto backplateTex = + sg::createNodeAs("map_backplate", "texture_2d"); + if (backplateTex->load(backPlateTexture, false, false)) + r.add(backplateTex); + else { + backplateTex = nullptr; + backPlateTexture = ""; + } + } else { + // Node removal requires waiting on previous frame completion + frame->cancelFrame(); + frame->waitOnFrame(); + r.remove("map_backplate"); + r.handle().removeParam("map_backplate"); + } +} + +void MultiWindows::saveRendererParams() +{ + auto &r = frame->childAs("renderer"); + + optRendererTypeStr = r["type"].valueAs(); + optPF = (OSPPixelFilterTypes) r["pixelFilter"].valueAs(); + optBackGroundColor = r["backgroundColor"].valueAs(); + optSPP = r["pixelSamples"].valueAs(); + optVariance = r["varianceThreshold"].valueAs(); + if (r.hasChild("maxContribution")) + maxContribution = r["maxContribution"].valueAs(); +} + +void MultiWindows::refreshScene(bool resetCam) +{ + if (frameAccumLimit) + frame->accumLimit = frameAccumLimit; + // Check that the frame contains a world, if not create one + auto world = frame->hasChild("world") ? frame->childNodeAs("world") + : sg::createNode("world", "world"); + if (optSceneConfig == "dynamic") + world->child("dynamicScene").setValue(true); + else if (optSceneConfig == "compact") + world->child("compactMode").setValue(true); + else if (optSceneConfig == "robust") + world->child("robustMode").setValue(true); + + world->createChild( + "materialref", "reference_to_material", defaultMaterialIdx); + + if (!filesToImport.empty()) + importFiles(world); + else if (scene != "") { + auto &gen = world->createChildAs( + scene + "_generator", "generator_" + scene); + gen.setMaterialRegistry(baseMaterialRegistry); + // The generators should reset the camera + resetCam = true; + } + + if (world->isModified()) { + // Cancel any in-progress frame as world->render() will modify live device + // parameters + frame->cancelFrame(); + frame->waitOnFrame(); + world->render(); + } + + frame->add(world); + + if (resetCam && !sgScene) { + const auto &worldBounds = frame->child("world").bounds(); + arcballCamera.reset(new ArcballCamera(worldBounds, windowSize)); + lastCamXfm = arcballCamera->getTransform(); + } + updateCamera(); + auto &fb = frame->childAs("framebuffer"); + fb.resetAccumulation(); +} + +void MultiWindows::clearScene() +{ + // Cancel any in-progress frame + frame->cancelFrame(); + frame->waitOnFrame(); + frame->remove("world"); + lightsManager->clear(); + animationManager->getAnimations().clear(); + animationManager->setTimeRange(range1f(rkcommon::math::empty)); + animationWidget->update(); + + // TODO: lights caching to avoid complete re-importing after clearing + sg::clearAssets(); + + // Recreate MaterialRegistry, clearing old registry and all materials + // Then, add the new one to the frame and set the renderer type + baseMaterialRegistry = sg::createNodeAs( + "baseMaterialRegistry", "materialRegistry"); + frame->add(baseMaterialRegistry); + baseMaterialRegistry->updateRendererType(); + + scene = ""; + refreshScene(true); +} + +nlohmann::ordered_json MultiWindows::getSceneState() { + auto ¤tCamera = frame->child("camera"); + JSON camera = { + {"cameraIdx", currentCamera.child("cameraId").valueAs()}, + {"cameraToWorld", arcballCamera->getTransform()}}; + if (currentCamera.hasChild("cameraSettingsId")) + camera["cameraSettingsIdx"] = + currentCamera.child("cameraSettingsId").valueAs(); + JSON animation; + animation = {{"time", animationManager->getTime()}, + {"shutter", animationManager->getShutter()}}; + JSON j = {{"world", frame->child("world")}, + {"camera", camera}, + {"lightsManager", *lightsManager}, + {"materialRegistry", *baseMaterialRegistry}, + {"animation", animation}}; + + return j; +} + +void MultiWindows::addToCommandLine(std::shared_ptr app) { + app->add_option( + "--displayConfig", + [&](const std::vector val) { + try { + std::ifstream configFile(val[0]); + if (!configFile) { + std::cerr << "The display config file does not exist." << std::endl; + return false; + } + configFile >> configDisplay; + } catch (nlohmann::json::exception &e) { + std::cerr << "Failed to parse the display config file: " << e.what() << std::endl; + return false; + } + if (configDisplay.empty()) { + std::cerr << "The display config file is empty." << std::endl; + return false; + } + return true; + }, + "Sets the display configuration file path" + ); + + app->add_option( + "--scene", + [&](const std::vector val) { + for (size_t i = 0; i < g_scenes.size(); ++i) { + if (val[0] == g_scenes[i]) { + scene = g_scenes[i]; + return true; + } + } + std::cerr << "The scene name does not exist." << std::endl; + return false; + }, + "Sets the opening scene name" + ); +} + +bool MultiWindows::parseCommandLine() +{ + int ac = studioCommon.argc; + const char **av = studioCommon.argv; + + std::shared_ptr app = std::make_shared("OSPRay Studio MULTIWINDOWS"); + StudioContext::addToCommandLine(app); + // remove --resolution option as it is set by a config file + app->remove_option(app->get_option("--resolution")); + MultiWindows::addToCommandLine(app); + try { + app->parse(ac, av); + } catch (const CLI::ParseError &e) { + exit(app->exit(e)); + } + + rendererTypeStr = optRendererTypeStr; + + return true; +} + +// Importer for all known file types (geometry and models) +void MultiWindows::importFiles(sg::NodePtr world) +{ + std::shared_ptr cameras{nullptr}; + if (!sgFileCameras) { + cameras = std::make_shared(); + // populate cameras map with default camera + auto mainCamera = frame->child("camera").nodeAs(); + cameras->operator[](mainCamera->child("uniqueCameraName").valueAs()) = mainCamera; + } + + for (auto file : filesToImport) { + try { + rkcommon::FileName fileName(file); + + // XXX: handling loading a scene here for now + if (fileName.ext() == "sg") { + sg::importScene(shared_from_this(), fileName); + sgScene = true; + } else { + std::cout << "Importing: " << file << std::endl; + + auto importer = sg::getImporter(world, file); + if (importer) { + if (volumeParams->children().size() > 0) { + auto vp = importer->getVolumeParams(); + for (auto &c : volumeParams->children()) { + vp->remove(c.first); + vp->add(c.second); + } + } + + importer->pointSize = pointSize; + importer->setFb(frame->childAs("framebuffer")); + importer->setMaterialRegistry(baseMaterialRegistry); + if (sgFileCameras) { + importer->importCameras = false; + importer->setCameraList(sgFileCameras); + } else if (cameras) { + importer->importCameras = true; + importer->setCameraList(cameras); + } + importer->setLightsManager(lightsManager); + importer->setArguments(studioCommon.argc, (char**)studioCommon.argv); + importer->setScheduler(scheduler); + importer->setAnimationList(animationManager->getAnimations()); + if (optInstanceConfig == "dynamic") + importer->setInstanceConfiguration( + sg::InstanceConfiguration::DYNAMIC); + else if (optInstanceConfig == "compact") + importer->setInstanceConfiguration( + sg::InstanceConfiguration::COMPACT); + else if (optInstanceConfig == "robust") + importer->setInstanceConfiguration( + sg::InstanceConfiguration::ROBUST); + + importer->importScene(); + } + } + } catch (const std::exception &e) { + std::cerr << "Failed to open file '" << file << "'!\n"; + std::cerr << " " << e.what() << std::endl; + } catch (...) { + std::cerr << "Failed to open file '" << file << "'!\n"; + } + + if (!optDoAsyncTasking) { + for (;;) { + size_t numTasksExecuted = 0; + + numTasksExecuted += scheduler->background()->executeAllTasksSync(); + numTasksExecuted += scheduler->ospray()->executeAllTasksSync(); + numTasksExecuted += scheduler->studio()->executeAllTasksSync(); + + if (numTasksExecuted == 0) { + break; + } + } + } + } + filesToImport.clear(); + + // Initializes time range for newly imported models + animationWidget->init(); + + if (sgFileCameras) + g_sceneCameras = *sgFileCameras; + else if (cameras) + g_sceneCameras = *cameras; +} + +void MultiWindows::saveCurrentFrame() +{ + int filenum = 0; + char filename[64]; + const char *ext = optImageFormat.c_str(); + + // Find an unused filename to ensure we don't overwrite and existing file + do + std::snprintf( + filename, 64, "%s.%04d.%s", optImageName.c_str(), filenum++, ext); + while (std::ifstream(filename).good()); + + int screenshotFlags = optSaveLayersSeparately << 3 | optSaveNormal << 2 + | optSaveDepth << 1 | optSaveAlbedo; + + auto &fb = frame->childAs("framebuffer"); + auto fbFloatFormat = fb["floatFormat"].valueAs(); + if (screenshotFlags > 0 && !fbFloatFormat) + std::cout + << " *** Cannot save additional information without changing FB format to float ***" + << std::endl; + frame->saveFrame(std::string(filename), screenshotFlags); +} + +void MultiWindows::pushLookMark() +{ + cameraStack.push_back(arcballCamera->getState()); + vec3f from = arcballCamera->eyePos(); + vec3f up = arcballCamera->upDir(); + vec3f at = arcballCamera->lookDir() + from; + fprintf(stderr, + "-vp %f %f %f -vu %f %f %f -vi %f %f %f\n", + from.x, + from.y, + from.z, + up.x, + up.y, + up.z, + at.x, + at.y, + at.z); +} + +void MultiWindows::popLookMark() +{ + if (cameraStack.empty()) + return; + CameraState cs = cameraStack.back(); + cameraStack.pop_back(); + + arcballCamera->setState(cs); + updateCamera(); +} + +// Main menu ////////////////////////////////////////////////////////////////// + +void MultiWindows::buildMainMenu() +{ + // build main menu bar and options + ImGui::BeginMainMenuBar(); + buildMainMenuFile(); + buildMainMenuEdit(); + buildMainMenuView(); + buildMainMenuPlugins(); + ImGui::EndMainMenuBar(); +} + +void MultiWindows::buildMainMenuFile() +{ + static bool showImportFileBrowser = false; + + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Import ...", nullptr)) + + showImportFileBrowser = true; + if (ImGui::BeginMenu("Demo Scene")) { + for (size_t i = 0; i < g_scenes.size(); ++i) { + if (ImGui::MenuItem(g_scenes[i].c_str(), nullptr)) { + scene = g_scenes[i]; + refreshScene(true); + } + } + ImGui::EndMenu(); + } + ImGui::Separator(); + if (ImGui::BeginMenu("Save")) { + if (ImGui::MenuItem("Scene (entire)")) { + std::ofstream dump("studio_scene.sg"); + dump << getSceneState().dump(); + } + + ImGui::Separator(); + if (ImGui::MenuItem("Materials (only)")) { + std::ofstream materials("studio_materials.sg"); + JSON j = {{"materialRegistry", *baseMaterialRegistry}}; + materials << j.dump(); + } + if (ImGui::MenuItem("Lights (only)")) { + std::ofstream lights("studio_lights.sg"); + JSON j = {{"lightsManager", *lightsManager}}; + lights << j.dump(); + } + if (ImGui::MenuItem("Camera (only)")) { + std::ofstream camera("studio_camera.sg"); + JSON j = {{"camera", arcballCamera->getState()}}; + camera << j.dump(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Screenshot", "Ctrl+S", nullptr)) + g_saveNextFrame = true; + + static const std::vector screenshotFiletypes = + sg::getExporterTypes(); + + static int screenshotFiletypeChoice = + std::distance(screenshotFiletypes.begin(), + std::find(screenshotFiletypes.begin(), + screenshotFiletypes.end(), + optImageFormat)); + + ImGui::SetNextItemWidth(5.f * ImGui::GetFontSize()); + if (ImGui::Combo("##screenshot_filetype", + (int *)&screenshotFiletypeChoice, + stringVec_callback, + (void *)screenshotFiletypes.data(), + screenshotFiletypes.size())) { + optImageFormat = screenshotFiletypes[screenshotFiletypeChoice]; + } + sg::showTooltip("Image filetype for saving screenshots"); + + if (optImageFormat == "exr") { + // the following options should be available only when FB format is + // float. + auto &fb = frame->childAs("framebuffer"); + auto fbFloatFormat = fb["floatFormat"].valueAs(); + if (ImGui::Checkbox("FB float format ", &fbFloatFormat)) + fb["floatFormat"] = fbFloatFormat; + if (fbFloatFormat) { + ImGui::Checkbox("albedo##saveAlbedo", &optSaveAlbedo); + ImGui::SameLine(); + ImGui::Checkbox("layers as separate files", &optSaveLayersSeparately); + ImGui::Checkbox("depth##saveDepth", &optSaveDepth); + ImGui::Checkbox("normal##saveNormal", &optSaveNormal); + } + } + + ImGui::EndMenu(); + } + + ImGui::Separator(); + // Remove Quit option if in "show mode" to make it more difficult to exit + // by mistake. + auto showMode = + rkcommon::utility::getEnvVar("OSPSTUDIO_SHOW_MODE"); + if (showMode) { + ImGui::TextColored( + ImVec4(.8f, .2f, .2f, 1.f), "ShowMode, use ctrl-Q to exit"); + } else { + if (ImGui::MenuItem("Quit", "Alt+F4")) + g_quitNextFrame = true; + } + + ImGui::EndMenu(); + } + + // Leave the fileBrowser open until files are selected + if (showImportFileBrowser) { + if (fileBrowser(filesToImport, "Select Import File(s) - ", true)) { + showImportFileBrowser = false; + // do not reset camera when loading a scene file + bool resetCam = true; + for (auto &fn : filesToImport) + if (rkcommon::FileName(fn).ext() == "sg") + resetCam = false; + refreshScene(resetCam); + } + } +} + +void MultiWindows::buildMainMenuEdit() +{ + if (ImGui::BeginMenu("Edit")) { + // Scene stuff ///////////////////////////////////////////////////// + + if (ImGui::MenuItem("Lights...", "", nullptr)) + showLightEditor = true; + if (ImGui::MenuItem("Transforms...", "", nullptr)) + showTransformEditor = true; + if (ImGui::MenuItem("Materials...", "", nullptr)) + showMaterialEditor = true; + if (ImGui::MenuItem("Transfer Functions...", "", nullptr)) + showTransferFunctionEditor = true; + if (ImGui::MenuItem("Isosurfaces...", "", nullptr)) + showIsosurfaceEditor = true; + ImGui::Separator(); + + if (ImGui::MenuItem("Clear scene")) + g_clearSceneConfirm = true; + + ImGui::EndMenu(); + } + + if (g_clearSceneConfirm) { + g_clearSceneConfirm = false; + ImGui::OpenPopup("Clear scene"); + } + + if (ImGui::BeginPopupModal("Clear scene")) { + ImGui::Text("Are you sure you want to clear the scene?"); + ImGui::Text("This will delete all objects, materials and lights."); + + if (ImGui::Button("No!")) + ImGui::CloseCurrentPopup(); + ImGui::SameLine(ImGui::GetWindowWidth()-(8*ImGui::GetFontSize())); + + if (ImGui::Button("Yes, clear it")) { + clearScene(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void MultiWindows::buildMainMenuView() +{ + static bool showFileBrowser = false; + static bool guizmoOn = false; + if (ImGui::BeginMenu("View")) { + // Camera stuff //////////////////////////////////////////////////// + + if (ImGui::MenuItem("Camera...", "", nullptr)) + showCameraEditor = true; + if (ImGui::MenuItem("Center camera", "", nullptr)) { + arcballCamera.reset( + new ArcballCamera(frame->child("world").bounds(), windowSize)); + updateCamera(); + } + + static bool lockUpDir = false; + if (ImGui::Checkbox("Lock UpDir", &lockUpDir)) + arcballCamera->setLockUpDir(lockUpDir); + + if (lockUpDir) { + ImGui::SameLine(); + static int dir = 1; + static int _dir = -1; + ImGui::RadioButton("X##setUpDir", &dir, 0); + ImGui::SameLine(); + ImGui::RadioButton("Y##setUpDir", &dir, 1); + ImGui::SameLine(); + ImGui::RadioButton("Z##setUpDir", &dir, 2); + if (dir != _dir) { + arcballCamera->setUpDir(vec3f(dir == 0, dir == 1, dir == 2)); + _dir = dir; + } + } + + ImGui::Text("Camera Movement Speed:"); + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + ImGui::SliderFloat("Speed##camMov", &maxMoveSpeed, 0.1f, 5.0f); + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + ImGui::SliderFloat("FineControl##camMov", &fineControl, 0.1f, 1.0f, "%0.2fx"); + sg::showTooltip("hold for more sensitive camera movement."); + + ImGui::Separator(); + + if (ImGui::MenuItem("Animation Controls...", "", nullptr)) + animationWidget->setShowUI(); + + if (ImGui::MenuItem("Keyframes...", "", nullptr)) + showKeyframes = true; + if (ImGui::MenuItem("Snapshots...", "", nullptr)) + showSnapshots = true; + + ImGui::Checkbox("Auto rotate", &optAutorotate); + if (optAutorotate) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + ImGui::SliderInt(" speed", &autorotateSpeed, 1, 100); + } + ImGui::Separator(); + + // Renderer stuff ////////////////////////////////////////////////// + + if (ImGui::MenuItem("Renderer...")) + showRendererEditor = true; + + auto &renderer = frame->childAs("renderer"); + + ImGui::Checkbox("Rendering stats", &showRenderingStats); + ImGui::Checkbox("Pause rendering", &frame->pauseRendering); + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + ImGui::DragInt( + "Limit accumulation", &frame->accumLimit, 1, 0, INT_MAX, "%d frames"); + + // Although varianceThreshold and backgroundColor are found in the + // renderer UI, also add them here to make them easier to find. + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + GenerateWidget(renderer["varianceThreshold"]); + GenerateWidget(renderer["backgroundColor"]); + + if (ImGui::MenuItem("Set background texture...")) + showFileBrowser = true; + if (!backPlateTexture.str().empty()) { + ImGui::TextColored(ImVec4(.5f, .5f, .5f, 1.f), + "current: %s", + backPlateTexture.base().c_str()); + if (ImGui::MenuItem("Clear background texture")) { + backPlateTexture = ""; + refreshRenderer(); + } + } + + // Framebuffer and window stuff //////////////////////////////////// + ImGui::Separator(); + if (ImGui::MenuItem("Framebuffer...")) + showFrameBufferEditor = true; + if (ImGui::BeginMenu("Quick window size")) { + const std::vector options = {{480, 270}, + {960, 540}, + {1280, 720}, + {1920, 1080}, + {2560, 1440}, + {3840, 2160}}; + for (auto &sizeChoice : options) { + char label[64]; + snprintf(label, + sizeof(label), + "%s%d x %d", + windowSize == sizeChoice ? "*" : " ", + sizeChoice.x, + sizeChoice.y); + if (ImGui::MenuItem(label)) { + glfwSetWindowSize(glfwWindow, sizeChoice.x, sizeChoice.y); + reshape(sizeChoice); + } + } + ImGui::EndMenu(); + } + ImGui::Checkbox("Display as sRGB", &uiDisplays_sRGB); + sg::showTooltip("Display linear framebuffers as sRGB,\n" + "maintains consistent display across all formats."); + // Allows the user to cancel long frame renders, such as too-many spp or + // very large resolution. Don't wait on the frame-cancel completion as + // this locks up the UI. Note: Next frame-start following frame + // cancellation isn't immediate. + if (ImGui::MenuItem("Cancel frame")) + frame->cancelFrame(); + + ImGui::Separator(); + + // UI options ////////////////////////////////////////////////////// + ImGui::Text("ui options"); + + ImGui::Checkbox("Show tooltips", &g_ShowTooltips); + if (g_ShowTooltips) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + ImGui::DragInt("delay", &g_TooltipDelay, 50, 0, 1000, "%d ms"); + } + +#if 1 // XXX example new features that need to be integrated + ImGui::Separator(); + + ImGuiIO& io = ImGui::GetIO(); + ImGui::CheckboxFlags( + "DockingEnable", &io.ConfigFlags, ImGuiConfigFlags_DockingEnable); + sg::showTooltip("[experimental] Menu docking"); + ImGui::CheckboxFlags( + "ViewportsEnable", &io.ConfigFlags, ImGuiConfigFlags_ViewportsEnable); + sg::showTooltip("[experimental] Mind blowing multi-viewports support"); + + ImGui::Checkbox("Guizmo", &guizmoOn); + + ImGui::Text("...right-click to open pie menu..."); + pieMenu(); +#endif + + ImGui::EndMenu(); + } + +#if 1 // Guizmo shows outsize menu window + if (guizmoOn) { + ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration + | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings + | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav + | ImGuiWindowFlags_NoMove; + ImGui::SetNextWindowBgAlpha(0.75f); + + // Bottom right corner + ImVec2 window_pos(ImGui::GetIO().DisplaySize.x, + ImGui::GetIO().DisplaySize.y); + ImVec2 window_pos_pivot(1.f, 1.f); + ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot); + + if (ImGui::Begin("###guizmo", &guizmoOn, flags)) { + guizmo(); + ImGui::End(); + } + } +#endif + + // Leave the fileBrowser open until file is selected + if (showFileBrowser) { + FileList fileList = {}; + if (fileBrowser(fileList, "Select Background Texture")) { + showFileBrowser = false; + + if (!fileList.empty()) { + backPlateTexture = fileList[0]; + refreshRenderer(); + } + } + } +} + +void MultiWindows::buildMainMenuPlugins() +{ + if (!pluginPanels.empty() && ImGui::BeginMenu("Plugins")) { + for (auto &p : pluginPanels) + if (ImGui::MenuItem(p->name().c_str())) + p->setShown(true); + + ImGui::EndMenu(); + } +} + +// Option windows ///////////////////////////////////////////////////////////// + +void MultiWindows::buildWindows() +{ + if (showRendererEditor) + buildWindowRendererEditor(); + if (showFrameBufferEditor) + buildWindowFrameBufferEditor(); + if (showKeyframes) + buildWindowKeyframes(); + if (showSnapshots) + buildWindowSnapshots(); + if (showCameraEditor) + buildWindowCameraEditor(); + if (showLightEditor) + buildWindowLightEditor(); + if (showMaterialEditor) + buildWindowMaterialEditor(); + if (showTransferFunctionEditor) + buildWindowTransferFunctionEditor(); + if (showIsosurfaceEditor) + buildWindowIsosurfaceEditor(); + if (showTransformEditor) + buildWindowTransformEditor(); + if (showRenderingStats) + buildWindowRenderingStats(); + + // Add the animation widget's UI + animationWidget->addUI(); +} + +void MultiWindows::buildWindowRendererEditor() +{ + if (!ImGui::Begin( + "Renderer editor", &showRendererEditor, g_imguiWindowFlags)) { + ImGui::End(); + return; + } + + int whichRenderer = + find(g_renderers.begin(), g_renderers.end(), rendererTypeStr) + - g_renderers.begin(); + + static int whichDebuggerType = 0; + ImGui::PushItemWidth(10.f * ImGui::GetFontSize()); + if (ImGui::Combo("renderer##whichRenderer", + &whichRenderer, + rendererUI_callback, + nullptr, + g_renderers.size())) { + rendererTypeStr = g_renderers[whichRenderer]; + + if (rendererType == OSPRayRendererType::DEBUGGER) + whichDebuggerType = 0; // reset UI if switching away + // from debug renderer + + if (rendererTypeStr == "scivis") + rendererType = OSPRayRendererType::SCIVIS; + else if (rendererTypeStr == "pathtracer") + rendererType = OSPRayRendererType::PATHTRACER; + else if (rendererTypeStr == "ao") + rendererType = OSPRayRendererType::AO; + else if (rendererTypeStr == "debug") + rendererType = OSPRayRendererType::DEBUGGER; +#ifdef USE_MPI + if (rendererTypeStr == "mpiRaycast") + rendererType = OSPRayRendererType::MPIRAYCAST; +#endif + else + rendererType = OSPRayRendererType::OTHER; + + // Change the renderer type, if the new renderer is different. + auto &renderer = frame->childAs("renderer"); + auto newType = "renderer_" + rendererTypeStr; + + if (renderer["type"].valueAs() != newType) { + // Save properties of current renderer then create new renderer + saveRendererParams(); + optRendererTypeStr = rendererTypeStr; + refreshRenderer(); + } + } + + auto &renderer = frame->childAs("renderer"); + + if (rendererType == OSPRayRendererType::DEBUGGER) { + if (ImGui::Combo("debug type##whichDebugType", + &whichDebuggerType, + debugTypeUI_callback, + nullptr, + g_debugRendererTypes.size())) { + renderer["method"] = g_debugRendererTypes[whichDebuggerType]; + } + } + + if (GenerateWidget(renderer)) + saveRendererParams(); + + ImGui::End(); +} + +void MultiWindows::buildWindowFrameBufferEditor() +{ + if (!ImGui::Begin( + "Framebuffer editor", &showFrameBufferEditor, g_imguiWindowFlags)) { + ImGui::End(); + return; + } + + auto &fb = frame->childAs("framebuffer"); + GenerateWidget(fb, sg::TreeState::ALLOPEN); + + ImGui::Separator(); + + static int whichBuffer = 0; + ImGui::Text("Display Buffer"); + ImGui::RadioButton("color##displayColor", &whichBuffer, 0); + + if (!fb.hasAlbedoChannel() && !fb.hasDepthChannel()) { + ImGui::TextColored( + ImVec4(.5f, .5f, .5f, 1.f), "Enable float format for more buffers"); + } + + if (fb.hasAlbedoChannel()) { + ImGui::SameLine(); + ImGui::RadioButton("albedo##displayAlbedo", &whichBuffer, 1); + } + if (fb.hasDepthChannel()) { + ImGui::SameLine(); + ImGui::RadioButton("depth##displayDepth", &whichBuffer, 2); + ImGui::SameLine(); + ImGui::RadioButton("invert depth##displayDepthInv", &whichBuffer, 3); + } + + switch (whichBuffer) { + case 0: + optShowColor = true; + optShowAlbedo = optShowDepth = optShowDepthInvert = false; + break; + case 1: + optShowAlbedo = true; + optShowColor = optShowDepth = optShowDepthInvert = false; + break; + case 2: + optShowDepth = true; + optShowColor = optShowAlbedo = optShowDepthInvert = false; + break; + case 3: + optShowDepth = true; + optShowDepthInvert = true; + optShowColor = optShowAlbedo = false; + break; + } + + ImGui::Separator(); + + ImGui::Text("Post-processing"); + if (fb.isFloatFormat()) { + ImGui::Checkbox("Tonemap", &frame->toneMapFB); + ImGui::SameLine(); + ImGui::Checkbox("Tonemap nav", &frame->toneMapNavFB); + + if (studioCommon.denoiserAvailable) { + if (ImGui::Checkbox("Denoise", &optDenoiser)) + frame->denoiseFB = optDenoiser; + ImGui::SameLine(); + ImGui::Checkbox("Denoise nav", &frame->denoiseNavFB); + } + if (frame->denoiseFB || frame->denoiseNavFB) { + ImGui::Checkbox("Denoise only PathTracer", &frame->denoiseOnlyPathTracer); + ImGui::Checkbox("Denoise on final frame", &frame->denoiseFBFinalFrame); + ImGui::SameLine(); + // Add accum here for convenience with final-frame denoising + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + ImGui::DragInt( + "Limit accumulation", &frame->accumLimit, 1, 0, INT_MAX, "%d frames"); + } + } else { + ImGui::TextColored( + ImVec4(.5f, .5f, .5f, 1.f), "Enable float format for post-processing"); + } + + ImGui::Separator(); + + ImGui::Text("Scaling"); + { + static const float scaleValues[9] = { + 0.25f, 0.5f, 0.75f, 1.f, 1.25f, 1.5f, 2.f, 4.f, 8.f}; + + auto size = frame->child("windowSize").valueAs(); + char _label[56]; + auto createLabel = [&_label, size](std::string uniqueId, float v) { + const vec2i _sz = v * size; + snprintf(_label, + sizeof(_label), + "%1.2fx (%d,%d)##%s", + v, + _sz.x, + _sz.y, + uniqueId.c_str()); + return _label; + }; + + auto selectNewScale = [&](std::string id, const float _scale) { + auto scale = _scale; + auto custom = true; + for (auto v : scaleValues) { + if (ImGui::Selectable(createLabel(id, v), v == scale)) + scale = v; + custom &= (v != scale); + } + + ImGui::Separator(); + char cLabel[64]; + snprintf(cLabel, sizeof(cLabel), "custom %s", createLabel(id, scale)); + if (ImGui::BeginMenu(cLabel)) { + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + ImGui::InputFloat("x##fb_scaling", &scale); + ImGui::EndMenu(); + } + + return scale; + }; + + auto scale = frame->child("scale").valueAs(); + ImGui::SetNextItemWidth(12 * ImGui::GetFontSize()); + if (ImGui::BeginCombo("Scale resolution", createLabel("still", scale))) { + auto newScale = selectNewScale("still", scale); + if (scale != newScale) + frame->child("scale") = newScale; + ImGui::EndCombo(); + } + + scale = frame->child("scaleNav").valueAs(); + ImGui::SetNextItemWidth(12 * ImGui::GetFontSize()); + if (ImGui::BeginCombo("Scale Nav resolution", createLabel("nav", scale))) { + auto newScale = selectNewScale("nav", scale); + if (scale != newScale) + frame->child("scaleNav") = newScale; + ImGui::EndCombo(); + } + } + + ImGui::Separator(); + + ImGui::Text("Aspect Ratio"); + const float origAspect = lockAspectRatio; + if (lockAspectRatio != 0.f) { + ImGui::SameLine(); + ImGui::Text("locked at %f", lockAspectRatio); + if (ImGui::Button("Unlock")) { + lockAspectRatio = 0.f; + } + } else { + if (ImGui::Button("Lock")) { + lockAspectRatio = + static_cast(windowSize.x) / static_cast(windowSize.y); + } + sg::showTooltip("Lock to current aspect ratio"); + } + + ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); + ImGui::InputFloat("Set", &lockAspectRatio); + sg::showTooltip("Lock to custom aspect ratio"); + lockAspectRatio = std::max(lockAspectRatio, 0.f); + + if (origAspect != lockAspectRatio) + reshape(windowSize); + + ImGui::End(); +} + +void MultiWindows::buildWindowKeyframes() +{ + if (!ImGui::Begin("Keyframe editor", &showKeyframes, g_imguiWindowFlags)) { + ImGui::End(); + return; + } + + ImGui::SetNextItemWidth(25 * ImGui::GetFontSize()); + + if (ImGui::Button("add")) { // add current camera state after the selected one + if (cameraStack.empty()) { + cameraStack.push_back(arcballCamera->getState()); + g_camSelectedStackIndex = 0; + } else { + cameraStack.insert(cameraStack.begin() + g_camSelectedStackIndex + 1, + arcballCamera->getState()); + g_camSelectedStackIndex++; + } + } + + sg::showTooltip("insert a new keyframe after the selected keyframe, based\n" + "on the current camera state"); + + ImGui::SameLine(); + if (ImGui::Button("remove")) { // remove the selected camera state + cameraStack.erase(cameraStack.begin() + g_camSelectedStackIndex); + g_camSelectedStackIndex = std::max(0, g_camSelectedStackIndex - 1); + } + sg::showTooltip("remove the currently selected keyframe"); + + if (cameraStack.size() >= 2) { + ImGui::SameLine(); + if (ImGui::Button(g_animatingPath ? "stop" : "play")) { + g_animatingPath = !g_animatingPath; + g_camCurrentPathIndex = 0; + if (g_animatingPath) + g_camPath = buildPath(cameraStack, g_camPathSpeed * 0.01); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(10 * ImGui::GetFontSize()); + ImGui::SliderFloat("speed##path", &g_camPathSpeed, 0.f, 10.0); + sg::showTooltip("Animation speed for computed path.\n" + "Slow speeds may cause jitter for small objects"); + + static bool showCameraPath = false; + if (ImGui::Checkbox("show camera path", &showCameraPath)) { + if (!showCameraPath) { + frame->child("world").remove("cameraPath_xfm"); + refreshScene(false); + } else { + auto pathXfm = sg::createNode("cameraPath_xfm", "transform"); + + const auto &worldBounds = frame->child("world").bounds(); + float pathRad = 0.0075f * reduce_min(worldBounds.size()); + std::vector cameraPath = + buildPath(cameraStack, g_camPathSpeed * 0.01f); + std::vector pathVertices; // position and radius + for (const auto &state : cameraPath) + pathVertices.emplace_back(state.position(), pathRad); + pathVertices.emplace_back(cameraStack.back().position(), pathRad); + + std::vector indexes(pathVertices.size()); + std::iota(indexes.begin(), indexes.end(), 0); + + std::vector colors(pathVertices.size()); + std::fill(colors.begin(), colors.end(), vec4f(0.8f, 0.4f, 0.4f, 1.f)); + + const std::vector mID = { + static_cast(baseMaterialRegistry->baseMaterialOffSet())}; + auto mat = sg::createNode("pathGlass", "thinGlass"); + baseMaterialRegistry->add(mat); + + auto path = sg::createNode("cameraPath", "geometry_curves"); + path->createChildData("vertex.position_radius", pathVertices); + path->createChildData("vertex.color", colors); + path->createChildData("index", indexes); + path->createChild("type", "uchar", (unsigned char)OSP_ROUND); + path->createChild("basis", "uchar", (unsigned char)OSP_CATMULL_ROM); + path->createChildData("material", mID); + path->child("material").setSGOnly(); + + + std::vector capVertexes; + std::vector capColors; + for (int i = 0; i < cameraStack.size(); i++) { + capVertexes.push_back(cameraStack[i].position()); + if (i == 0) + capColors.push_back(vec4f(.047f, .482f, .863f, 1.f)); + else + capColors.push_back(vec4f(vec3f(0.8f), 1.f)); + } + + auto caps = sg::createNode("cameraPathCaps", "geometry_spheres"); + caps->createChildData("sphere.position", capVertexes); + caps->createChildData("color", capColors); + caps->child("color").setSGOnly(); + caps->child("radius") = pathRad * 1.5f; + caps->createChildData("material", mID); + caps->child("material").setSGOnly(); + + pathXfm->add(path); + pathXfm->add(caps); + + frame->child("world").add(pathXfm); + } + } + } + + if (ImGui::ListBoxHeader("##")) { + for (int i = 0; i < cameraStack.size(); i++) { + if (ImGui::Selectable( + (std::to_string(i) + ": " + to_string(cameraStack[i])).c_str(), + (g_camSelectedStackIndex == (int) i))) { + g_camSelectedStackIndex = i; + arcballCamera->setState(cameraStack[i]); + updateCamera(); + } + } + ImGui::ListBoxFooter(); + } + + ImGui::End(); +} + +void MultiWindows::setCameraSnapshot(size_t snapshot) +{ + if (snapshot < cameraStack.size()) { + const CameraState &cs = cameraStack.at(snapshot); + arcballCamera->setState(cs); + updateCamera(); + } +} + +void MultiWindows::buildWindowSnapshots() +{ + if (!ImGui::Begin("Camera snap shots", &showSnapshots, g_imguiWindowFlags)) { + ImGui::End(); + return; + } + ImGui::Text("+ key to add new snapshots"); + for (size_t s = 0; s < cameraStack.size(); s++) { + if (ImGui::Button(std::to_string(s).c_str())) { + setCameraSnapshot(s); + } + } + if (cameraStack.size()) { + if (ImGui::Button("save to cams.json")) { + std::ofstream cams("cams.json"); + if (cams) { + JSON j = cameraStack; + cams << j; + } + } + } + ImGui::End(); +} + +void MultiWindows::buildWindowLightEditor() +{ + if (!ImGui::Begin("Light editor", &showLightEditor, g_imguiWindowFlags)) { + ImGui::End(); + return; + } + + auto &lights = lightsManager->children(); + static int whichLight = -1; + + // Validate that selected light is still a valid light. Clear scene will + // change the lights list, elsewhere. + if (whichLight >= lights.size()) + whichLight = -1; + + ImGui::Text("lights"); + if (ImGui::ListBoxHeader("", 3)) { + int i = 0; + for (auto &light : lights) { + if (ImGui::Selectable(light.first.c_str(), (whichLight == i))) { + whichLight = i; + } + i++; + } + ImGui::ListBoxFooter(); + + if (whichLight != -1) { + ImGui::Text("edit"); + GenerateWidget(lightsManager->child(lights.at_index(whichLight).first)); + } + } + + if (lights.size() > 1) { + if (ImGui::Button("remove")) { + if (whichLight != -1) { + // Node removal requires waiting on previous frame completion + frame->cancelFrame(); + frame->waitOnFrame(); + lightsManager->removeLight(lights.at_index(whichLight).first); + whichLight = std::max(0, whichLight - 1); + } + } + } + + ImGui::Separator(); + + ImGui::Text("new light"); + + static std::string lightType = ""; + if (ImGui::Combo("type##whichLightType", + &whichLightType, + lightTypeUI_callback, + nullptr, + g_lightTypes.size())) { + if (whichLightType > -1 && whichLightType < (int) g_lightTypes.size()) + lightType = g_lightTypes[whichLightType]; + } + + static bool lightNameWarning = false; + static bool lightTexWarning = false; + + static char lightName[64] = ""; + if (ImGui::InputText("name", lightName, sizeof(lightName))) + lightNameWarning = !(*lightName) || lightsManager->lightExists(lightName); + + // HDRI lights need a texture + static bool showHDRIFileBrowser = false; + static rkcommon::FileName texFileName(""); + if (lightType == "hdri") { + // This field won't be typed into. + ImGui::InputTextWithHint( + "texture", "select...", (char *)texFileName.base().c_str(), 0); + if (ImGui::IsItemClicked()) + showHDRIFileBrowser = true; + } else + lightTexWarning = false; + + // Leave the fileBrowser open until file is selected + if (showHDRIFileBrowser) { + FileList fileList = {}; + if (fileBrowser(fileList, "Select HDRI Texture")) { + showHDRIFileBrowser = false; + + lightTexWarning = fileList.empty(); + if (!fileList.empty()) { + texFileName = fileList[0]; + } + } + } + + if ((!lightNameWarning && !lightTexWarning)) { + if (ImGui::Button("add")) { + if (lightsManager->addLight(lightName, lightType)) { + if (lightType == "hdri") { + auto &hdri = lightsManager->child(lightName); + hdri["filename"] = texFileName.str(); + } + // Select newly added light + whichLight = lights.size() - 1; + } else { + lightNameWarning = true; + } + } + if (lightsManager->hasDefaultLight()) { + auto &rmDefaultLight = lightsManager->rmDefaultLight; + ImGui::SameLine(); + ImGui::Checkbox("remove default", &rmDefaultLight); + } + } + + if (lightNameWarning) + ImGui::TextColored( + ImVec4(1.f, 0.f, 0.f, 1.f), "Light must have unique non-empty name"); + if (lightTexWarning) + ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "No texture provided"); + + ImGui::End(); +} + +void MultiWindows::buildWindowCameraEditor() +{ + if (!ImGui::Begin("Camera editor", &showCameraEditor)) { + ImGui::End(); + return; + } + + whichCamera = frame->child("camera").child("cameraId").valueAs(); + + // Only present selector UI if more than one camera + if (!g_sceneCameras.empty() + && ImGui::Combo("sceneCameras##whichCamera", + &whichCamera, + cameraUI_callback, + nullptr, + g_sceneCameras.size())) { + if (whichCamera > -1 && whichCamera < (int)g_sceneCameras.size()) { + auto &newCamera = g_sceneCameras.at_index(whichCamera); + g_selectedSceneCamera = newCamera.second; + auto hasParents = g_selectedSceneCamera->parents().size(); + frame->remove("camera"); + frame->add(g_selectedSceneCamera); + + if (g_selectedSceneCamera->hasChild("aspect")) + lockAspectRatio = + g_selectedSceneCamera->child("aspect").valueAs(); + reshape(windowSize); // resets aspect + if (!hasParents) + updateCamera(); + } + } + + // Change camera type + ImGui::Text("Type:"); + static const std::vector cameraTypes = { + "perspective", "orthographic", "panoramic"}; + auto currentType = frame->childAs("camera").subType(); + for (auto &type : cameraTypes) { + auto newType = "camera_" + type; + ImGui::SameLine(); + if (ImGui::RadioButton(type.c_str(), currentType == newType)) { + // Create new camera of new type + frame->createChildAs("camera", newType); + reshape(windowSize); // resets aspect + updateCamera(); // resets position, direction, etc + break; + } + } + + // UI Widget + GenerateWidget(frame->childAs("camera")); + + ImGui::End(); +} + +void MultiWindows::buildWindowMaterialEditor() +{ + if (!ImGui::Begin("Material editor", &showMaterialEditor)) { + ImGui::End(); + return; + } + + static std::vector types{sg::NodeType::MATERIAL}; + static SearchWidget searchWidget(types, types, sg::TreeState::ALLCLOSED); + static AdvancedMaterialEditor advMaterialEditor; + + searchWidget.addSearchBarUI(*baseMaterialRegistry); + searchWidget.addSearchResultsUI(*baseMaterialRegistry); + auto selected = searchWidget.getSelected(); + if (selected) { + GenerateWidget(*selected); + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(245, 200, 66, 255)); + if (ImGui::TreeNodeEx( + "Advanced options##materials", ImGuiTreeNodeFlags_None)) { + ImGui::PopStyleColor(); + advMaterialEditor.buildUI(baseMaterialRegistry, selected); + ImGui::TreePop(); + } else { + ImGui::PopStyleColor(); + } + } + + if (baseMaterialRegistry->isModified()) { + frame->cancelFrame(); + frame->waitOnFrame(); + } + + ImGui::End(); +} + +void MultiWindows::buildWindowTransferFunctionEditor() +{ + if (!ImGui::Begin("Transfer Function editor", &showTransferFunctionEditor)) { + ImGui::End(); + return; + } + + // Gather all transfer functions in the scene + sg::CollectTransferFunctions visitor; + frame->traverse(visitor); + auto &transferFunctions = visitor.transferFunctions; + + if (transferFunctions.empty()) { + ImGui::Text("== empty == "); + + } else { + ImGui::Text("TransferFunctions"); + static int whichTFn = -1; + static std::string selected = ""; + + // Called by TransferFunctionWidget to update selected TFn + auto transferFunctionUpdatedCallback = + [&](const range1f &valueRange, + const std::vector &colorsAndOpacities) { + if (whichTFn != -1) { + auto &tfn = + *(transferFunctions[selected]->nodeAs()); + auto &colors = tfn.colors; + auto &opacities = tfn.opacities; + + colors.resize(colorsAndOpacities.size()); + opacities.resize(colorsAndOpacities.size()); + + // Separate out colors + std::transform(colorsAndOpacities.begin(), + colorsAndOpacities.end(), + colors.begin(), + [](vec4f c4) { return vec3f(c4[0], c4[1], c4[2]); }); + + // Separate out opacities + std::transform(colorsAndOpacities.begin(), + colorsAndOpacities.end(), + opacities.begin(), + [](vec4f c4) { return c4[3]; }); + + tfn.createChildData("color", colors); + tfn.createChildData("opacity", opacities); + tfn["valueRange"] = valueRange.toVec2(); + } + }; + + static TransferFunctionWidget transferFunctionWidget( + transferFunctionUpdatedCallback, + range1f(0.f, 1.f), + "TransferFunctionEditor"); + + if (ImGui::ListBoxHeader("", transferFunctions.size())) { + int i = 0; + for (auto t : transferFunctions) { + if (ImGui::Selectable(t.first.c_str(), (whichTFn == i))) { + whichTFn = i; + selected = t.first; + + auto &tfn = *(t.second->nodeAs()); + const auto numSamples = tfn.colors.size(); + + if (numSamples > 1) { + auto vRange = tfn["valueRange"].valueAs(); + + // Create a c4 from c3 + opacity + std::vector c4; + + if (tfn.opacities.size() != numSamples) + tfn.opacities.resize(numSamples, tfn.opacities.back()); + + for (int n = 0; n < numSamples; n++) { + c4.emplace_back(vec4f(tfn.colors.at(n), tfn.opacities.at(n))); + } + + transferFunctionWidget.setValueRange(range1f(vRange[0], vRange[1])); + transferFunctionWidget.setColorsAndOpacities(c4); + } + } + i++; + } + ImGui::ListBoxFooter(); + + ImGui::Separator(); + if (whichTFn != -1) { + transferFunctionWidget.updateUI(); + } + } + } + + ImGui::End(); +} + +void MultiWindows::buildWindowIsosurfaceEditor() +{ + if (!ImGui::Begin("Isosurface editor", &showIsosurfaceEditor)) { + ImGui::End(); + return; + } + + // Specialized node vector list box + using vNodePtr = std::vector; + auto ListBox = [](const char *label, int *selected, vNodePtr &nodes) { + auto getter = [](void *vec, int index, const char **name) { + auto nodes = static_cast(vec); + if (0 > index || index >= (int) nodes->size()) + return false; + // Need longer lifetime than this lambda? + static std::string copy = ""; + copy = nodes->at(index)->name(); + *name = copy.data(); + return true; + }; + + if (nodes.empty()) + return false; + return ImGui::ListBox( + label, selected, getter, static_cast(&nodes), nodes.size()); + }; + + // Gather all volumes in the scene + vNodePtr volumes = {}; + for (auto &node : frame->child("world").children()) + if (node.second->type() == sg::NodeType::GENERATOR + || node.second->type() == sg::NodeType::IMPORTER + || node.second->type() == sg::NodeType::VOLUME) { + auto volume = + findFirstNodeOfType(node.second, sg::NodeType::VOLUME); + if (volume) + volumes.push_back(volume); + } + + ImGui::Text("Volumes"); + ImGui::Text("(select to create an isosurface)"); + if (volumes.empty()) { + ImGui::Text("== empty == "); + + } else { + static int currentVolume = 0; + if (ListBox("##Volumes", ¤tVolume, volumes)) { + auto selected = volumes.at(currentVolume); + + auto &world = frame->childAs("world"); + + auto count = 1; + auto surfName = selected->name() + "_surf"; + while (world.hasChild(surfName + std::to_string(count) + "_xfm")) + count++; + surfName += std::to_string(count); + + auto isoXfm = + sg::createNode(surfName + "_xfm", "transform", affine3f{one}); + + auto valueRange = selected->child("valueRange").valueAs(); + + auto isoGeom = sg::createNode(surfName, "geometry_isosurfaces"); + isoGeom->createChild("valueRange", "range1f", valueRange); + isoGeom->child("valueRange").setSGOnly(); + isoGeom->createChild("isovalue", "float", valueRange.center()); + isoGeom->child("isovalue").setMinMax(valueRange.lower, valueRange.upper); + + uint32_t materialID = baseMaterialRegistry->baseMaterialOffSet(); + const std::vector mID = {materialID}; + auto mat = sg::createNode(surfName, "obj"); + + // Give it some editable parameters + mat->createChild("kd", "rgb", "diffuse color", vec3f(0.8f)); + mat->createChild("ks", "rgb", "specular color", vec3f(0.f)); + mat->createChild("ns", "float", "shininess [2-10e4]", 10.f); + mat->createChild("d", "float", "opacity [0-1]", 1.f); + mat->createChild("tf", "rgb", "transparency filter color", vec3f(0.f)); + mat->child("ns").setMinMax(2.f,10000.f); + mat->child("d").setMinMax(0.f,1.f); + + baseMaterialRegistry->add(mat); + isoGeom->createChildData("material", mID); + isoGeom->child("material").setSGOnly(); + + auto &handle = isoGeom->valueAs(); + handle.setParam("volume", selected->valueAs()); + + isoXfm->add(isoGeom); + + world.add(isoXfm); + } + } + + // Gather all isosurfaces in the scene + vNodePtr surfaces = {}; + for (auto &node : frame->child("world").children()) + if (node.second->type() == sg::NodeType::GENERATOR + || node.second->type() == sg::NodeType::IMPORTER + || node.second->type() == sg::NodeType::TRANSFORM) { + auto surface = + findFirstNodeOfType(node.second, sg::NodeType::GEOMETRY); + if (surface && (surface->subType() == "geometry_isosurfaces")) + surfaces.push_back(surface); + } + + ImGui::Separator(); + ImGui::Text("Isosurfaces"); + if (surfaces.empty()) { + ImGui::Text("== empty == "); + } else { + for (auto &surface : surfaces) { + GenerateWidget(*surface); + if (surface->isModified()) + break; + } + } + + ImGui::End(); +} + +void MultiWindows::buildWindowTransformEditor() +{ + if (!ImGui::Begin("Transform Editor", &showTransformEditor)) { + ImGui::End(); + return; + } + + typedef sg::NodeType NT; + + auto toggleSearch = [&](sg::SearchResults &results, bool visible) { + for (auto result : results) { + auto resultNode = result.lock(); + if (resultNode->hasChild("visible")) + resultNode->child("visible").setValue(visible); + } + }; + auto showSearch = [&](sg::SearchResults &r) { toggleSearch(r, true); }; + auto hideSearch = [&](sg::SearchResults &r) { toggleSearch(r, false); }; + + auto &warudo = frame->child("world"); + auto toggleDisplay = [&](bool visible) { + warudo.traverse(NT::GEOMETRY, "visible", visible); + warudo.traverse(NT::VOLUME, "visible", visible); + }; + auto showDisplay = [&]() { toggleDisplay(true); }; + auto hideDisplay = [&]() { toggleDisplay(false); }; + + static std::vector searchTypes{ + NT::IMPORTER, NT::TRANSFORM, NT::GENERATOR, NT::GEOMETRY, NT::VOLUME}; + static std::vector displayTypes{ + NT::IMPORTER, NT::TRANSFORM, NT::GENERATOR}; + static SearchWidget searchWidget(searchTypes, displayTypes); + + searchWidget.addSearchBarUI(warudo); + searchWidget.addCustomAction("show all", showSearch, showDisplay); + searchWidget.addCustomAction("hide all", hideSearch, hideDisplay, true); + searchWidget.addSearchResultsUI(warudo); + + auto selected = searchWidget.getSelected(); + if (selected) { + auto toggleSelected = [&](bool visible) { + selected->traverse(NT::GEOMETRY, "visible", visible); + selected->traverse(NT::VOLUME, "visible", visible); + }; + + ImGui::Text("Selected "); + ImGui::SameLine(); + if (ImGui::Button("show")) + toggleSelected(true); + ImGui::SameLine(); + if (ImGui::Button("hide")) + toggleSelected(false); + + GenerateWidget(*selected); + } + + ImGui::End(); +} + +void MultiWindows::buildWindowRenderingStats() +{ + ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration + | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings + | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav + | ImGuiWindowFlags_NoMove; + ImGui::SetNextWindowBgAlpha(0.75f); + + // Bottom left corner + static const float FROM_EDGE = 10.f; + ImVec2 window_pos(FROM_EDGE, ImGui::GetIO().DisplaySize.y - FROM_EDGE); + ImVec2 window_pos_pivot(0.f, 1.f); + ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot); + + if (!ImGui::Begin("Rendering stats", &showRenderingStats, flags)) { + ImGui::End(); + return; + } + + auto &fb = frame->childAs("framebuffer"); + auto variance = fb.variance(); + auto &v = frame->childAs("renderer")["varianceThreshold"]; + auto varianceThreshold = v.valueAs(); + + std::string mode = frame->child("navMode").valueAs() ? "Nav" : ""; + float scale = frame->child("scale" + mode).valueAs(); + + ImGui::Text("renderer: %s", rendererTypeStr.c_str()); + ImGui::Text("frame size: (%d,%d)", windowSize.x, windowSize.y); + ImGui::SameLine(); + ImGui::Text("x%1.2f", scale); + ImGui::Text("framerate: %-4.1f fps", latestFPS); + ImGui::Text("ui framerate: %-4.1f fps", ImGui::GetIO().Framerate); + + if (varianceThreshold == 0) { + ImGui::Text("variance : %-5.2f ", variance); + } else { + ImGui::Text("variance :"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(8 * ImGui::GetFontSize()); + float progress = varianceThreshold / variance; + char message[64]; + snprintf( + message, sizeof(message), "%.2f/%.2f", variance, varianceThreshold); + ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), message); + } + + if (frame->accumLimit == 0) { + ImGui::Text("accumulation: %d", frame->currentAccum); + } else { + ImGui::Text("accumulation:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(8 * ImGui::GetFontSize()); + float progress = float(frame->currentAccum) / frame->accumLimit; + char message[64]; + snprintf(message, + sizeof(message), + "%d/%d", + frame->currentAccum, + frame->accumLimit); + ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), message); + auto remaining = frame->accumLimit - frame->currentAccum; + if (remaining > 0) { + auto secondsPerFrame = 1.f / (latestFPS + 1e-6); + ImGui::SameLine(); + ImGui::Text( + "ETA: %4.2f s", int(remaining * secondsPerFrame * 100.f) / 100.f); + } + } + + ImGui::End(); +} + +} // namespace devel \ No newline at end of file diff --git a/app/MultiWindows.h b/app/MultiWindows.h new file mode 100644 index 00000000..a76f4f0f --- /dev/null +++ b/app/MultiWindows.h @@ -0,0 +1,255 @@ +// copied MainWindow.h and use "devel" namespace + +#pragma once + +#include "ospStudio.h" + +#include "ArcballCamera.h" +// glfw +#include +// ospray sg +#include "sg/Frame.h" +#include "sg/renderer/MaterialRegistry.h" +// std +#include + +#include +#include "widgets/AnimationWidget.h" +#include "widgets/GenerateImGuiWidgets.h" +#include "PluginManager.h" +#include "sg/importer/Importer.h" + +using namespace rkcommon::math; +using namespace ospray; +using rkcommon::make_unique; + +namespace devel { + +// on Windows often only GL 1.1 headers are present +// and Mac may be missing the float defines +#ifndef GL_CLAMP_TO_BORDER +#define GL_CLAMP_TO_BORDER 0x812D +#endif +#ifndef GL_FRAMEBUFFER_SRGB +#define GL_FRAMEBUFFER_SRGB 0x8DB9 +#endif +#ifndef GL_RGBA32F +#define GL_RGBA32F 0x8814 +#endif +#ifndef GL_RGB32F +#define GL_RGB32F 0x8815 +#endif + +enum class OSPRayRendererType +{ + SCIVIS, + PATHTRACER, + AO, + DEBUGGER, +#ifdef USE_MPI + MPIRAYCAST, +#endif + OTHER +}; + +struct SharedState +{ + bool quit; + + bool camChanged; + CameraState camState; + + bool sceneChanged; + int sceneStateSize; + std::string sceneState; + + SharedState(); +}; + +class MultiWindows : public StudioContext +{ + public: + + MultiWindows(StudioCommon &studioCommon); + + ~MultiWindows(); + + static MultiWindows *getActiveWindow(); + + GLFWwindow* getGLFWWindow(); + + void registerDisplayCallback(std::function callback); + + void registerKeyCallback( + std::function); + + void registerImGuiCallback(std::function callback); + + void mainLoop(); + + void addToCommandLine(std::shared_ptr app) override; + bool parseCommandLine() override; + + void start() override; + + void importFiles(sg::NodePtr world) override; + + std::shared_ptr getFrame(); + + bool timeseriesMode = false; + + std::stringstream windowTitle; + + void updateTitleBar(); + + void refreshRenderer(); + void saveRendererParams(); + + void changeToDefaultCamera(); + void updateCamera() override; + void setCameraState(CameraState &cs) override; + void refreshScene(bool resetCamera) override; + void clearScene(); + nlohmann::ordered_json getSceneState(); + int whichLightType{-1}; + int whichCamera{0}; + std::string lightTypeStr{"ambient"}; + std::string scene; + std::string rendererTypeStr; + + protected: + void buildPanel(); + void reshape(const vec2i &newWindowSize); + void motion(const vec2f &position); + void keyboardMotion(); + void mouseButton(const vec2f &position); + void mouseWheel(const vec2f &scroll); + void display(); + void startNewOSPRayFrame(); + void waitOnOSPRayFrame(); + void buildUI(); + void addLight(); + void removeLight(); + void addPTMaterials(); + + void saveCurrentFrame(); + void centerOnEyePos(); + void pickCenterOfRotation(float x, float y); + void pushLookMark(); + void popLookMark(); + + // menu and window UI + void buildMainMenu(); + void buildMainMenuFile(); + void buildMainMenuEdit(); + void buildMainMenuView(); + void buildMainMenuPlugins(); + + void buildWindows(); + void buildWindowRendererEditor(); + void buildWindowFrameBufferEditor(); + void buildWindowKeyframes(); + void buildWindowSnapshots(); + void buildWindowLightEditor(); + void buildWindowCameraEditor(); + void buildWindowMaterialEditor(); + void buildWindowTransferFunctionEditor(); + void buildWindowIsosurfaceEditor(); + void buildWindowTransformEditor(); + void buildWindowRenderingStats(); + + void setCameraSnapshot(size_t snapshot); + + std::vector cameraStack; + sg::NodePtr g_selectedSceneCamera; + + // Plugins // + std::vector> pluginPanels; + + // imgui window visibility toggles + bool showRendererEditor{false}; + bool showFrameBufferEditor{false}; + bool showKeyframes{false}; + bool showSnapshots{false}; + bool showLightEditor{false}; + bool showCameraEditor{false}; + bool showMaterialEditor{false}; + bool showTransferFunctionEditor{false}; + bool showIsosurfaceEditor{false}; + bool showTransformEditor{false}; + bool showRenderingStats{false}; + + // Option to always show a gamma corrected display to user. Native sRGB + // buffer is untouched, linear buffers are displayed as sRGB. + bool uiDisplays_sRGB{true}; + + static MultiWindows *activeWindow; + + int fontSize{13}; // pixels + vec2f contentScale{1.0f}; + vec2i windowSize; + vec2i fbSize; + vec2f previousMouse{-1.f}; + + OSPRayRendererType rendererType{OSPRayRendererType::SCIVIS}; + + rkcommon::FileName backPlateTexture = ""; + + // GLFW window instance + GLFWwindow *glfwWindow = nullptr; + + // OpenGL framebuffer texture + GLuint framebufferTexture = 0; + + // optional registered display callback, called before every display() + std::function displayCallback; + + // toggles display of ImGui UI, if an ImGui callback is provided + bool showUi = true; + + // optional registered ImGui callback, called during every frame to build UI + std::function uiCallback; + + // optional registered key callback, called when keys are pressed + std::function + keyCallback; + + // FPS measurement of last frame + float latestFPS{0.f}; + + // auto rotation speed, 1=0.1% window width mouse movement, 100=10% + int autorotateSpeed{1}; + + // Camera motion controls + float maxMoveSpeed{1.f}; + float fineControl{0.2f}; + float preFPVZoom{0.f}; + affine3f lastCamXfm{one}; + + // format used by glTexImage2D, as determined at context creation time + GLenum gl_rgb_format; + GLenum gl_rgba_format; + + std::shared_ptr animationWidget{nullptr}; + + // CLI + bool optShowColor{true}; + bool optShowAlbedo{false}; + bool optShowDepth{false}; + bool optShowDepthInvert{false}; + bool optAutorotate{false}; + bool optAnimate{false}; + + // configuration for displays + nlohmann::ordered_json configDisplay; + // the state to be sent out over MPI to the other rendering processes + SharedState sharedState; + // three corners of a display + vec3f topLeftLocal; + vec3f botLeftLocal; + vec3f botRightLocal; +}; + +} // namespace devel \ No newline at end of file diff --git a/app/ospStudio.cpp b/app/ospStudio.cpp index f0d7dd79..15b3ce8e 100644 --- a/app/ospStudio.cpp +++ b/app/ospStudio.cpp @@ -6,6 +6,7 @@ #include "MainWindow.h" #include "Batch.h" #include "TimeSeriesWindow.h" +#include "MultiWindows.h" #include "sg/Mpi.h" // CLI @@ -380,6 +381,11 @@ int main(int argc, const char *argv[]) case StudioMode::BENCHMARK: context = std::make_shared(studioCommon); break; +#endif +#ifdef USE_MPI + case StudioMode::MULTIWINDOWS: + context = std::make_shared(studioCommon); + break; #endif default: std::cerr << "unknown mode! How did I get here?!\n"; diff --git a/app/ospStudio.h b/app/ospStudio.h index f9c6449e..d6445070 100644 --- a/app/ospStudio.h +++ b/app/ospStudio.h @@ -42,6 +42,9 @@ enum class StudioMode #ifdef USE_BENCHMARK BENCHMARK, #endif +#ifdef USE_MPI + MULTIWINDOWS, +#endif }; const static std::map StudioModeMap = { @@ -52,6 +55,9 @@ const static std::map StudioModeMap = { #ifdef USE_BENCHMARK {"benchmark", StudioMode::BENCHMARK}, #endif +#ifdef USE_MPI + {"multiwindows", StudioMode::MULTIWINDOWS}, +#endif }; const static std::map standardResolutionSizeMap = { diff --git a/app/widgets/Panel.h b/app/widgets/Panel.h index bbc730e0..71d30982 100644 --- a/app/widgets/Panel.h +++ b/app/widgets/Panel.h @@ -28,6 +28,9 @@ struct PANEL_INTERFACE Panel virtual void buildUI(void* ImGuiCtx) = 0; + // Process 'key' even though the UI is not shown, e.g, check "update" to run something in every frame in mainLoop() + virtual void process(std::string key) {}; + // Controls to show/hide the panel in the app // void setShown(bool shouldBeShown); diff --git a/example-config/display_settings.json b/example-config/display_settings.json new file mode 100644 index 00000000..dfc6d73c --- /dev/null +++ b/example-config/display_settings.json @@ -0,0 +1,56 @@ +[ +{ + "hostName": "localhost", + + "topLeft": [-0.178950, 0.122950, -1.000000], + "botLeft": [-0.178950, -0.122950, -1.000000], + "botRight": [0.178950, -0.122950, -1.000000], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.006320, + "mullionRight": 0.006320, + "mullionTop": 0.015056, + "mullionBottom": 0.015056, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 1024, + "screenHeight": 640 +}, +{ + "hostName": "localhost", + + "topLeft": [-0.178950, 0.122950, -1.000000], + "botLeft": [-0.178950, -0.122950, -1.000000], + "botRight": [0.000000, -0.122950, -1.000000], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.006320, + "mullionRight": 0.000000, + "mullionTop": 0.015056, + "mullionBottom": 0.015056, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 896, + "screenHeight": 1120 +}, +{ + "hostName": "localhost", + + "topLeft": [0.000000, 0.122950, -1.000000], + "botLeft": [0.000000, -0.122950, -1.000000], + "botRight": [0.178950, -0.122950, -1.000000], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.000000, + "mullionRight": 0.006320, + "mullionTop": 0.015056, + "mullionBottom": 0.015056, + + "display": 0, + "screenX": 896, + "screenY": 0, + "screenWidth": 896, + "screenHeight": 1120 +} +] \ No newline at end of file diff --git a/example-config/rattler.json b/example-config/rattler.json new file mode 100644 index 00000000..e29fe4f6 --- /dev/null +++ b/example-config/rattler.json @@ -0,0 +1,344 @@ +[ +{ + "hostName": "localhost", + + "topLeft": [-1.887851, 1.078200, 0.613400], + "botLeft": [-1.887851, -1.078200, 0.613400], + "botRight": [1.887851, -1.078200, 0.613400], + "eye": [0.000000, 0.000000, 2.501251], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 1024, + "screenHeight": 577 +}, +{ + "hostName": "r07", + + "topLeft": [-1.887851, 1.078200, 0.613400], + "botLeft": [-1.887851, 0.359400, 0.613400], + "botRight": [-1.887851, 0.359400, -0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r08", + + "topLeft": [-1.887851, 0.359400, 0.613400], + "botLeft": [-1.887851, -0.359400, 0.613400], + "botRight": [-1.887851, -0.359400, -0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r09", + + "topLeft": [-1.887851, -0.359400, 0.613400], + "botLeft": [-1.887851, -1.078200, 0.613400], + "botRight": [-1.887851, -1.078200, -0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r04", + + "topLeft": [-1.887851, 1.078200, -0.613400], + "botLeft": [-1.887851, 0.359400, -0.613400], + "botRight": [-1.166756, 0.359400, -1.605902], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r05", + + "topLeft": [-1.887851, 0.359400, -0.613400], + "botLeft": [-1.887851, -0.359400, -0.613400], + "botRight": [-1.166756, -0.359400, -1.605902], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r06", + + "topLeft": [-1.887851, -0.359400, -0.613400], + "botLeft": [-1.887851, -1.078200, -0.613400], + "botRight": [-1.166756, -1.078200, -1.605902], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r10", + + "topLeft": [-1.166756, 1.078200, -1.605902], + "botLeft": [-1.166756, 0.359400, -1.605902], + "botRight": [0.000000, 0.359400, -1.985004], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r11", + + "topLeft": [-1.166756, 0.359400, -1.605902], + "botLeft": [-1.166756, -0.359400, -1.605902], + "botRight": [0.000000, -0.359400, -1.985004], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r12", + + "topLeft": [-1.166756, -0.359400, -1.605902], + "botLeft": [-1.166756, -1.078200, -1.605902], + "botRight": [0.000000, -1.078200, -1.985004], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r13", + + "topLeft": [0.000000, 1.078200, -1.985004], + "botLeft": [0.000000, 0.359400, -1.985004], + "botRight": [1.166756, 0.359400, -1.605902], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r14", + + "topLeft": [0.000000, 0.359400, -1.985004], + "botLeft": [0.000000, -0.359400, -1.985004], + "botRight": [1.166756, -0.359400, -1.605902], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r15", + + "topLeft": [0.000000, -0.359400, -1.985004], + "botLeft": [0.000000, -1.078200, -1.985004], + "botRight": [1.166756, -1.078200, -1.605902], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r16", + + "topLeft": [1.166756, 1.078200, -1.605902], + "botLeft": [1.166756, 0.359400, -1.605902], + "botRight": [1.887851, 0.359400, -0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r17", + + "topLeft": [1.166756, 0.359400, -1.605902], + "botLeft": [1.166756, -0.359400, -1.605902], + "botRight": [1.887851, -0.359400, -0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r18", + + "topLeft": [1.166756, -0.359400, -1.605902], + "botLeft": [1.166756, -1.078200, -1.605902], + "botRight": [1.887851, -1.078200, -0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r01", + + "topLeft": [1.887851, 1.078200, -0.613400], + "botLeft": [1.887851, 0.359400, -0.613400], + "botRight": [1.887851, 0.359400, 0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r02", + + "topLeft": [1.887851, 0.359400, -0.613400], + "botLeft": [1.887851, -0.359400, -0.613400], + "botRight": [1.887851, -0.359400, 0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r03", + + "topLeft": [1.887851, -0.359400, -0.613400], + "botLeft": [1.887851, -1.078200, -0.613400], + "botRight": [1.887851, -1.078200, 0.613400], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +} +] \ No newline at end of file diff --git a/example-config/rattler_planar.json b/example-config/rattler_planar.json new file mode 100644 index 00000000..860d96d5 --- /dev/null +++ b/example-config/rattler_planar.json @@ -0,0 +1,344 @@ +[ +{ + "hostName": "localhost", + + "topLeft": [-3.680400, 1.078200, -1.887851], + "botLeft": [-3.680400, -1.078200, -1.887851], + "botRight": [3.680400, -1.078200, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 1024, + "screenHeight": 295 +}, +{ + "hostName": "r07", + + "topLeft": [-3.680400, 1.078200, -1.887851], + "botLeft": [-3.680400, 0.359400, -1.887851], + "botRight": [-2.453600, 0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r08", + + "topLeft": [-3.680400, 0.359400, -1.887851], + "botLeft": [-3.680400, -0.359400, -1.887851], + "botRight": [-2.453600, -0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r09", + + "topLeft": [-3.680400, -0.359400, -1.887851], + "botLeft": [-3.680400, -1.078200, -1.887851], + "botRight": [-2.453600, -1.078200, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r04", + + "topLeft": [-2.453600, 1.078200, -1.887851], + "botLeft": [-2.453600, 0.359400, -1.887851], + "botRight": [-1.226800, 0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r05", + + "topLeft": [-2.453600, 0.359400, -1.887851], + "botLeft": [-2.453600, -0.359400, -1.887851], + "botRight": [-1.226800, -0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r06", + + "topLeft": [-2.453600, -0.359400, -1.887851], + "botLeft": [-2.453600, -1.078200, -1.887851], + "botRight": [-1.226800, -1.078200, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r10", + + "topLeft": [-1.226800, 1.078200, -1.887851], + "botLeft": [-1.226800, 0.359400, -1.887851], + "botRight": [0.000000, 0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r11", + + "topLeft": [-1.226800, 0.359400, -1.887851], + "botLeft": [-1.226800, -0.359400, -1.887851], + "botRight": [0.000000, -0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r12", + + "topLeft": [-1.226800, -0.359400, -1.887851], + "botLeft": [-1.226800, -1.078200, -1.887851], + "botRight": [0.000000, -1.078200, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r13", + + "topLeft": [0.000000, 1.078200, -1.887851], + "botLeft": [0.000000, 0.359400, -1.887851], + "botRight": [1.226800, 0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r14", + + "topLeft": [0.000000, 0.359400, -1.887851], + "botLeft": [0.000000, -0.359400, -1.887851], + "botRight": [1.226800, -0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r15", + + "topLeft": [0.000000, -0.359400, -1.887851], + "botLeft": [0.000000, -1.078200, -1.887851], + "botRight": [1.226800, -1.078200, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r16", + + "topLeft": [1.226800, 1.078200, -1.887851], + "botLeft": [1.226800, 0.359400, -1.887851], + "botRight": [2.453600, 0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r17", + + "topLeft": [1.226800, 0.359400, -1.887851], + "botLeft": [1.226800, -0.359400, -1.887851], + "botRight": [2.453600, -0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r18", + + "topLeft": [1.226800, -0.359400, -1.887851], + "botLeft": [1.226800, -1.078200, -1.887851], + "botRight": [2.453600, -1.078200, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r01", + + "topLeft": [2.453600, 1.078200, -1.887851], + "botLeft": [2.453600, 0.359400, -1.887851], + "botRight": [3.680400, 0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r02", + + "topLeft": [2.453600, 0.359400, -1.887851], + "botLeft": [2.453600, -0.359400, -1.887851], + "botRight": [3.680400, -0.359400, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +}, +{ + "hostName": "r03", + + "topLeft": [2.453600, -0.359400, -1.887851], + "botLeft": [2.453600, -1.078200, -1.887851], + "botRight": [3.680400, -1.078200, -1.887851], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.011326, + "mullionRight": 0.011326, + "mullionTop": 0.020733, + "mullionBottom": 0.020733, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 3840, + "screenHeight": 2160 +} +] \ No newline at end of file diff --git a/example-config/tracking_settings.json b/example-config/tracking_settings.json new file mode 100644 index 00000000..debcdd99 --- /dev/null +++ b/example-config/tracking_settings.json @@ -0,0 +1,10 @@ +{ + "ipAddress": "127.0.0.1", + "portNumber": 8888, + + "scaleOffset": [0.001, -0.001, -0.001], + "translationOffset": [0.0, -0.1, 1.19], + "confidenceLevelThreshold": 1, + "leaningAngleThreshold": 1.0, + "leaningDirScaleFactor": [1.0, 1.0, 1.0] +} \ No newline at end of file diff --git a/example-config/two_displays.json b/example-config/two_displays.json new file mode 100644 index 00000000..28d80946 --- /dev/null +++ b/example-config/two_displays.json @@ -0,0 +1,56 @@ +[ +{ + "hostName": "localhost", + + "topLeft": [-0.357900, 0.320370, -1.000000], + "botLeft": [-0.357900, -0.320370, -1.000000], + "botRight": [0.379660, -0.320370, -1.000000], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.006320, + "mullionRight": 0.021536, + "mullionTop": 0.021180, + "mullionBottom": 0.021180, + + "display": 1, + "screenX": 0, + "screenY": 0, + "screenWidth": 840, + "screenHeight": 708 +}, +{ + "hostName": "localhost", + + "topLeft": [-0.357900, 0.122950, -1.000000], + "botLeft": [-0.357900, -0.122950, -1.000000], + "botRight": [0.000000, -0.122950, -1.000000], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.006320, + "mullionRight": 0.006320, + "mullionTop": 0.015056, + "mullionBottom": 0.015056, + + "display": 0, + "screenX": 0, + "screenY": 0, + "screenWidth": 1792, + "screenHeight": 1120 +}, +{ + "hostName": "localhost", + + "topLeft": [0.000000, 0.320370, -1.000000], + "botLeft": [0.000000, -0.320370, -1.000000], + "botRight": [0.379660, -0.320370, -1.000000], + "eye": [0.000000, 0.000000, 0.000000], + "mullionLeft": 0.021536, + "mullionRight": 0.021536, + "mullionTop": 0.021180, + "mullionBottom": 0.021180, + + "display": 1, + "screenX": 0, + "screenY": 0, + "screenWidth": 1440, + "screenHeight": 2560 +} +] \ No newline at end of file diff --git a/plugins/example_plugin/CMakeLists.txt b/plugins/example_plugin/CMakeLists.txt index cecaaa39..f6d366e2 100644 --- a/plugins/example_plugin/CMakeLists.txt +++ b/plugins/example_plugin/CMakeLists.txt @@ -11,7 +11,7 @@ if (BUILD_PLUGIN_EXAMPLE) PanelExample.cpp ) - target_link_libraries(${pluginName} ospray_sg) + target_link_libraries(${pluginName} ospray_sg ospray_ui) # Only link against imgui if needed (ie, pure file importers don't) target_link_libraries(${pluginName} imgui) diff --git a/plugins/gesture_plugin/CMakeLists.txt b/plugins/gesture_plugin/CMakeLists.txt new file mode 100644 index 00000000..9e9da39f --- /dev/null +++ b/plugins/gesture_plugin/CMakeLists.txt @@ -0,0 +1,33 @@ +option(BUILD_PLUGIN_GESTURE "Gesture plugin" OFF) + +if (BUILD_PLUGIN_GESTURE) + set(pluginName "ospray_studio_plugin_gesture") + + add_library(${pluginName} SHARED + plugin_gesture.cpp + PanelGesture.cpp + tracker/TrackingManager.cpp + ) + + target_link_libraries(${pluginName} ospray_sg ospray_ui) + + # Only link against imgui if needed (ie, pure file importers don't) + target_link_libraries(${pluginName} imgui) + + # Add external libraries for this plugin + add_subdirectory(external) + target_link_libraries(${pluginName} network) + + target_include_directories(${pluginName} + PRIVATE ${CMAKE_SOURCE_DIR} + ) + + install(TARGETS ${pluginName} + DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT lib + # on Windows put the dlls into bin + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT lib + ) + +endif() diff --git a/plugins/gesture_plugin/PanelGesture.cpp b/plugins/gesture_plugin/PanelGesture.cpp new file mode 100644 index 00000000..da18eeee --- /dev/null +++ b/plugins/gesture_plugin/PanelGesture.cpp @@ -0,0 +1,105 @@ +#include + +#include "PanelGesture.h" + +#include "app/widgets/GenerateImGuiWidgets.h" + +#include "sg/JSONDefs.h" +#include "sg/Math.h" +#include "sg/JSONDefs.h" + +#include "imgui.h" + +namespace ospray { +namespace gesture_plugin { + +PanelGesture::PanelGesture(std::shared_ptr _context, std::string _panelName, std::string _configFilePath) + : Panel(_panelName.c_str(), _context) + , panelName(_panelName) + , configFilePath(_configFilePath) +{ + trackingManager.reset(new TrackingManager(configFilePath)); +} + +void PanelGesture::buildUI(void *ImGuiCtx) +{ + // Need to set ImGuiContext in *this* address space + ImGui::SetCurrentContext((ImGuiContext *)ImGuiCtx); + ImGui::OpenPopup(panelName.c_str()); + + if (!ImGui::BeginPopupModal(panelName.c_str(), nullptr, ImGuiWindowFlags_None)) return; + + if (trackingManager->isRunning()) { + ImGui::Text("%s", "Currently connected to the server..."); + + if (ImGui::Button("Disconnect")) { + trackingManager->close(); + } + } + else { + ImGui::Text("%s", "Currently NOT connected to the server..."); + std::string str = "- Connect to " + trackingManager->ipAddress + ":" + std::to_string(trackingManager->portNumber); + ImGui::Text("%s", str.c_str()); + + if (ImGui::Button("Connect")) { + trackingManager->start(); + } + } + ImGui::Separator(); + + // Close button + if (ImGui::Button("Close")) { + setShown(false); + ImGui::CloseCurrentPopup(); + } + ImGui::Separator(); + + if (ImGui::CollapsingHeader("Configuration", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("%s", "Offset(s)"); + ImGui::DragFloat3("Scale", trackingManager->scaleOffset, 0.001, -100, 100, "%.3f"); + ImGui::DragFloat3("Translate", trackingManager->translationOffset, 0, -100, 100, "%.1f"); + + ImGui::Text("%s", "Gestures(s)"); + ImGui::DragInt("Confidence Level Threshold", &trackingManager->confidenceLevelThreshold, K4ABT_JOINT_CONFIDENCE_LOW, K4ABT_JOINT_CONFIDENCE_NONE, K4ABT_JOINT_CONFIDENCE_LEVELS_COUNT); + ImGui::DragFloat("Leaning Angle Threshold", &trackingManager->leaningAngleThreshold, 1, 0, 180); + ImGui::DragFloat3("Leaning Dir Scale", trackingManager->leaningDirScaleFactor, 0, -100, 100, "%.1f"); + + ImGui::Separator(); + if (ImGui::Button("Save")) { + trackingManager->saveConfig(this->configFilePath); + } + // ImGui::SameLine(); + // if (ImGui::Button("Load")) { + // // TODO + // // loadConfig(); + // } + } + ImGui::Separator(); + + // Display statuses in a scrolling region + if (ImGui::CollapsingHeader("Status", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::BeginChild("Scrolling", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysAutoResize); + for (std::string status : trackingManager->statuses) { + ImGui::Text("%s", status.c_str()); + } + ImGui::EndChild(); + } + ImGui::Separator(); + + ImGui::EndPopup(); +} + +void PanelGesture::process(std::string key) { + if (key == "update") { + TrackingState state = trackingManager->pollState(); + if (state.mode == INTERACTION_FLYING) { + context->arcballCamera->move(state.leaningDir); + } + } + else if (key == "start") { + trackingManager->start(); + } +} + +} // namespace gesture_plugin +} // namespace ospray diff --git a/plugins/gesture_plugin/PanelGesture.h b/plugins/gesture_plugin/PanelGesture.h new file mode 100644 index 00000000..bf2d5bff --- /dev/null +++ b/plugins/gesture_plugin/PanelGesture.h @@ -0,0 +1,27 @@ +#pragma once + +#include "app/widgets/Panel.h" +#include "app/ospStudio.h" + +#include "tracker/TrackingManager.h" + +namespace ospray { +namespace gesture_plugin { + +struct PanelGesture : public Panel +{ + PanelGesture(std::shared_ptr _context, std::string _panelName, std::string _configFilePath); + + void buildUI(void *ImGuiCtx) override; + + void process(std::string key) override; + +private: + std::string panelName; + std::string configFilePath; + + std::unique_ptr trackingManager; +}; + +} // namespace gesture_plugin +} // namespace ospray diff --git a/plugins/gesture_plugin/external/CMakeLists.txt b/plugins/gesture_plugin/external/CMakeLists.txt new file mode 100644 index 00000000..fc999e8a --- /dev/null +++ b/plugins/gesture_plugin/external/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(async-sockets) \ No newline at end of file diff --git a/plugins/gesture_plugin/external/async-sockets/CMakeLists.txt b/plugins/gesture_plugin/external/async-sockets/CMakeLists.txt new file mode 100644 index 00000000..8d9267dd --- /dev/null +++ b/plugins/gesture_plugin/external/async-sockets/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(network INTERFACE) +target_include_directories(network + INTERFACE $) \ No newline at end of file diff --git a/plugins/gesture_plugin/external/async-sockets/basesocket.hpp b/plugins/gesture_plugin/external/async-sockets/basesocket.hpp new file mode 100644 index 00000000..47312430 --- /dev/null +++ b/plugins/gesture_plugin/external/async-sockets/basesocket.hpp @@ -0,0 +1,146 @@ +// The Linux and Mac implementations are from https://github.com/eminfedar/async-sockets-cpp +// Updated it to suport Windows. +#pragma once + +#if defined(__linux__) || defined(__APPLE__) + +#include +#include +#include +#include +#include + +#include +#include +#include + +#define FDR_UNUSED(expr){ (void)(expr); } +#define FDR_ON_ERROR std::function onError = [](int errorCode, std::string errorMessage){FDR_UNUSED(errorCode); FDR_UNUSED(errorMessage)} + +class BaseSocket +{ +// Definitions +public: + enum SocketType + { + TCP = SOCK_STREAM, + UDP = SOCK_DGRAM + }; + const uint16_t BUFFER_SIZE = 0xFFFF; + sockaddr_in address; + bool isClosed = false; + +protected: + int sock = 0; + static std::string ipToString(sockaddr_in addr) + { + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(addr.sin_addr), ip, INET_ADDRSTRLEN); + + return std::string(ip); + } + + BaseSocket(FDR_ON_ERROR, SocketType sockType = TCP, int socketId = -1) + { + if (socketId < 0) + { + if ((this->sock = socket(AF_INET, sockType, 0)) < 0) + { + onError(errno, "Socket creating error."); + } + } + else + { + this->sock = socketId; + } + } + +// Methods +public: + virtual void Close() { + if(isClosed) return; + + isClosed = true; + close(this->sock); + } + + std::string remoteAddress() {return ipToString(this->address);} + int remotePort() {return ntohs(this->address.sin_port);} + int fileDescriptor() const { return this->sock; } +}; + +#elif _WIN32 + +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +#include +#include +#include + +// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib +#pragma comment (lib, "Ws2_32.lib") +#pragma comment (lib, "Mswsock.lib") +#pragma comment (lib, "AdvApi32.lib") + +#define DEFAULT_BUFLEN 512 +#define FDR_UNUSED(expr){ (void)(expr); } +#define FDR_ON_ERROR std::function onError = [](int errorCode, std::string errorMessage){FDR_UNUSED(errorCode); FDR_UNUSED(errorMessage)} + +class BaseSocket +{ +// Definitions +public: + sockaddr_in address; + bool isClosed = false; + +protected: + SOCKET sock = INVALID_SOCKET; + + static std::string ipToString(sockaddr_in addr) { + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(addr.sin_addr), ip, INET_ADDRSTRLEN); + + return std::string(ip); + } + + BaseSocket(FDR_ON_ERROR, int socketId = -1) { + // Initialize Winsock + WSADATA wsaData; + int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (iResult != 0) { + onError(errno, "WSAStartup failed with error: " + std::to_string(iResult)); + return; + } + + if (socketId < 0) { + // Create a SOCKET to connect to server or to listen for client connections + this->sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (this->sock == INVALID_SOCKET) { + onError(errno, "socket failed with error: " + std::to_string(WSAGetLastError())); + WSACleanup(); + return; + } + } else { + this->sock = (SOCKET) socketId; + } + } + +// Methods +public: + virtual void Close() { + if (isClosed) return; + + isClosed = true; + closesocket(this->sock); + WSACleanup(); + } + + std::string remoteAddress() { return ipToString(this->address); } + int remotePort() { return ntohs(this->address.sin_port); } +}; + +#endif \ No newline at end of file diff --git a/plugins/gesture_plugin/external/async-sockets/tcpserver.hpp b/plugins/gesture_plugin/external/async-sockets/tcpserver.hpp new file mode 100644 index 00000000..8e9096ec --- /dev/null +++ b/plugins/gesture_plugin/external/async-sockets/tcpserver.hpp @@ -0,0 +1,175 @@ +// The Linux and Mac implementations are from https://github.com/eminfedar/async-sockets-cpp +// Updated it to suport Windows. +#pragma once + +#if defined(__linux__) || defined(__APPLE__) + +#include "tcpsocket.hpp" +#include + +class TCPServer : public BaseSocket +{ +public: + // Event Listeners: + std::function onNewConnection = [](TCPSocket* sock){FDR_UNUSED(sock)}; + + explicit TCPServer(FDR_ON_ERROR): BaseSocket(onError, SocketType::TCP) + { + int opt = 1; + setsockopt(this->sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int)); + setsockopt(this->sock,SOL_SOCKET,SO_REUSEPORT,&opt,sizeof(int)); + } + + // Binding the server. + void Bind(const char *address, uint16_t port, FDR_ON_ERROR) + { + if (inet_pton(AF_INET, address, &this->address.sin_addr) <= 0) + { + onError(errno, "Invalid address. Address type not supported."); + return; + } + + this->address.sin_family = AF_INET; + this->address.sin_port = htons(port); + + if (bind(this->sock, (const sockaddr *)&this->address, sizeof(this->address)) < 0) + { + onError(errno, "Cannot bind the socket."); + return; + } + } + void Bind(int port, FDR_ON_ERROR) { this->Bind("0.0.0.0", port, onError); } + + // Start listening the server. + void Listen(FDR_ON_ERROR) + { + if (listen(this->sock, 20) < 0) + { + onError(errno, "Error: Server can't listen the socket."); + return; + } + + std::thread t(Accept, this, onError); + t.detach(); + } + + // Overriding Close to add shutdown(): + void Close() + { + shutdown(this->sock, SHUT_RDWR); + + BaseSocket::Close(); + } + +private: + static void Accept(TCPServer *server, FDR_ON_ERROR) + { + sockaddr_in newSocketInfo; + socklen_t newSocketInfoLength = sizeof(newSocketInfo); + + int newSock; + while (!server->isClosed) + { + while ((newSock = accept(server->sock, (sockaddr *)&newSocketInfo, &newSocketInfoLength)) < 0) + { + if (errno == EBADF || errno == EINVAL) return; + + onError(errno, "Error while accepting a new connection."); + return; + } + + if (!server->isClosed && newSock >= 0) + { + TCPSocket *newSocket = new TCPSocket(onError, newSock); + newSocket->deleteAfterClosed = true; + newSocket->setAddressStruct(newSocketInfo); + + server->onNewConnection(newSocket); + newSocket->Listen(); + } + } + } +}; + +#elif _WIN32 + +#include "tcpsocket.hpp" + +#include +#include + +class TCPServer : public BaseSocket +{ +public: + // Event Listeners: + std::function onNewConnection = [](TCPSocket* sock){FDR_UNUSED(sock)}; + + explicit TCPServer(FDR_ON_ERROR): BaseSocket(onError) { + BOOL bOptVal = TRUE; + setsockopt(this->sock, SOL_SOCKET, SO_REUSEADDR, (char *) &bOptVal, sizeof(BOOL)); + } + + // Binding the server. + void Bind(const char *address, uint16_t port, FDR_ON_ERROR) { + if (inet_pton(AF_INET, address, &this->address.sin_addr) <= 0) { + onError(errno, "Invalid address. Address type not supported."); + return; + } + + this->address.sin_family = AF_INET; + this->address.sin_port = htons(port); + + if (bind(this->sock, (const sockaddr *)&this->address, sizeof(this->address)) == SOCKET_ERROR) { + onError(errno, "Cannot bind the socket."); + //// + return; + } + } + void Bind(int port, FDR_ON_ERROR) { this->Bind("0.0.0.0", port, onError); } + + // Start listening the server. + void Listen(FDR_ON_ERROR) { + if (listen(this->sock, 20) == SOCKET_ERROR) { + onError(errno, "Error: Server can't listen the socket."); + return; + } + + std::thread t(Accept, this, onError); + t.detach(); + } + + // Overriding Close to add shutdown(): + void Close() { + shutdown(this->sock, SD_BOTH); + + BaseSocket::Close(); + } + +private: + static void Accept(TCPServer *server, FDR_ON_ERROR) { + sockaddr_in newSocketInfo; + socklen_t newSocketInfoLength = sizeof(newSocketInfo); + + SOCKET newSock = 0; + while (!server->isClosed) { + newSock = accept(server->sock, (sockaddr *)&newSocketInfo, &newSocketInfoLength); + if (newSock == INVALID_SOCKET) { + if (errno == EBADF || errno == EINVAL) return; + + onError(errno, "Error while accepting a new connection." + std::to_string(WSAGetLastError())); + return; + } + + if (!server->isClosed && newSock >= 0) { + TCPSocket *newSocket = new TCPSocket(onError, newSock); + newSocket->deleteAfterClosed = true; + newSocket->setAddressStruct(newSocketInfo); + + server->onNewConnection(newSocket); + newSocket->Listen(); + } + } + } +}; + +#endif \ No newline at end of file diff --git a/plugins/gesture_plugin/external/async-sockets/tcpsocket.hpp b/plugins/gesture_plugin/external/async-sockets/tcpsocket.hpp new file mode 100644 index 00000000..ea12f9e8 --- /dev/null +++ b/plugins/gesture_plugin/external/async-sockets/tcpsocket.hpp @@ -0,0 +1,258 @@ +// The Linux and Mac implementations are from https://github.com/eminfedar/async-sockets-cpp +// Updated it to suport Windows. +#pragma once + +#if defined(__linux__) || defined(__APPLE__) + +#include "basesocket.hpp" +#include +#include +#include +// copied from https://github.com/eminfedar/async-sockets-cpp + +#include + +class TCPSocket : public BaseSocket +{ +public: + // Event Listeners: + std::function onMessageReceived; + std::function onRawMessageReceived; + std::function onSocketClosed; + + explicit TCPSocket(FDR_ON_ERROR, int socketId = -1) : BaseSocket(onError, TCP, socketId){} + + // Send TCP Packages + int Send(const char *bytes, size_t byteslength) + { + if (this->isClosed) + return -1; + + int sent = 0; + if ((sent = send(this->sock, bytes, byteslength, 0)) < 0) + { + perror("send"); + } + return sent; + } + int Send(std::string message) { return this->Send(message.c_str(), message.length()); } + + void Connect(std::string host, uint16_t port, std::function onConnected = [](){}, FDR_ON_ERROR) + { + struct addrinfo hints, *res, *it; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + // Get address info from DNS + int status; + if ((status = getaddrinfo(host.c_str(), NULL, &hints, &res)) != 0) { + onError(errno, "Invalid address." + std::string(gai_strerror(status))); + return; + } + + for(it = res; it != NULL; it = it->ai_next) + { + if (it->ai_family == AF_INET) { // IPv4 + memcpy((void*)(&this->address), (void*)it->ai_addr, sizeof(sockaddr_in)); + break; // for now, just get first ip (ipv4). + } + } + + freeaddrinfo(res); + + this->Connect((uint32_t)this->address.sin_addr.s_addr, port, onConnected, onError); + } + void Connect(uint32_t ipv4, uint16_t port, std::function onConnected = [](){}, FDR_ON_ERROR) + { + this->address.sin_family = AF_INET; + this->address.sin_port = htons(port); + this->address.sin_addr.s_addr = ipv4; + + this->setTimeout(5); + + // Try to connect. + if (connect(this->sock, (const sockaddr *)&this->address, sizeof(sockaddr_in)) < 0) + { + onError(errno, "Connection failed to the host."); + this->setTimeout(0); + return; + } + + this->setTimeout(0); + + // Connected to the server, fire the event. + onConnected(); + + // Start listening from server: + this->Listen(); + } + + void Listen() + { + std::thread t(TCPSocket::Receive, this); + t.detach(); + } + + void setAddressStruct(sockaddr_in addr) {this->address = addr;} + sockaddr_in getAddressStruct() const {return this->address;} + + bool deleteAfterClosed = false; + +private: + static void Receive(TCPSocket *socket) + { + char tempBuffer[socket->BUFFER_SIZE]; + int messageLength; + + while ((messageLength = recv(socket->sock, tempBuffer, socket->BUFFER_SIZE, 0)) > 0) + { + tempBuffer[messageLength] = '\0'; + if(socket->onMessageReceived) + socket->onMessageReceived(std::string(tempBuffer, messageLength)); + + if(socket->onRawMessageReceived) + socket->onRawMessageReceived(tempBuffer, messageLength); + } + + socket->Close(); + if(socket->onSocketClosed) + socket->onSocketClosed(errno); + + if (socket->deleteAfterClosed && socket != nullptr) + delete socket; + } + + void setTimeout(int seconds) + { + struct timeval tv; + tv.tv_sec = seconds; + tv.tv_usec = 0; + + setsockopt(this->sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(this->sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + } +}; + +#elif _WIN32 + +#include "basesocket.hpp" + +#include +#include +#include + +class TCPSocket : public BaseSocket +{ +public: + // Event Listeners: + std::function onMessageReceived; + std::function onRawMessageReceived; + std::function onSocketClosed; + + explicit TCPSocket(FDR_ON_ERROR, int socketId = -1) : BaseSocket(onError, socketId){} + + // Send TCP Packages + int Send(const char *bytes, size_t byteslength) { + if (this->isClosed) + return -1; + + int sent = send(this->sock, bytes, byteslength, 0); + if (sent == SOCKET_ERROR) { + perror("send failed..."); + } + return sent; + } + int Send(std::string message) { return this->Send(message.c_str(), message.length()); } + + void Connect(std::string host, uint16_t port, std::function onConnected = [](){}, FDR_ON_ERROR) { + struct addrinfo hints, *res, *it; + ZeroMemory( &hints, sizeof(hints) ); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + // Get address info from DNS + int status; + if ((status = getaddrinfo(host.c_str(), NULL, &hints, &res)) != 0) { + onError(errno, "Invalid address." + std::string(gai_strerror(status))); + return; + } + + for(it = res; it != NULL; it = it->ai_next) { + if (it->ai_family == AF_INET) { // IPv4 + memcpy((void*)(&this->address), (void*)it->ai_addr, sizeof(sockaddr_in)); + break; // for now, just get first ip (ipv4). + } + } + + freeaddrinfo(res); + + this->Connect((uint32_t)this->address.sin_addr.s_addr, port, onConnected, onError); + } + void Connect(uint32_t ipv4, uint16_t port, std::function onConnected = [](){}, FDR_ON_ERROR) { + this->address.sin_family = AF_INET; + this->address.sin_port = htons(port); + this->address.sin_addr.s_addr = ipv4; + + this->setTimeout(5); + + // Try to connect. + if (connect(this->sock, (const sockaddr *)&this->address, sizeof(sockaddr_in)) == SOCKET_ERROR) { + onError(errno, "Connection failed to the host."); + this->setTimeout(0); + return; + } + + this->setTimeout(0); + + // Connected to the server, fire the event. + onConnected(); + + // Start listening from server: + this->Listen(); + } + + void Listen() { + std::thread t(TCPSocket::Receive, this); + t.detach(); + } + + void setAddressStruct(sockaddr_in addr) { this->address = addr; } + sockaddr_in getAddressStruct() const { return this->address; } + + bool deleteAfterClosed = false; + +private: + static void Receive(TCPSocket *socket) { + char tempBuffer[DEFAULT_BUFLEN]; + int messageLength = 0; + + while ((messageLength = recv(socket->sock, tempBuffer, DEFAULT_BUFLEN, 0)) > 0) { + tempBuffer[messageLength] = '\0'; + if(socket->onMessageReceived) + socket->onMessageReceived(std::string(tempBuffer, messageLength)); + + if(socket->onRawMessageReceived) + socket->onRawMessageReceived(tempBuffer, messageLength); + } + + socket->Close(); + if(socket->onSocketClosed) + socket->onSocketClosed(errno); + + if (socket->deleteAfterClosed && socket != nullptr) + delete socket; + } + + void setTimeout(int seconds) { + struct timeval tv; + tv.tv_sec = seconds; + tv.tv_usec = 0; + + setsockopt(this->sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(this->sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + } +}; + +#endif \ No newline at end of file diff --git a/plugins/gesture_plugin/plugin_gesture.cpp b/plugins/gesture_plugin/plugin_gesture.cpp new file mode 100644 index 00000000..85656802 --- /dev/null +++ b/plugins/gesture_plugin/plugin_gesture.cpp @@ -0,0 +1,52 @@ +#include + +#include "PanelGesture.h" + +#include "app/ospStudio.h" +#include "app/Plugin.h" + +namespace ospray { +namespace gesture_plugin { + +struct PluginGesture : public Plugin +{ + PluginGesture() : Plugin("Gesture") {} + + void mainMethod(std::shared_ptr ctx) override + { + if (ctx->mode == StudioMode::GUI) { + auto &studioCommon = ctx->studioCommon; + int ac = studioCommon.plugin_argc; + const char **av = studioCommon.plugin_argv; + + std::string optPanelName = "Gesture Panel"; + std::string configFilePath = "config/tracking_settings.json"; + + for (int i=0; i + +namespace ospray { +namespace gesture_plugin { + +TrackingManager::TrackingManager(std::string configFilePath) { + tcpSocket = nullptr; + updated = false; + + JSON config = nullptr; + try { + std::ifstream configFile(configFilePath); + if (configFile) + configFile >> config; + else + std::cerr << "The gesture config file does not exist." << std::endl; + } catch (nlohmann::json::exception &e) { + std::cerr << "Failed to parse the gesture config file: " << e.what() << std::endl; + } + + if (config == nullptr) + return; + + if (config != nullptr && config.contains("ipAddress")) + ipAddress = config["ipAddress"]; + if (config != nullptr && config.contains("portNumber")) + portNumber = config["portNumber"]; + if (config != nullptr && config.contains("scaleOffset")) + scaleOffset = config["scaleOffset"].get(); + // if (config != nullptr && config.contains("rotationOffset")) + // rotationOffset = config["rotationOffset"].get(); + if (config != nullptr && config.contains("translationOffset")) + translationOffset = config["translationOffset"].get(); + if (config != nullptr && config.contains("confidenceLevelThreshold")) + confidenceLevelThreshold = config["confidenceLevelThreshold"].get(); + if (config != nullptr && config.contains("leaningAngleThreshold")) + leaningAngleThreshold = config["leaningAngleThreshold"]; + if (config != nullptr && config.contains("leaningDirScaleFactor")) + leaningDirScaleFactor = config["leaningDirScaleFactor"].get(); +} + +TrackingManager::~TrackingManager() { + this->close(); +} + +void TrackingManager::saveConfig(std::string configFilePath) { + std::ofstream config(configFilePath); + + JSON j; + j["ipAddress"] = ipAddress; + j["portNumber"] = portNumber; + j["scaleOffset"] = scaleOffset; + // j["rotationOffset"] = rotationOffset; + j["translationOffset"] = translationOffset; + j["confidenceLevelThreshold"] = confidenceLevelThreshold; + j["leaningAngleThreshold"] = leaningAngleThreshold; + j["leaningDirScaleFactor"] = leaningDirScaleFactor; + + config << std::setw(4) << j << std::endl; + addStatus("Saved the configuration to " + configFilePath); +} + +void TrackingManager::start() { + if (tcpSocket != nullptr) { + std::cout << "Connection has already been set." << std::endl; + return; + } + + // Initialize socket. + tcpSocket = new TCPSocket([&](int errorCode, std::string errorMessage){ + addStatus("Socket creation error: " + std::to_string(errorCode) + " : " + errorMessage); + }); + + // Start receiving from the host. + tcpSocket->onMessageReceived = [&](std::string message) { + std::lock_guard guard(mtx); + + // std::cout << "Message from the Server: " << message << std::endl << std::flush; + updateState(message); + updated = true; + }; + + // On socket closed: + tcpSocket->onSocketClosed = [&](int errorCode){ + addStatus("Connection closed: " + std::to_string(errorCode)); + delete tcpSocket; + tcpSocket = nullptr; + updateState("{}"); + updated = true; + }; + + // Connect to the host. + tcpSocket->Connect(ipAddress, portNumber, [&] { + addStatus("Connected to the server successfully."); + }, + [&](int errorCode, std::string errorMessage){ // Connection failed + addStatus(std::to_string(errorCode) + " : " + errorMessage); + delete tcpSocket; + tcpSocket = nullptr; + }); +} + +void TrackingManager::close() { + if (tcpSocket == nullptr) + return; + + tcpSocket->Close(); +} + +bool TrackingManager::isRunning() { + return tcpSocket != nullptr && !tcpSocket->isClosed; +} + +bool TrackingManager::isUpdated() { + return updated; +} + +TrackingState TrackingManager::pollState() { + std::lock_guard guard(mtx); + + updated = false; + return state; +} + +void TrackingManager::updateState(std::string message) { + // set the default state + for (int i = 0; i < K4ABT_JOINT_COUNT; i++) { + state.positions[i] = vec3f(0.f); + state.confidences[i] = K4ABT_JOINT_CONFIDENCE_NONE; + } + state.mode = INTERACTION_NONE; + state.leaningAngle = 0.0f; + state.leaningDir = vec3f(0.0f); + + // parse the message (which is supposed to be in a JSON format) + nlohmann::ordered_json j; + try { + j = nlohmann::ordered_json::parse(message); + } catch (nlohmann::json::exception& e) { + std::cout << "Parse exception: " << e.what() << std::endl; + j = nullptr; + } + + // check if the tracking data is reliable. + if (j == nullptr || j.size() != K4ABT_JOINT_COUNT) { + return; + } + + // update positions and confidence levels + for (int i = 0; i < K4ABT_JOINT_COUNT; i++) { + if (j[i].contains("pos")) { + state.positions[i] = j[i]["pos"].get() * scaleOffset + translationOffset; + } + if (j[i].contains("conf")) { + state.confidences[i] = j[i]["conf"]; + } + } + + // check if the tracking data is reliable for further detections. + if (state.confidences[K4ABT_JOINT_SPINE_NAVEL] < confidenceLevelThreshold || + state.confidences[K4ABT_JOINT_WRIST_LEFT] < confidenceLevelThreshold || + state.confidences[K4ABT_JOINT_WRIST_RIGHT] < confidenceLevelThreshold || + state.confidences[K4ABT_JOINT_NECK] < confidenceLevelThreshold) { + return; + } + + // compute additional states (angle and direction) + vec3f spine = state.positions[K4ABT_JOINT_NECK] - state.positions[K4ABT_JOINT_SPINE_NAVEL]; + vec3f normal (0.0f, 1.0f, 0.0f); + state.leaningAngle = acos(dot(spine, normal) / (length(spine) * length(normal))) / M_PI * 180.f; + state.leaningDir = spine - dot(spine, normal) / length(normal) * normal; + state.leaningDir *= leaningDirScaleFactor; + + // compute additional states (mode) + bool leftHandUp = state.positions[K4ABT_JOINT_SPINE_NAVEL].y < state.positions[K4ABT_JOINT_WRIST_LEFT].y; + bool rightHandUp = state.positions[K4ABT_JOINT_SPINE_NAVEL].y < state.positions[K4ABT_JOINT_WRIST_RIGHT].y; + state.mode = (leftHandUp && rightHandUp && state.leaningAngle > leaningAngleThreshold) ? INTERACTION_FLYING : INTERACTION_IDLE; +} + +void TrackingManager::addStatus(std::string status) { + // write time before status + time_t now = time(0); + tm *ltm = localtime(&now); + status.insert(0, "(" + std::to_string(ltm->tm_hour) + ":" + std::to_string(ltm->tm_min) + ":" + std::to_string(ltm->tm_sec) + ") "); + + statuses.push_back(status); +} + +// show "important" tracking information in a readable format +std::string TrackingManager::getResultsInReadableForm() { + std::string result; + + result += "headPos.x: " + std::to_string(state.positions[K4ABT_JOINT_HEAD].x) + "\n"; + result += "headPos.y: " + std::to_string(state.positions[K4ABT_JOINT_HEAD].y) + "\n"; + result += "headPos.z: " + std::to_string(state.positions[K4ABT_JOINT_HEAD].z) + "\n"; + + if (state.mode == INTERACTION_NONE) result += "mode: NONE\n"; + else if (state.mode == INTERACTION_IDLE) result += "mode: IDLE\n"; + else if (state.mode == INTERACTION_FLYING) result += "mode: FLYING\n"; + + result += "leaningAngle: " + std::to_string(state.leaningAngle) + " °\n"; + + result += "leaningDir.x: " + std::to_string(state.leaningDir.x) + "\n"; + result += "leaningDir.y: " + std::to_string(state.leaningDir.y) + " (proj. to x-z plane)\n"; + result += "leaningDir.z: " + std::to_string(state.leaningDir.z); + + return result; +} + +} // namespace gesture_plugin +} // namespace ospray \ No newline at end of file diff --git a/plugins/gesture_plugin/tracker/TrackingManager.h b/plugins/gesture_plugin/tracker/TrackingManager.h new file mode 100644 index 00000000..02bb2eae --- /dev/null +++ b/plugins/gesture_plugin/tracker/TrackingManager.h @@ -0,0 +1,55 @@ +#pragma once + +#include "TrackingData.h" +#include "tcpsocket.hpp" + +#include +#include +#include + +namespace ospray { +namespace gesture_plugin { + +using namespace rkcommon::math; + +class TrackingManager +{ +public: + TrackingManager(std::string configFilePath); + ~TrackingManager(); + + void saveConfig(std::string configFilePath); + + void start(); + void close(); + bool isRunning(); + bool isUpdated(); + + TrackingState pollState(); + std::string getResultsInReadableForm(); + + std::string ipAddress { "localhost" }; + uint portNumber { 8888 }; + // Kinect - right-hand, y-down, z-forward, in milli-meters + // OSPRay - right-hand, y-up, z-forward, in meters + vec3f scaleOffset { -0.001f, -0.001f, +0.001f}; + // vec3f rotationOffset { 0.0f, 0.0f, 0.0f}; + vec3f translationOffset { 0.0f, 0.0f, 0.0f}; + int confidenceLevelThreshold { K4ABT_JOINT_CONFIDENCE_LOW }; + float leaningAngleThreshold { 8.0f }; // in degrees + vec3f leaningDirScaleFactor { 1.0f, 1.0f, 1.0f }; + + std::list statuses; +private: + void updateState(std::string message); + void addStatus(std::string status); + + TCPSocket *tcpSocket; + TrackingState state; + bool updated; + + std::mutex mtx; +}; + +} // namespace gesture_plugin +} // namespace ospray \ No newline at end of file diff --git a/plugins/gesture_plugin/tracker/tracking_settings.json b/plugins/gesture_plugin/tracker/tracking_settings.json new file mode 100644 index 00000000..dbb08094 --- /dev/null +++ b/plugins/gesture_plugin/tracker/tracking_settings.json @@ -0,0 +1,10 @@ +{ + "ipAddress": "129.114.10.237", + "portNumber": 8888, + + "scaleOffset": [0.001, -0.001, -0.001], + "translationOffset": [0.0, -0.1, 1.19], + "confidenceLevelThreshold": 1, + "leaningAngleThreshold": 1.0, + "leaningDirScaleFactor": [1.0, 1.0, 1.0] +} \ No newline at end of file diff --git a/plugins/storyboard_plugin/CMakeLists.txt b/plugins/storyboard_plugin/CMakeLists.txt new file mode 100644 index 00000000..deae80b4 --- /dev/null +++ b/plugins/storyboard_plugin/CMakeLists.txt @@ -0,0 +1,32 @@ +option(BUILD_PLUGIN_STORYBOARD "Storyboard plugin" OFF) + +if (BUILD_PLUGIN_STORYBOARD) + set(pluginName "ospray_studio_plugin_storyboard") + + add_library(${pluginName} SHARED + plugin_storyboard.cpp + PanelStoryboard.cpp + request/RequestManager.cpp + request/ScreenShotContainer.cpp + ) + + target_link_libraries(${pluginName} ospray_sg ospray_ui) + target_link_libraries(${pluginName} imgui stb_image) + + # Add external libraries for this plugin + # add_subdirectory(external) + target_link_libraries(${pluginName} network) + + target_include_directories(${pluginName} + PRIVATE ${CMAKE_SOURCE_DIR} + ) + + install(TARGETS ${pluginName} + DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT lib + # on Windows put the dlls into bin + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT lib + ) + +endif() diff --git a/plugins/storyboard_plugin/PanelStoryboard.cpp b/plugins/storyboard_plugin/PanelStoryboard.cpp new file mode 100644 index 00000000..0bf517f6 --- /dev/null +++ b/plugins/storyboard_plugin/PanelStoryboard.cpp @@ -0,0 +1,217 @@ +#include + +#include "PanelStoryboard.h" + +#include "app/widgets/GenerateImGuiWidgets.h" + +#include "rkcommon/math/vec.h" + +#include "sg/JSONDefs.h" +#include "sg/Math.h" +#include "sg/JSONDefs.h" +#include "sg/fb/FrameBuffer.h" + +#include "imgui.h" +#include "stb_image_write.h" + +namespace ospray { +namespace storyboard_plugin { + +static void WriteToMemory_stbi(void *context, void *data, int size) { + std::vector *buffer = + reinterpret_cast *>(context); + + unsigned char *pData = reinterpret_cast(data); + + buffer->insert(buffer->end(), pData, pData + size); +} + +PanelStoryboard::PanelStoryboard(std::shared_ptr _context, std::string _panelName, std::string _configFilePath) + : Panel(_panelName.c_str(), _context) + , panelName(_panelName) + , configFilePath(_configFilePath) +{ + requestManager.reset(new RequestManager(configFilePath)); + + requestManager->start(); + + context->frame->child("scale") = 0.5f; + context->frame->child("scaleNav") = 0.25f; +} + +void PanelStoryboard::buildUI(void *ImGuiCtx) +{ + // Need to set ImGuiContext in *this* address space + ImGui::SetCurrentContext((ImGuiContext *)ImGuiCtx); + ImGui::OpenPopup(panelName.c_str()); + + if (!ImGui::BeginPopupModal(panelName.c_str(), nullptr, ImGuiWindowFlags_None)) return; + + if (requestManager->isRunning()) { + ImGui::Text("%s", "Currently connected to the storyboard server..."); + + if (ImGui::Button("Disconnect")) { + requestManager->close(); + } + } + else { + ImGui::Text("%s", "Currently NOT connected to the storyboard server..."); + std::string str = "- Connect to " + requestManager->ipAddress + ":" + std::to_string(requestManager->portNumber); + ImGui::Text("%s", str.c_str()); + + if (ImGui::Button("Connect")) { + requestManager->start(); + } + } + ImGui::Separator(); + + // Close button + if (ImGui::Button("Close")) { + setShown(false); + ImGui::CloseCurrentPopup(); + } + ImGui::Separator(); + + // if (ImGui::CollapsingHeader("Configuration", ImGuiTreeNodeFlags_DefaultOpen)) { + // ImGui::Text("%s", "Offset(s)"); + // ImGui::DragFloat3("Scale", storyboardManager->scaleOffset, 0.001, -100, 100, "%.3f"); + // ImGui::DragFloat3("Translate", storyboardManager->translationOffset, 0, -100, 100, "%.1f"); + + // ImGui::Separator(); + // if (ImGui::Button("Save")) { + // storyboardManager->saveConfig(this->configFilePath); + // } + // } + ImGui::Separator(); + + // Display statuses in a scrolling region + if (ImGui::CollapsingHeader("Status", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::BeginChild("Scrolling", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysAutoResize); + for (std::string status : requestManager->statuses) { + ImGui::Text("%s", status.c_str()); + } + ImGui::EndChild(); + } + ImGui::Separator(); + + ImGui::EndPopup(); +} + +void PanelStoryboard::process(std::string key) { + if (key == "update") { + std::queue requests; + requestManager->pollRequests(requests); + + std::cout << "Requests... " << requests.size() << std::flush << std::endl; + + while (!requests.empty()) { + nlohmann::ordered_json req = requests.front(); + + if (req.contains("type") && req["type"] == "request" && + req.contains("action") && req["action"] == "capture.view") { + + nlohmann::ordered_json j = + { + {"type", "response"}, + {"action", "capture.view"}, + {"snapshotIdx", req["snapshotIdx"].get()}, + {"camera", context->arcballCamera->getState()} + }; + requestManager->send(j.dump()); + } + else if (req.contains("type") && req["type"] == "request" && + req.contains("action") && req["action"] == "capture.image") { + + // 1) capture the image + auto &fb = context->frame->childAs("framebuffer"); + auto img = fb.map(OSP_FB_COLOR); + auto size = fb.child("size").valueAs(); // 2048 x 1280 + auto fmt = fb.child("colorFormat").valueAs(); // sRGB + + std::vector values; + stbi_flip_vertically_on_write(1); + stbi_write_png_to_func(WriteToMemory_stbi, &values, size.x, size.y, 4, img, 4 * size.x); + requestManager->screenshotContainer->clear(); + requestManager->screenshotContainer->setImage(size.x, size.y, values); + + // 2) send the setup information + nlohmann::ordered_json jSetUp = + { + {"type", "response"}, + {"action", "capture.image.setup"}, + {"snapshotIdx", req["snapshotIdx"].get()}, + {"width", size.x}, + {"height", size.y}, + {"length", values.size()} + }; + requestManager->send(jSetUp.dump()); + } + else if (req.contains("type") && req["type"] == "ack" && + req.contains("action") && (req["action"] == "capture.image.setup" || req["action"] == "capture.image.data")) { + + if (!requestManager->screenshotContainer->isNextChunkAvailable()) { + // clear the memory + requestManager->screenshotContainer->clear(); + // send the message to indicate the sending the image part is done + nlohmann::ordered_json jChunk = + { + {"type", "response"}, + {"action", "capture.image.done"}, + {"snapshotIdx", req["snapshotIdx"].get()} + }; + requestManager->send(jChunk.dump()); + } + else { // send the first message + int len = 512; + int start = requestManager->screenshotContainer->getStartIndex(); + std::vector chunk = requestManager->screenshotContainer->getNextChunk(len); + + auto binary = nlohmann::ordered_json::binary_t(chunk); + nlohmann::ordered_json jChunk = + { + {"type", "response"}, + {"action", "capture.image.data"}, + {"snapshotIdx", req["snapshotIdx"].get()}, + {"start", start }, + {"data", binary } + }; + requestManager->send(jChunk.dump()); + } + } + else if (req.contains("type") && req["type"] == "request" && + req.contains("action") && req["action"] == "update.view") { + // int snapshotIdx = req["snapshotIdx"].get(); + + if (req.contains("camera")) { + CameraState state; + from_json(req["camera"], state); + context->arcballCamera->setState(state); + } + } + else if (req.contains("type") && req["type"] == "request" && + req.contains("action") && req["action"] == "update.transition") { + // int snapshotIdx = req["snapshotIdx"].get(); + + if (req.contains("amount") && req.contains("from") && req.contains("to")) { + CameraState from; + from_json(req["from"], from); + + CameraState to; + from_json(req["to"], to); + + CameraState curr = from.slerp(to, req["amount"]); + context->arcballCamera->setState(curr); + } + } + + requests.pop(); + } + } + else if (key == "start") { + requestManager->start(); + } + +} + +} // namespace storyboard_plugin +} // namespace ospray diff --git a/plugins/storyboard_plugin/PanelStoryboard.h b/plugins/storyboard_plugin/PanelStoryboard.h new file mode 100644 index 00000000..e3e78905 --- /dev/null +++ b/plugins/storyboard_plugin/PanelStoryboard.h @@ -0,0 +1,27 @@ +#pragma once + +#include "app/widgets/Panel.h" +#include "app/ospStudio.h" + +#include "request/RequestManager.h" + +namespace ospray { +namespace storyboard_plugin { + +struct PanelStoryboard : public Panel +{ + PanelStoryboard(std::shared_ptr _context, std::string _panelName, std::string _configFilePath); + + void buildUI(void *ImGuiCtx) override; + + void process(std::string key) override; + +private: + std::string panelName; + std::string configFilePath; + + std::unique_ptr requestManager; +}; + +} // namespace storyboard_plugin +} // namespace ospray diff --git a/plugins/storyboard_plugin/external/CMakeLists.txt b/plugins/storyboard_plugin/external/CMakeLists.txt new file mode 100644 index 00000000..fc999e8a --- /dev/null +++ b/plugins/storyboard_plugin/external/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(async-sockets) \ No newline at end of file diff --git a/plugins/storyboard_plugin/external/async-sockets/CMakeLists.txt b/plugins/storyboard_plugin/external/async-sockets/CMakeLists.txt new file mode 100644 index 00000000..8d9267dd --- /dev/null +++ b/plugins/storyboard_plugin/external/async-sockets/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(network INTERFACE) +target_include_directories(network + INTERFACE $) \ No newline at end of file diff --git a/plugins/storyboard_plugin/external/async-sockets/basesocket.hpp b/plugins/storyboard_plugin/external/async-sockets/basesocket.hpp new file mode 100644 index 00000000..47312430 --- /dev/null +++ b/plugins/storyboard_plugin/external/async-sockets/basesocket.hpp @@ -0,0 +1,146 @@ +// The Linux and Mac implementations are from https://github.com/eminfedar/async-sockets-cpp +// Updated it to suport Windows. +#pragma once + +#if defined(__linux__) || defined(__APPLE__) + +#include +#include +#include +#include +#include + +#include +#include +#include + +#define FDR_UNUSED(expr){ (void)(expr); } +#define FDR_ON_ERROR std::function onError = [](int errorCode, std::string errorMessage){FDR_UNUSED(errorCode); FDR_UNUSED(errorMessage)} + +class BaseSocket +{ +// Definitions +public: + enum SocketType + { + TCP = SOCK_STREAM, + UDP = SOCK_DGRAM + }; + const uint16_t BUFFER_SIZE = 0xFFFF; + sockaddr_in address; + bool isClosed = false; + +protected: + int sock = 0; + static std::string ipToString(sockaddr_in addr) + { + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(addr.sin_addr), ip, INET_ADDRSTRLEN); + + return std::string(ip); + } + + BaseSocket(FDR_ON_ERROR, SocketType sockType = TCP, int socketId = -1) + { + if (socketId < 0) + { + if ((this->sock = socket(AF_INET, sockType, 0)) < 0) + { + onError(errno, "Socket creating error."); + } + } + else + { + this->sock = socketId; + } + } + +// Methods +public: + virtual void Close() { + if(isClosed) return; + + isClosed = true; + close(this->sock); + } + + std::string remoteAddress() {return ipToString(this->address);} + int remotePort() {return ntohs(this->address.sin_port);} + int fileDescriptor() const { return this->sock; } +}; + +#elif _WIN32 + +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +#include +#include +#include + +// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib +#pragma comment (lib, "Ws2_32.lib") +#pragma comment (lib, "Mswsock.lib") +#pragma comment (lib, "AdvApi32.lib") + +#define DEFAULT_BUFLEN 512 +#define FDR_UNUSED(expr){ (void)(expr); } +#define FDR_ON_ERROR std::function onError = [](int errorCode, std::string errorMessage){FDR_UNUSED(errorCode); FDR_UNUSED(errorMessage)} + +class BaseSocket +{ +// Definitions +public: + sockaddr_in address; + bool isClosed = false; + +protected: + SOCKET sock = INVALID_SOCKET; + + static std::string ipToString(sockaddr_in addr) { + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(addr.sin_addr), ip, INET_ADDRSTRLEN); + + return std::string(ip); + } + + BaseSocket(FDR_ON_ERROR, int socketId = -1) { + // Initialize Winsock + WSADATA wsaData; + int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (iResult != 0) { + onError(errno, "WSAStartup failed with error: " + std::to_string(iResult)); + return; + } + + if (socketId < 0) { + // Create a SOCKET to connect to server or to listen for client connections + this->sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (this->sock == INVALID_SOCKET) { + onError(errno, "socket failed with error: " + std::to_string(WSAGetLastError())); + WSACleanup(); + return; + } + } else { + this->sock = (SOCKET) socketId; + } + } + +// Methods +public: + virtual void Close() { + if (isClosed) return; + + isClosed = true; + closesocket(this->sock); + WSACleanup(); + } + + std::string remoteAddress() { return ipToString(this->address); } + int remotePort() { return ntohs(this->address.sin_port); } +}; + +#endif \ No newline at end of file diff --git a/plugins/storyboard_plugin/external/async-sockets/tcpserver.hpp b/plugins/storyboard_plugin/external/async-sockets/tcpserver.hpp new file mode 100644 index 00000000..8e9096ec --- /dev/null +++ b/plugins/storyboard_plugin/external/async-sockets/tcpserver.hpp @@ -0,0 +1,175 @@ +// The Linux and Mac implementations are from https://github.com/eminfedar/async-sockets-cpp +// Updated it to suport Windows. +#pragma once + +#if defined(__linux__) || defined(__APPLE__) + +#include "tcpsocket.hpp" +#include + +class TCPServer : public BaseSocket +{ +public: + // Event Listeners: + std::function onNewConnection = [](TCPSocket* sock){FDR_UNUSED(sock)}; + + explicit TCPServer(FDR_ON_ERROR): BaseSocket(onError, SocketType::TCP) + { + int opt = 1; + setsockopt(this->sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int)); + setsockopt(this->sock,SOL_SOCKET,SO_REUSEPORT,&opt,sizeof(int)); + } + + // Binding the server. + void Bind(const char *address, uint16_t port, FDR_ON_ERROR) + { + if (inet_pton(AF_INET, address, &this->address.sin_addr) <= 0) + { + onError(errno, "Invalid address. Address type not supported."); + return; + } + + this->address.sin_family = AF_INET; + this->address.sin_port = htons(port); + + if (bind(this->sock, (const sockaddr *)&this->address, sizeof(this->address)) < 0) + { + onError(errno, "Cannot bind the socket."); + return; + } + } + void Bind(int port, FDR_ON_ERROR) { this->Bind("0.0.0.0", port, onError); } + + // Start listening the server. + void Listen(FDR_ON_ERROR) + { + if (listen(this->sock, 20) < 0) + { + onError(errno, "Error: Server can't listen the socket."); + return; + } + + std::thread t(Accept, this, onError); + t.detach(); + } + + // Overriding Close to add shutdown(): + void Close() + { + shutdown(this->sock, SHUT_RDWR); + + BaseSocket::Close(); + } + +private: + static void Accept(TCPServer *server, FDR_ON_ERROR) + { + sockaddr_in newSocketInfo; + socklen_t newSocketInfoLength = sizeof(newSocketInfo); + + int newSock; + while (!server->isClosed) + { + while ((newSock = accept(server->sock, (sockaddr *)&newSocketInfo, &newSocketInfoLength)) < 0) + { + if (errno == EBADF || errno == EINVAL) return; + + onError(errno, "Error while accepting a new connection."); + return; + } + + if (!server->isClosed && newSock >= 0) + { + TCPSocket *newSocket = new TCPSocket(onError, newSock); + newSocket->deleteAfterClosed = true; + newSocket->setAddressStruct(newSocketInfo); + + server->onNewConnection(newSocket); + newSocket->Listen(); + } + } + } +}; + +#elif _WIN32 + +#include "tcpsocket.hpp" + +#include +#include + +class TCPServer : public BaseSocket +{ +public: + // Event Listeners: + std::function onNewConnection = [](TCPSocket* sock){FDR_UNUSED(sock)}; + + explicit TCPServer(FDR_ON_ERROR): BaseSocket(onError) { + BOOL bOptVal = TRUE; + setsockopt(this->sock, SOL_SOCKET, SO_REUSEADDR, (char *) &bOptVal, sizeof(BOOL)); + } + + // Binding the server. + void Bind(const char *address, uint16_t port, FDR_ON_ERROR) { + if (inet_pton(AF_INET, address, &this->address.sin_addr) <= 0) { + onError(errno, "Invalid address. Address type not supported."); + return; + } + + this->address.sin_family = AF_INET; + this->address.sin_port = htons(port); + + if (bind(this->sock, (const sockaddr *)&this->address, sizeof(this->address)) == SOCKET_ERROR) { + onError(errno, "Cannot bind the socket."); + //// + return; + } + } + void Bind(int port, FDR_ON_ERROR) { this->Bind("0.0.0.0", port, onError); } + + // Start listening the server. + void Listen(FDR_ON_ERROR) { + if (listen(this->sock, 20) == SOCKET_ERROR) { + onError(errno, "Error: Server can't listen the socket."); + return; + } + + std::thread t(Accept, this, onError); + t.detach(); + } + + // Overriding Close to add shutdown(): + void Close() { + shutdown(this->sock, SD_BOTH); + + BaseSocket::Close(); + } + +private: + static void Accept(TCPServer *server, FDR_ON_ERROR) { + sockaddr_in newSocketInfo; + socklen_t newSocketInfoLength = sizeof(newSocketInfo); + + SOCKET newSock = 0; + while (!server->isClosed) { + newSock = accept(server->sock, (sockaddr *)&newSocketInfo, &newSocketInfoLength); + if (newSock == INVALID_SOCKET) { + if (errno == EBADF || errno == EINVAL) return; + + onError(errno, "Error while accepting a new connection." + std::to_string(WSAGetLastError())); + return; + } + + if (!server->isClosed && newSock >= 0) { + TCPSocket *newSocket = new TCPSocket(onError, newSock); + newSocket->deleteAfterClosed = true; + newSocket->setAddressStruct(newSocketInfo); + + server->onNewConnection(newSocket); + newSocket->Listen(); + } + } + } +}; + +#endif \ No newline at end of file diff --git a/plugins/storyboard_plugin/external/async-sockets/tcpsocket.hpp b/plugins/storyboard_plugin/external/async-sockets/tcpsocket.hpp new file mode 100644 index 00000000..ea12f9e8 --- /dev/null +++ b/plugins/storyboard_plugin/external/async-sockets/tcpsocket.hpp @@ -0,0 +1,258 @@ +// The Linux and Mac implementations are from https://github.com/eminfedar/async-sockets-cpp +// Updated it to suport Windows. +#pragma once + +#if defined(__linux__) || defined(__APPLE__) + +#include "basesocket.hpp" +#include +#include +#include +// copied from https://github.com/eminfedar/async-sockets-cpp + +#include + +class TCPSocket : public BaseSocket +{ +public: + // Event Listeners: + std::function onMessageReceived; + std::function onRawMessageReceived; + std::function onSocketClosed; + + explicit TCPSocket(FDR_ON_ERROR, int socketId = -1) : BaseSocket(onError, TCP, socketId){} + + // Send TCP Packages + int Send(const char *bytes, size_t byteslength) + { + if (this->isClosed) + return -1; + + int sent = 0; + if ((sent = send(this->sock, bytes, byteslength, 0)) < 0) + { + perror("send"); + } + return sent; + } + int Send(std::string message) { return this->Send(message.c_str(), message.length()); } + + void Connect(std::string host, uint16_t port, std::function onConnected = [](){}, FDR_ON_ERROR) + { + struct addrinfo hints, *res, *it; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + // Get address info from DNS + int status; + if ((status = getaddrinfo(host.c_str(), NULL, &hints, &res)) != 0) { + onError(errno, "Invalid address." + std::string(gai_strerror(status))); + return; + } + + for(it = res; it != NULL; it = it->ai_next) + { + if (it->ai_family == AF_INET) { // IPv4 + memcpy((void*)(&this->address), (void*)it->ai_addr, sizeof(sockaddr_in)); + break; // for now, just get first ip (ipv4). + } + } + + freeaddrinfo(res); + + this->Connect((uint32_t)this->address.sin_addr.s_addr, port, onConnected, onError); + } + void Connect(uint32_t ipv4, uint16_t port, std::function onConnected = [](){}, FDR_ON_ERROR) + { + this->address.sin_family = AF_INET; + this->address.sin_port = htons(port); + this->address.sin_addr.s_addr = ipv4; + + this->setTimeout(5); + + // Try to connect. + if (connect(this->sock, (const sockaddr *)&this->address, sizeof(sockaddr_in)) < 0) + { + onError(errno, "Connection failed to the host."); + this->setTimeout(0); + return; + } + + this->setTimeout(0); + + // Connected to the server, fire the event. + onConnected(); + + // Start listening from server: + this->Listen(); + } + + void Listen() + { + std::thread t(TCPSocket::Receive, this); + t.detach(); + } + + void setAddressStruct(sockaddr_in addr) {this->address = addr;} + sockaddr_in getAddressStruct() const {return this->address;} + + bool deleteAfterClosed = false; + +private: + static void Receive(TCPSocket *socket) + { + char tempBuffer[socket->BUFFER_SIZE]; + int messageLength; + + while ((messageLength = recv(socket->sock, tempBuffer, socket->BUFFER_SIZE, 0)) > 0) + { + tempBuffer[messageLength] = '\0'; + if(socket->onMessageReceived) + socket->onMessageReceived(std::string(tempBuffer, messageLength)); + + if(socket->onRawMessageReceived) + socket->onRawMessageReceived(tempBuffer, messageLength); + } + + socket->Close(); + if(socket->onSocketClosed) + socket->onSocketClosed(errno); + + if (socket->deleteAfterClosed && socket != nullptr) + delete socket; + } + + void setTimeout(int seconds) + { + struct timeval tv; + tv.tv_sec = seconds; + tv.tv_usec = 0; + + setsockopt(this->sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(this->sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + } +}; + +#elif _WIN32 + +#include "basesocket.hpp" + +#include +#include +#include + +class TCPSocket : public BaseSocket +{ +public: + // Event Listeners: + std::function onMessageReceived; + std::function onRawMessageReceived; + std::function onSocketClosed; + + explicit TCPSocket(FDR_ON_ERROR, int socketId = -1) : BaseSocket(onError, socketId){} + + // Send TCP Packages + int Send(const char *bytes, size_t byteslength) { + if (this->isClosed) + return -1; + + int sent = send(this->sock, bytes, byteslength, 0); + if (sent == SOCKET_ERROR) { + perror("send failed..."); + } + return sent; + } + int Send(std::string message) { return this->Send(message.c_str(), message.length()); } + + void Connect(std::string host, uint16_t port, std::function onConnected = [](){}, FDR_ON_ERROR) { + struct addrinfo hints, *res, *it; + ZeroMemory( &hints, sizeof(hints) ); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + // Get address info from DNS + int status; + if ((status = getaddrinfo(host.c_str(), NULL, &hints, &res)) != 0) { + onError(errno, "Invalid address." + std::string(gai_strerror(status))); + return; + } + + for(it = res; it != NULL; it = it->ai_next) { + if (it->ai_family == AF_INET) { // IPv4 + memcpy((void*)(&this->address), (void*)it->ai_addr, sizeof(sockaddr_in)); + break; // for now, just get first ip (ipv4). + } + } + + freeaddrinfo(res); + + this->Connect((uint32_t)this->address.sin_addr.s_addr, port, onConnected, onError); + } + void Connect(uint32_t ipv4, uint16_t port, std::function onConnected = [](){}, FDR_ON_ERROR) { + this->address.sin_family = AF_INET; + this->address.sin_port = htons(port); + this->address.sin_addr.s_addr = ipv4; + + this->setTimeout(5); + + // Try to connect. + if (connect(this->sock, (const sockaddr *)&this->address, sizeof(sockaddr_in)) == SOCKET_ERROR) { + onError(errno, "Connection failed to the host."); + this->setTimeout(0); + return; + } + + this->setTimeout(0); + + // Connected to the server, fire the event. + onConnected(); + + // Start listening from server: + this->Listen(); + } + + void Listen() { + std::thread t(TCPSocket::Receive, this); + t.detach(); + } + + void setAddressStruct(sockaddr_in addr) { this->address = addr; } + sockaddr_in getAddressStruct() const { return this->address; } + + bool deleteAfterClosed = false; + +private: + static void Receive(TCPSocket *socket) { + char tempBuffer[DEFAULT_BUFLEN]; + int messageLength = 0; + + while ((messageLength = recv(socket->sock, tempBuffer, DEFAULT_BUFLEN, 0)) > 0) { + tempBuffer[messageLength] = '\0'; + if(socket->onMessageReceived) + socket->onMessageReceived(std::string(tempBuffer, messageLength)); + + if(socket->onRawMessageReceived) + socket->onRawMessageReceived(tempBuffer, messageLength); + } + + socket->Close(); + if(socket->onSocketClosed) + socket->onSocketClosed(errno); + + if (socket->deleteAfterClosed && socket != nullptr) + delete socket; + } + + void setTimeout(int seconds) { + struct timeval tv; + tv.tv_sec = seconds; + tv.tv_usec = 0; + + setsockopt(this->sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(this->sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + } +}; + +#endif \ No newline at end of file diff --git a/plugins/storyboard_plugin/plugin_storyboard.cpp b/plugins/storyboard_plugin/plugin_storyboard.cpp new file mode 100644 index 00000000..29454d81 --- /dev/null +++ b/plugins/storyboard_plugin/plugin_storyboard.cpp @@ -0,0 +1,52 @@ +#include + +#include "PanelStoryboard.h" + +#include "app/ospStudio.h" +#include "app/Plugin.h" + +namespace ospray { +namespace storyboard_plugin { + +struct PluginStoryboard : public Plugin +{ + PluginStoryboard() : Plugin("Storyboard") {} + + void mainMethod(std::shared_ptr ctx) override + { + if (ctx->mode == StudioMode::GUI) { + auto &studioCommon = ctx->studioCommon; + int ac = studioCommon.plugin_argc; + const char **av = studioCommon.plugin_argv; + + std::string optPanelName = "Storyboard Panel"; + std::string configFilePath = "config/storyboard_settings.json"; + + for (int i=0; i + +namespace ospray { +namespace storyboard_plugin { + +RequestManager::RequestManager(std::string configFilePath) { + tcpSocket = nullptr; + screenshotContainer.reset(new ScreenShotContainer()); + + JSON config = nullptr; + try { + std::ifstream configFile(configFilePath); + if (configFile) + configFile >> config; + else + std::cerr << "The storyboard config file does not exist." << std::endl; + } catch (nlohmann::json::exception &e) { + std::cerr << "Failed to parse the storyboard config file: " << e.what() << std::endl; + } + + if (config == nullptr) + return; + + if (config != nullptr && config.contains("ipAddress")) + ipAddress = config["ipAddress"]; + if (config != nullptr && config.contains("portNumber")) + portNumber = config["portNumber"]; +} + +RequestManager::~RequestManager() { + this->close(); +} + +void RequestManager::start() { + if (tcpSocket != nullptr) { + std::cout << "Connection has already been set." << std::endl; + return; + } + + // Initialize socket. + tcpSocket = new TCPSocket([&](int errorCode, std::string errorMessage){ + addStatus("Socket creation error: " + std::to_string(errorCode) + " : " + errorMessage); + }); + + // Start receiving from the host. + tcpSocket->onMessageReceived = [&](std::string message) { + std::lock_guard guard(mtx); + + // parse the message (which is supposed to be in a JSON format) + nlohmann::ordered_json j; + try { + j = nlohmann::ordered_json::parse(message); + } catch (nlohmann::json::exception& e) { + std::cout << "Parse exception: " << e.what() << std::endl; + j = nullptr; + } + + // add it to the pending actions + if (j != nullptr) { + pendingRequests.push(j); + } + }; + + // On socket closed: + tcpSocket->onSocketClosed = [&](int errorCode){ + std::lock_guard guard(mtx); + while (!pendingRequests.empty()) pendingRequests.pop(); + + addStatus("Connection closed: " + std::to_string(errorCode)); + delete tcpSocket; + tcpSocket = nullptr; + }; + + // Connect to the host. + tcpSocket->Connect(ipAddress, portNumber, [&] { + addStatus("Connected to the server successfully."); + }, + [&](int errorCode, std::string errorMessage){ // Connection failed + addStatus(std::to_string(errorCode) + " : " + errorMessage); + delete tcpSocket; + tcpSocket = nullptr; + }); +} + +void RequestManager::close() { + if (tcpSocket == nullptr) return; + + tcpSocket->Close(); +} + +bool RequestManager::isRunning() { + return tcpSocket != nullptr && !tcpSocket->isClosed; +} + +bool RequestManager::isUpdated() { + std::lock_guard guard(mtx); + + return pendingRequests.size() > 0; +} + +void RequestManager::pollRequests(std::queue& requests) { + std::lock_guard guard(mtx); + + while (!pendingRequests.empty()) { + requests.push(pendingRequests.front()); + pendingRequests.pop(); + } +} + +void RequestManager::send(std::string message) { + if (tcpSocket == nullptr) return; + + tcpSocket->Send(message); +} + +void RequestManager::addStatus(std::string status) { + // write time before status + time_t now = time(0); + tm *ltm = localtime(&now); + status.insert(0, "(" + std::to_string(ltm->tm_hour) + ":" + std::to_string(ltm->tm_min) + ":" + std::to_string(ltm->tm_sec) + ") "); + + statuses.push_back(status); +} + +// show "important" storyboard information in a readable format +std::string RequestManager::getResultsInReadableForm() { + std::lock_guard guard(mtx); + + return std::to_string(pendingRequests.size()) + " pending action(s)"; +} + +} // namespace storyboard_plugin +} // namespace ospray \ No newline at end of file diff --git a/plugins/storyboard_plugin/request/RequestManager.h b/plugins/storyboard_plugin/request/RequestManager.h new file mode 100644 index 00000000..4a0ea3b0 --- /dev/null +++ b/plugins/storyboard_plugin/request/RequestManager.h @@ -0,0 +1,49 @@ +#pragma once + +#include "tcpsocket.hpp" +#include "ScreenShotContainer.h" +#include "sg/JSONDefs.h" + +#include +#include +#include + +namespace ospray { +namespace storyboard_plugin { + +using namespace rkcommon::math; + +class RequestManager +{ +public: + RequestManager(std::string configFilePath); + ~RequestManager(); + + void start(); + void close(); + bool isRunning(); + bool isUpdated(); + + void pollRequests(std::queue& requests); + + void send(std::string message); + + std::string getResultsInReadableForm(); + + std::string ipAddress { "localhost" }; + uint portNumber { 8889 }; + + std::unique_ptr screenshotContainer; + + std::list statuses; +private: + void addStatus(std::string status); + + TCPSocket *tcpSocket; + + std::queue pendingRequests; + std::mutex mtx; +}; + +} // namespace storyboard_plugin +} // namespace ospray \ No newline at end of file diff --git a/plugins/storyboard_plugin/request/ScreenShotContainer.cpp b/plugins/storyboard_plugin/request/ScreenShotContainer.cpp new file mode 100644 index 00000000..755e5242 --- /dev/null +++ b/plugins/storyboard_plugin/request/ScreenShotContainer.cpp @@ -0,0 +1,51 @@ +#include "ScreenShotContainer.h" + +#include + +namespace ospray { +namespace storyboard_plugin { + +ScreenShotContainer::ScreenShotContainer() { } + +ScreenShotContainer::~ScreenShotContainer() { } + +void ScreenShotContainer::setImage(int w, int h, std::vector values) { + imgValues.insert(imgValues.end(), values.begin(), values.end()); + width = w; + height = h; + currStartIndex = 0; +} + +void ScreenShotContainer::clear() { + imgValues.clear(); + width = 0; + height = 0; + currStartIndex = 0; +} + +bool ScreenShotContainer::isNextChunkAvailable() { + return currStartIndex < imgValues.size(); +} + +std::vector ScreenShotContainer::getNextChunk(int len) { + int chunkLen = (currStartIndex + len) > imgValues.size() ? (imgValues.size() - currStartIndex) : len; + + // Starting and Ending iterators + auto start = imgValues.begin() + currStartIndex; + auto end = imgValues.begin() + currStartIndex + chunkLen; + + // To store the sliced vector + std::vector result(chunkLen); + copy(start, end, result.begin()); + currStartIndex += chunkLen; + + // Return the final sliced vector + return result; +} + +int ScreenShotContainer::getStartIndex() { + return currStartIndex; +} + +} // namespace storyboard_plugin +} // namespace ospray \ No newline at end of file diff --git a/plugins/storyboard_plugin/request/ScreenShotContainer.h b/plugins/storyboard_plugin/request/ScreenShotContainer.h new file mode 100644 index 00000000..f15c31b5 --- /dev/null +++ b/plugins/storyboard_plugin/request/ScreenShotContainer.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace ospray { +namespace storyboard_plugin { + +class ScreenShotContainer +{ +public: + ScreenShotContainer(); + ~ScreenShotContainer(); + + void setImage(int w, int h, std::vector values); + void clear(); + + bool isNextChunkAvailable(); + std::vector getNextChunk(int len); + int getStartIndex(); + +private: + std::vector imgValues; + int width; + int height; + int currStartIndex; +}; + +} // namespace storyboard_plugin +} // namespace ospray \ No newline at end of file diff --git a/plugins/storyboard_plugin/request/storyboard_settings.json b/plugins/storyboard_plugin/request/storyboard_settings.json new file mode 100644 index 00000000..95dbaa00 --- /dev/null +++ b/plugins/storyboard_plugin/request/storyboard_settings.json @@ -0,0 +1,4 @@ +{ + "ipAddress": "127.0.0.1", + "portNumber": 8888 +} \ No newline at end of file diff --git a/sg/importer/Importer.cpp b/sg/importer/Importer.cpp index a3ef0fbd..1b3c7389 100644 --- a/sg/importer/Importer.cpp +++ b/sg/importer/Importer.cpp @@ -52,21 +52,9 @@ inline bool FindCameraNode::operator()(Node &node, TraversalContext &) return traverseChildren; } -OSPSG_INTERFACE void importScene( - std::shared_ptr context, rkcommon::FileName &sceneFileName) +void importScene( + std::shared_ptr context, JSON j, std::string filesToImportDir = "") { - std::cout << "Importing a scene" << std::endl; - context->filesToImport.clear(); - std::ifstream sgFile(sceneFileName.str()); - if (!sgFile) { - std::cerr << "Could not open " << sceneFileName << " for reading" - << std::endl; - return; - } - - JSON j; - sgFile >> j; - std::map jImporters; std::map jGenerators; sg::NodePtr lights; @@ -106,7 +94,7 @@ OSPSG_INTERFACE void importScene( // Try a couple different paths to find the file before giving up std::vector possibleFileNames = {fileName, // as imported - sceneFileName.path() + fileName.base(), // in scenefile directory + filesToImportDir + fileName.base(), // in scenefile directory fileName.base(), // in local directory ""}; @@ -314,6 +302,33 @@ OSPSG_INTERFACE void importScene( } } +OSPSG_INTERFACE void importScene( + std::shared_ptr context, rkcommon::FileName &sceneFileName) +{ + std::cout << "Importing a scene" << std::endl; + context->filesToImport.clear(); + std::ifstream sgFile(sceneFileName.str()); + if (!sgFile) { + std::cerr << "Could not open " << sceneFileName << " for reading" + << std::endl; + return; + } + + JSON j; + sgFile >> j; + + importScene(context, j, sceneFileName.path()); +} + +OSPSG_INTERFACE void importScene( + std::shared_ptr context, std::string json, std::string filesToImportDir) +{ + JSON j; + j = JSON::parse(json); + + importScene(context, j, filesToImportDir); +} + // global assets catalogue AssetsCatalogue cat; diff --git a/sg/importer/Importer.h b/sg/importer/Importer.h index e790b12c..b66c768a 100644 --- a/sg/importer/Importer.h +++ b/sg/importer/Importer.h @@ -218,5 +218,8 @@ inline void clearAssets() OSPSG_INTERFACE void importScene( std::shared_ptr context, rkcommon::FileName &fileName); +// for loading scene +OSPSG_INTERFACE void importScene( + std::shared_ptr context, std::string sceneDesc, std::string filesToImportDir = ""); } // namespace sg } // namespace ospray