From a05472108e19f01e6accf025ba489ced256621d1 Mon Sep 17 00:00:00 2001 From: Sergej Geringer Date: Fri, 18 Feb 2022 12:10:29 +0100 Subject: [PATCH 1/6] OpenGL GLFW Service: publish Gamepads recognized by GLFW as resource --- frontend/resources/include/GamepadState.h | 94 ++++++++++++ .../opengl_glfw/OpenGL_GLFW_Service.hpp | 37 +++++ .../opengl_glfw/gl/OpenGL_GLFW_Service.cpp | 140 +++++++++++++++++- 3 files changed, 267 insertions(+), 4 deletions(-) create mode 100644 frontend/resources/include/GamepadState.h diff --git a/frontend/resources/include/GamepadState.h b/frontend/resources/include/GamepadState.h new file mode 100644 index 0000000000..5af211be1e --- /dev/null +++ b/frontend/resources/include/GamepadState.h @@ -0,0 +1,94 @@ +/* + * GamepadState.h + * + * Copyright (C) 2022 by VISUS (Universitaet Stuttgart). + * Alle Rechte vorbehalten. + */ + +#pragma once + +#include +#include +#include +#include + +namespace megamol { +namespace frontend_resources { + +// SDL compatible XBox-like gamepad layout +// as defined in glfw3.h +struct GamepadState { + // Axis, Button enumeration taken from the GLFW docs + // https://www.glfw.org/docs/3.3/group__input.html + enum class Axis : unsigned int { + LEFT_X = 0, + LEFT_Y = 1, + RIGHT_X = 2, + RIGHT_Y = 3, + LEFT_TRIGGER = 4, + RIGHT_TRIGGER = 5, + LAST = RIGHT_TRIGGER, + }; + enum class Button : unsigned int { + A = 0, + B = 1, + X = 2, + Y = 3, + LEFT_BUMPER = 4, + RIGHT_BUMPER = 5, + BACK = 6, + START = 7, + GUIDE = 8, + LEFT_THUMB = 9, + RIGHT_THUMB = 10, + DPAD_UP = 11, + DPAD_RIGHT = 12, + DPAD_DOWN = 13, + DPAD_LEFT = 14, + LAST = DPAD_LEFT, + CROSS = A, + CIRCLE = B, + SQUARE = X, + TRIANGLE = Y, + }; + enum class ButtonIs : unsigned char { + Released = 0, + Pressed = 1, + }; + + unsigned char buttons[15] = {}; + float axes[6] = {}; + + std::string name; + std::string guid; + + float axis(const Axis a) const { + return axes[static_cast(a)]; + } + + unsigned char button(const Button b) const { + return buttons[static_cast(b)]; + } + + bool pressed(const unsigned char button) const { + return button == static_cast(ButtonIs::Pressed); + } + + bool released(const unsigned char button) const { + return button == static_cast(ButtonIs::Released); + } + +#define zero(X) std::memset(X, 0, sizeof(X)) + void clear() { + zero(axes); + zero(buttons); + } +#undef zero; +}; + +struct Connected_Gamepads { + std::list> gamepads; +}; + +} /* end namespace frontend_resources */ +} /* end namespace megamol */ diff --git a/frontend/services/opengl_glfw/OpenGL_GLFW_Service.hpp b/frontend/services/opengl_glfw/OpenGL_GLFW_Service.hpp index fe704b9929..150f2d6740 100644 --- a/frontend/services/opengl_glfw/OpenGL_GLFW_Service.hpp +++ b/frontend/services/opengl_glfw/OpenGL_GLFW_Service.hpp @@ -11,12 +11,16 @@ #include "AbstractFrontendService.hpp" #include "Framebuffer_Events.h" #include "GL_STUB.h" +#include "GamepadState.h" #include "KeyboardMouse_Events.h" #include "OpenGL_Context.h" #include "OpenGL_Helper.h" #include "WindowManipulation.h" #include "Window_Events.h" +#include +#include + namespace megamol::frontend { struct WindowPlacement { @@ -103,6 +107,9 @@ class OpenGL_GLFW_Service final : public AbstractFrontendService { // framebuffer events void glfw_onFramebufferSize_func(const int widthpx, const int heightpx); + // gamepad/joystick events + void glfw_onJoystickConnect_func(const int jid, const int event); + private: void register_glfw_callbacks(); void do_every_second(); @@ -128,6 +135,36 @@ class OpenGL_GLFW_Service final : public AbstractFrontendService { std::vector m_renderResourceReferences; std::vector m_requestedResourcesNames; std::vector m_requestedResourceReferences; + + using GamepadState = megamol::frontend_resources::GamepadState; + + struct Joystick { + int id = -1; + + bool is_gamepad = false; + std::string gamepad_name; + + GamepadState gamepad_state; + + std::string joystick_name; + std::string joystick_GUID; + + float* joystick_axes = nullptr; // owned by GLFW + unsigned char* joystick_buttons = nullptr; // owned by GLFW + unsigned char* joystick_hats = nullptr; // owned by GLFW + + int joystick_axes_count = 0; + int joystick_buttons_count = 0; + int joystick_hats_count = 0; + + void* user_ptr = nullptr; + }; + std::unordered_map m_joysticks; + Joystick make_joystick(const int jid); + int poll_joystick_state(Joystick& j); + void poll_joysticks(); + + frontend_resources::Connected_Gamepads m_connected_gamepads_resource; }; } // namespace megamol::frontend diff --git a/frontend/services/opengl_glfw/gl/OpenGL_GLFW_Service.cpp b/frontend/services/opengl_glfw/gl/OpenGL_GLFW_Service.cpp index 526fe6ddf4..90017b65fe 100644 --- a/frontend/services/opengl_glfw/gl/OpenGL_GLFW_Service.cpp +++ b/frontend/services/opengl_glfw/gl/OpenGL_GLFW_Service.cpp @@ -316,6 +316,16 @@ void megamol::frontend_resources::WindowManipulation::set_fullscreen(const Fulls namespace megamol::frontend { +// to register joysticks from the glfw callback with the opengl/glfw service, we sadly need this detour via the static ptr +static megamol::frontend::OpenGL_GLFW_Service* glfw_service_ptr = nullptr; + +static void glfw_joystick_callback(const int jid, const int event) { + if (!glfw_service_ptr) + return; + + glfw_service_ptr->glfw_onJoystickConnect_func(jid, event); +} + struct OpenGL_GLFW_Service::PimplData { GLFWwindow* glfwContextWindowPtr{nullptr}; OpenGL_GLFW_Service::Config config; // keep copy of user-provided config @@ -552,6 +562,9 @@ bool OpenGL_GLFW_Service::init(const Config& config) { if (m_pimpl->config.windowPlacement.pos || m_pimpl->config.windowPlacement.fullScreen) ::glfwSetWindowPos(window_ptr, m_pimpl->config.windowPlacement.x, m_pimpl->config.windowPlacement.y); + // static ptr to glfw service to register connected joysticks + // this ptr is used in the joystick registration callback + glfw_service_ptr = this; register_glfw_callbacks(); int vsync = (m_pimpl->config.enableVsync) ? 1 : 0; @@ -576,16 +589,21 @@ bool OpenGL_GLFW_Service::init(const Config& config) { m_windowManipulation.set_mouse_cursor = [&](const int cursor_id) -> void { update_glfw_mouse_cursors(cursor_id); }; // make the events and resources managed/provided by this service available to the outside world - m_renderResourceReferences = {{frontend_resources::KeyboardEvents_Req_Name, m_keyboardEvents}, + m_renderResourceReferences = { + {frontend_resources::KeyboardEvents_Req_Name, m_keyboardEvents}, {frontend_resources::MouseEvents_Req_Name, m_mouseEvents}, {frontend_resources::WindowEvents_Req_Name, m_windowEvents}, - //{"FramebufferEvents", m_framebufferEvents}, // pushes own events into global FramebufferEvents {frontend_resources::OpenGL_Context_Req_Name, m_opengl_context}, {frontend_resources::WindowManipulation_Req_Name, m_windowManipulation}, - {frontend_resources::OpenGL_Helper_Req_Name, m_opengl_helper}}; + {frontend_resources::OpenGL_Helper_Req_Name, m_opengl_helper}, + {"Connected_Gamepads", m_connected_gamepads_resource}, + }; m_requestedResourcesNames = { - "FrameStatistics", "FramebufferEvents", frontend_resources::MegaMolGraph_SubscriptionRegistry_Req_Name}; + "FrameStatistics", + "FramebufferEvents", + frontend_resources::MegaMolGraph_SubscriptionRegistry_Req_Name, + }; m_pimpl->last_time = std::chrono::system_clock::now(); @@ -700,6 +718,15 @@ void OpenGL_GLFW_Service::register_glfw_callbacks() { ::glfwSetFramebufferSizeCallback(window_ptr, [](GLFWwindow* wnd, int widthpx, int heightpx) { that->glfw_onFramebufferSize_func(widthpx, heightpx); }); + ::glfwSetJoystickCallback(glfw_joystick_callback); + // GLFW does not issue callbacks for joypads already present upon startup + // check present joysticks manually + for (auto i = GLFW_JOYSTICK_1; i <= GLFW_JOYSTICK_LAST; i++) { + if (::glfwJoystickPresent(i) == GLFW_TRUE && m_joysticks.find(i) == m_joysticks.end()) { + this->glfw_onJoystickConnect_func(i, GLFW_CONNECTED); + } + } + // set current framebuffer state as pending event glfwGetFramebufferSize( window_ptr, &this->m_framebufferEvents.previous_state.width, &this->m_framebufferEvents.previous_state.height); @@ -736,6 +763,8 @@ void OpenGL_GLFW_Service::updateProvidedResources() { // event struct get filled via GLFW callbacks when new input events come in during glfwPollEvents() ::glfwPollEvents(); // may only be called from main thread + poll_joysticks(); + m_windowEvents.time = glfwGetTime(); // from GLFW Docs: // Do not assume that callbacks will only be called through glfwPollEvents(). @@ -873,6 +902,109 @@ void OpenGL_GLFW_Service::glfw_onFramebufferSize_func(const int widthpx, const i this->m_framebufferEvents.size_events.emplace_back(frontend_resources::FramebufferState{widthpx, heightpx}); } +#define checked(X) \ + if (!(X)) { \ + return -j.id; \ + } + +// if joystick is not connected anymore, returns negative number +// else returns current id of joystick +int OpenGL_GLFW_Service::poll_joystick_state(Joystick& j) { + checked(j.joystick_axes = const_cast(glfwGetJoystickAxes(j.id, &j.joystick_axes_count))); + checked(j.joystick_buttons = const_cast(glfwGetJoystickButtons(j.id, &j.joystick_buttons_count))); + checked(j.joystick_hats = const_cast(glfwGetJoystickHats(j.id, &j.joystick_hats_count))); + + if (j.is_gamepad) { + GLFWgamepadstate state; + j.is_gamepad = (glfwGetGamepadState(j.id, &state) == GLFW_TRUE); + + static_assert(sizeof(j.gamepad_state.axes) == sizeof(state.axes)); + static_assert(sizeof(j.gamepad_state.buttons) == sizeof(state.buttons)); + + std::memcpy(j.gamepad_state.axes, state.axes, sizeof(state.axes)); + std::memcpy(j.gamepad_state.buttons, state.buttons, sizeof(state.buttons)); + } + + return j.id; +} + +OpenGL_GLFW_Service::Joystick OpenGL_GLFW_Service::make_joystick(const int jid) { + Joystick j; + + j.id = jid; + + j.joystick_name = std::string{glfwGetJoystickName(jid)}; + j.joystick_GUID = std::string{glfwGetJoystickGUID(jid)}; + + j.is_gamepad = (glfwJoystickIsGamepad(jid) == GLFW_TRUE); + + if (j.is_gamepad) { + j.gamepad_name = std::string{glfwGetGamepadName(jid)}; + j.gamepad_state.name = j.gamepad_name; + j.gamepad_state.guid = j.joystick_GUID; + } + + poll_joystick_state(j); + + return j; +} + +void OpenGL_GLFW_Service::poll_joysticks() { + std::vector erase_ids; + + for (auto& kj : m_joysticks) { + auto& j = kj.second; + + // controller may be marked as disconnected by joystick callback + if (j.id >= 0) + j.id = poll_joystick_state(j); + + // joystick turned out to be disconnected during state polling or by GLFW callback + // dont erase elemets during traversal of the map, delete later + if (j.id < 0) + erase_ids.push_back(kj.first); + } + + for (auto id : erase_ids) { + auto& j = m_joysticks.at(id); + + if (j.is_gamepad) + m_connected_gamepads_resource.gamepads.remove_if( + [&](GamepadState const& p) { return p.guid == j.joystick_GUID; }); + + m_joysticks.erase(id); + } +} + +void OpenGL_GLFW_Service::glfw_onJoystickConnect_func(const int jid, const int event) { + if (event == GLFW_CONNECTED) { + this->m_joysticks.insert({jid, make_joystick(jid)}); + + const auto& joystick = m_joysticks.at(jid); + log("connected joystick " + std::to_string(jid) + ": " + joystick.joystick_name + " GUID " + + joystick.joystick_GUID); + log("joystick axes: " + std::to_string(joystick.joystick_axes_count) + + ", buttons: " + std::to_string(joystick.joystick_buttons_count) + + ", hats: " + std::to_string(joystick.joystick_hats_count)); + if (joystick.is_gamepad) { + log("joystick is gamepad: " + joystick.gamepad_name); + m_connected_gamepads_resource.gamepads.push_back(std::reference_wrapper{joystick.gamepad_state}); + } + } else if (event == GLFW_DISCONNECTED) { + auto& j = m_joysticks.at(jid); + log("removed joystick " + std::to_string(jid) + ": " + j.joystick_name + + (j.is_gamepad ? "/" + j.gamepad_name : "")); + + // polling GLFW joystick state (in the main loop) checks whether the joystick is still available + // if the joystick turns out to be disconnected, GLFW triggers this disconnect-callback within the state polling callback + // this would lead to the size of the joystick map to change (invalidating memory) during the actual looping through joysticks! + // so here, instead of deleting it, we mark a joystick for deletion, and it gets deleted in the state polling loop + j.id = -1; + } else { + log_error("GLFW joystick event unknown: " + std::to_string(event)); + } +} + void OpenGL_GLFW_Service::glfw_onWindowSize_func( const int width, const int height) { // in screen coordinates, of the window this->m_windowEvents.size_events.emplace_back(std::tuple(width, height)); From c2a92a92b920d48c75d0205c337ba8c864300b4c Mon Sep 17 00:00:00 2001 From: Sergej Geringer Date: Fri, 18 Feb 2022 13:28:11 +0100 Subject: [PATCH 2/6] add Exotic Inputs Service: injecting input device controls (e.g. gamepads) into graph modules (e.g. View 3D) --- frontend/main/src/main.cpp | 6 + frontend/services/CMakeLists.txt | 3 + .../exotic_inputs/ExoticInputs_Service.cpp | 138 ++++++++++++++++++ .../exotic_inputs/ExoticInputs_Service.hpp | 59 ++++++++ 4 files changed, 206 insertions(+) create mode 100644 frontend/services/exotic_inputs/ExoticInputs_Service.cpp create mode 100644 frontend/services/exotic_inputs/ExoticInputs_Service.hpp diff --git a/frontend/main/src/main.cpp b/frontend/main/src/main.cpp index 7f2eaba399..f747a7a52b 100644 --- a/frontend/main/src/main.cpp +++ b/frontend/main/src/main.cpp @@ -7,6 +7,7 @@ #include "CLIConfigParsing.h" #include "CUDA_Service.hpp" #include "Command_Service.hpp" +#include "ExoticInputs_Service.hpp" #include "FrameStatistics_Service.hpp" #include "FrontendServiceCollection.hpp" #include "GUI_Service.hpp" @@ -100,6 +101,10 @@ int main(const int argc, const char** argv) { openglConfig.forceWindowSize = config.force_window_size; gl_service.setPriority(2); + megamol::frontend::ExoticInputs_Service exoticinputs_service; + megamol::frontend::ExoticInputs_Service::Config exoticinputsConfig; + exoticinputs_service.setPriority(gl_service.getPriority() + 1); // depends on gamepad updates from GLFW + megamol::frontend::GUI_Service gui_service; megamol::frontend::GUI_Service::Config guiConfig; guiConfig.backend = (with_gl) ? (megamol::gui::GUIRenderBackend::OPEN_GL) : (megamol::gui::GUIRenderBackend::CPU); @@ -196,6 +201,7 @@ int main(const int argc, const char** argv) { if (with_gl) { services.add(gl_service, &openglConfig); } + services.add(exoticinputs_service, &exoticinputsConfig); services.add(gui_service, &guiConfig); services.add(lua_service_wrapper, &luaConfig); services.add(screenshot_service, &screenshotConfig); diff --git a/frontend/services/CMakeLists.txt b/frontend/services/CMakeLists.txt index 3d314937e1..db4d5fd309 100644 --- a/frontend/services/CMakeLists.txt +++ b/frontend/services/CMakeLists.txt @@ -32,6 +32,7 @@ file(GLOB_RECURSE header_files RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "remote_service/*.hpp" "profiling_service/*.hpp" "vr_service/*.hpp" + "exotic_inputs/*.hpp" # "service_template/*.hpp" ) @@ -48,6 +49,7 @@ file(GLOB_RECURSE source_files RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "remote_service/*.cpp" "profiling_service/*.cpp" "vr_service/*.cpp" + "exotic_inputs/*.cpp" # "service_template/*.cpp" ) @@ -109,6 +111,7 @@ target_include_directories(${PROJECT_NAME} PUBLIC "gui/3rd" "gui/src" "vr_service" + "exotic_inputs" # "service_template" ) diff --git a/frontend/services/exotic_inputs/ExoticInputs_Service.cpp b/frontend/services/exotic_inputs/ExoticInputs_Service.cpp new file mode 100644 index 0000000000..364be09aa3 --- /dev/null +++ b/frontend/services/exotic_inputs/ExoticInputs_Service.cpp @@ -0,0 +1,138 @@ +/* + * ExoticInputs_Service.cpp + * + * Copyright (C) 2022 by MegaMol Team + * Alle Rechte vorbehalten. + */ + +#include "ExoticInputs_Service.hpp" + +#include "GamepadState.h" + +#include "GUIRegisterWindow.h" // register UI window for remote control +#include "imgui_stdlib.h" + +// local logging wrapper for your convenience until central MegaMol logger established +#include "mmcore/utility/log/Log.h" + +#include + +static const std::string service_name = "ExoticInputs_Service: "; +static void log(std::string const& text) { + const std::string msg = service_name + text; + megamol::core::utility::log::Log::DefaultLog.WriteInfo(msg.c_str()); +} + +static void log_error(std::string const& text) { + const std::string msg = service_name + text; + megamol::core::utility::log::Log::DefaultLog.WriteError(msg.c_str()); +} + +static void log_warning(std::string const& text) { + const std::string msg = service_name + text; + megamol::core::utility::log::Log::DefaultLog.WriteWarn(msg.c_str()); +} + + +namespace megamol { +namespace frontend { + +ExoticInputs_Service::ExoticInputs_Service() { + // init members to default states +} + +ExoticInputs_Service::~ExoticInputs_Service() { + // clean up raw pointers you allocated with new, which is bad practice and nobody does +} + +bool ExoticInputs_Service::init(void* configPtr) { + if (configPtr == nullptr) + return false; + + return init(*static_cast(configPtr)); +} + +bool ExoticInputs_Service::init(const Config& config) { + // initialize your service and its provided resources using config parameters + // for now, you dont need to worry about your service beeing initialized or closed multiple times + // init() and close() only get called once in the lifetime of each service object + // but maybe more instances of your service will get created? this may be relevant for central resources you manage (like libraries, network connections). + + m_providedResourceReferences = {}; + + m_requestedResourcesNames = { + "optional", + "optional", + }; + + log("initialized successfully"); + return true; +} + +void ExoticInputs_Service::close() {} + +std::vector& ExoticInputs_Service::getProvidedResources() { + return m_providedResourceReferences; +} + +const std::vector ExoticInputs_Service::getRequestedResourceNames() const { + return m_requestedResourcesNames; +} + +void ExoticInputs_Service::setRequestedResources(std::vector resources) { + m_requestedResourceReferences = resources; + + gamepad_window(); +} + +void ExoticInputs_Service::updateProvidedResources() {} + +void ExoticInputs_Service::digestChangedRequestedResources() {} + +void ExoticInputs_Service::resetProvidedResources() {} + +void ExoticInputs_Service::preGraphRender() {} + +void ExoticInputs_Service::postGraphRender() {} + +void ExoticInputs_Service::gamepad_window() const { + auto maybe_gamepad_resource = + m_requestedResourceReferences[0].getOptionalResource(); + auto maybe_window_resource = + m_requestedResourceReferences[1].getOptionalResource(); + + if (maybe_gamepad_resource.has_value() && maybe_window_resource.has_value()) { + // draw window showing gamepad stats + auto& gui_window = maybe_window_resource.value().get(); + auto& connected_gamepads = maybe_gamepad_resource.value().get(); + + gui_window.register_window("GLFW Gamepads ", [&](megamol::gui::AbstractWindow::BasicConfig& window_config) { + for (auto& gamepad : connected_gamepads.gamepads) { + window_config.flags = ImGuiWindowFlags_AlwaysAutoResize; + + ImGui::Text(gamepad.get().name.c_str()); + + int i = 0; + for (auto axis : gamepad.get().axes) { + ImGui::Text(("Axis " + std::to_string(i) + ": " + std::to_string(axis)).c_str()); + ImGui::SameLine(); + i++; + } + ImGui::NewLine(); + + i = 0; + for (auto button : gamepad.get().buttons) { + ImGui::Text(("Button " + std::to_string(i) + ": " + std::to_string(button)).c_str()); + ImGui::SameLine(); + i++; + } + ImGui::NewLine(); + ImGui::Separator(); + } + }); + } +} + + +} // namespace frontend +} // namespace megamol diff --git a/frontend/services/exotic_inputs/ExoticInputs_Service.hpp b/frontend/services/exotic_inputs/ExoticInputs_Service.hpp new file mode 100644 index 0000000000..6858f305dd --- /dev/null +++ b/frontend/services/exotic_inputs/ExoticInputs_Service.hpp @@ -0,0 +1,59 @@ +/* + * ExoticInputs_Service.hpp + * + * Copyright (C) 2022 by MegaMol Team + * Alle Rechte vorbehalten. + */ + +#pragma once + +#include "AbstractFrontendService.hpp" + +namespace megamol { +namespace frontend { + +// The Exotic Inputs Service is supposed to implement the injection of raw input device state (e.g.gamepads via GLFW) +// into the MegaMol Graph by matching input devices to graph modules which can be controlled in some usefull way using that device +// So this ervice looks up present input device resources, or manages input devices on his own, and knows how to connect them to graph modules +// For example, gamepads may be used to control View3D Cameras or positions of Clipping Planes in space +class ExoticInputs_Service final : public AbstractFrontendService { +public: + struct Config {}; + + std::string serviceName() const override { + return "ExoticInputs_Service"; + } + + ExoticInputs_Service(); + ~ExoticInputs_Service(); + + bool init(const Config& config); + bool init(void* configPtr) override; + void close() override; + + std::vector& getProvidedResources() override; + const std::vector getRequestedResourceNames() const override; + void setRequestedResources(std::vector resources) override; + + void updateProvidedResources() override; + void digestChangedRequestedResources() override; + void resetProvidedResources() override; + void preGraphRender() override; + void postGraphRender() override; + + // from AbstractFrontendService + // int setPriority(const int p) // priority initially 0 + // int getPriority() const; + // bool shouldShutdown() const; // shutdown initially false + // void setShutdown(const bool s = true); + +private: + std::vector m_providedResourceReferences; + std::vector m_requestedResourcesNames; + std::vector m_requestedResourceReferences; + + void gamepad_window() const; +}; + +} // namespace frontend +} // namespace megamol From 697e49e8bde6d9b38ae194797d9c4b71f32df1dd Mon Sep 17 00:00:00 2001 From: Sergej Geringer Date: Fri, 6 Oct 2023 13:22:16 +0200 Subject: [PATCH 3/6] OpenGL GLFW Service: harden joystick polling, use vectors in GamepadState --- frontend/resources/include/GamepadState.h | 38 +++++++-- .../opengl_glfw/OpenGL_GLFW_Service.hpp | 5 +- .../opengl_glfw/gl/OpenGL_GLFW_Service.cpp | 81 ++++++++++++++----- 3 files changed, 98 insertions(+), 26 deletions(-) diff --git a/frontend/resources/include/GamepadState.h b/frontend/resources/include/GamepadState.h index 5af211be1e..081852e8ff 100644 --- a/frontend/resources/include/GamepadState.h +++ b/frontend/resources/include/GamepadState.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace megamol { namespace frontend_resources { @@ -55,9 +56,21 @@ struct GamepadState { Released = 0, Pressed = 1, }; + enum class HatIs : unsigned char { + CENTERED = 0, + UP = 1, + RIGHT = 2, + DOWN = 4, + LEFT = 8, + RIGHT_UP = (RIGHT | UP), + RIGHT_DOWN = (RIGHT | DOWN), + LEFT_UP = (LEFT | UP), + LEFT_DOWN = (LEFT | DOWN), + }; - unsigned char buttons[15] = {}; - float axes[6] = {}; + std::vector axes = {}; + std::vector buttons = {}; + std::vector hats = {}; std::string name; std::string guid; @@ -74,16 +87,29 @@ struct GamepadState { return button == static_cast(ButtonIs::Pressed); } + bool pressed(const Button b) const { + return pressed(button(b)); + } + bool released(const unsigned char button) const { return button == static_cast(ButtonIs::Released); } -#define zero(X) std::memset(X, 0, sizeof(X)) + HatIs hat(const unsigned int index) const { + return static_cast(hats[index]); + } + + int hat_count() const { + return hats.size(); + } + void clear() { - zero(axes); - zero(buttons); + buttons.clear(); + axes.clear(); + hats.clear(); + name.clear(); + guid.clear(); } -#undef zero; }; struct Connected_Gamepads { diff --git a/frontend/services/opengl_glfw/OpenGL_GLFW_Service.hpp b/frontend/services/opengl_glfw/OpenGL_GLFW_Service.hpp index 150f2d6740..dcc05c84f9 100644 --- a/frontend/services/opengl_glfw/OpenGL_GLFW_Service.hpp +++ b/frontend/services/opengl_glfw/OpenGL_GLFW_Service.hpp @@ -157,10 +157,13 @@ class OpenGL_GLFW_Service final : public AbstractFrontendService { int joystick_buttons_count = 0; int joystick_hats_count = 0; + int gamepad_axes_count = 0; + int gamepad_buttons_count = 0; + void* user_ptr = nullptr; }; std::unordered_map m_joysticks; - Joystick make_joystick(const int jid); + std::optional make_joystick(const int jid); int poll_joystick_state(Joystick& j); void poll_joysticks(); diff --git a/frontend/services/opengl_glfw/gl/OpenGL_GLFW_Service.cpp b/frontend/services/opengl_glfw/gl/OpenGL_GLFW_Service.cpp index 90017b65fe..fa8bf6ea7c 100644 --- a/frontend/services/opengl_glfw/gl/OpenGL_GLFW_Service.cpp +++ b/frontend/services/opengl_glfw/gl/OpenGL_GLFW_Service.cpp @@ -914,37 +914,74 @@ int OpenGL_GLFW_Service::poll_joystick_state(Joystick& j) { checked(j.joystick_buttons = const_cast(glfwGetJoystickButtons(j.id, &j.joystick_buttons_count))); checked(j.joystick_hats = const_cast(glfwGetJoystickHats(j.id, &j.joystick_hats_count))); + j.gamepad_state.hats.resize(j.joystick_hats_count); + std::memcpy(j.gamepad_state.hats.data(), j.joystick_hats, sizeof(j.joystick_hats[0]) * j.joystick_hats_count); + if (j.is_gamepad) { GLFWgamepadstate state; j.is_gamepad = (glfwGetGamepadState(j.id, &state) == GLFW_TRUE); - static_assert(sizeof(j.gamepad_state.axes) == sizeof(state.axes)); - static_assert(sizeof(j.gamepad_state.buttons) == sizeof(state.buttons)); + j.gamepad_axes_count = sizeof(state.axes) / sizeof(state.axes[0]); + j.gamepad_buttons_count = sizeof(state.buttons) / sizeof(state.buttons[0]); + + j.gamepad_state.axes.resize(j.gamepad_axes_count); + std::memcpy(j.gamepad_state.axes.data(), state.axes, sizeof(state.axes)); - std::memcpy(j.gamepad_state.axes, state.axes, sizeof(state.axes)); - std::memcpy(j.gamepad_state.buttons, state.buttons, sizeof(state.buttons)); + j.gamepad_state.buttons.resize(j.gamepad_buttons_count); + std::memcpy(j.gamepad_state.buttons.data(), state.buttons, sizeof(state.buttons)); } return j.id; } +#undef checked -OpenGL_GLFW_Service::Joystick OpenGL_GLFW_Service::make_joystick(const int jid) { +std::optional OpenGL_GLFW_Service::make_joystick(const int jid) { + // during setup of the joystick the device may disconnect again, e.g. due to bluetooth or cable problems + // so every time we query GLFW for info we need to make sure whether the device is still connected + // and optionally return no joystick at all Joystick j; j.id = jid; - j.joystick_name = std::string{glfwGetJoystickName(jid)}; - j.joystick_GUID = std::string{glfwGetJoystickGUID(jid)}; + auto jname = glfwGetJoystickName(jid); + if (!jname) + return std::nullopt; + + j.joystick_name = std::string{jname}; + + auto jguid = glfwGetJoystickGUID(jid); + if (!jguid) + return std::nullopt; + + j.joystick_GUID = std::string{jguid}; j.is_gamepad = (glfwJoystickIsGamepad(jid) == GLFW_TRUE); if (j.is_gamepad) { - j.gamepad_name = std::string{glfwGetGamepadName(jid)}; + auto gname = glfwGetGamepadName(jid); + if (!gname) + return std::nullopt; + + j.gamepad_name = std::string{gname}; j.gamepad_state.name = j.gamepad_name; j.gamepad_state.guid = j.joystick_GUID; } - poll_joystick_state(j); + if (glfwJoystickPresent(jid) != GLFW_TRUE) + return std::nullopt; + + if (poll_joystick_state(j) < 0) + return std::nullopt; + + if (j.is_gamepad) { + if (j.joystick_axes_count != j.gamepad_axes_count) + log("Joystick axes count " + std::to_string(j.joystick_axes_count) + " but Gamepad axes count " + + std::to_string(j.gamepad_axes_count)); + + if (j.joystick_buttons_count != j.gamepad_buttons_count) + log("Joystick buttons count " + std::to_string(j.joystick_buttons_count) + " but Gamepad buttons count " + + std::to_string(j.gamepad_buttons_count)); + } return j; } @@ -978,7 +1015,11 @@ void OpenGL_GLFW_Service::poll_joysticks() { void OpenGL_GLFW_Service::glfw_onJoystickConnect_func(const int jid, const int event) { if (event == GLFW_CONNECTED) { - this->m_joysticks.insert({jid, make_joystick(jid)}); + auto j = make_joystick(jid); + if (!j.has_value()) + return; + + this->m_joysticks.insert({jid, std::move(j.value())}); const auto& joystick = m_joysticks.at(jid); log("connected joystick " + std::to_string(jid) + ": " + joystick.joystick_name + " GUID " + @@ -991,15 +1032,17 @@ void OpenGL_GLFW_Service::glfw_onJoystickConnect_func(const int jid, const int e m_connected_gamepads_resource.gamepads.push_back(std::reference_wrapper{joystick.gamepad_state}); } } else if (event == GLFW_DISCONNECTED) { - auto& j = m_joysticks.at(jid); - log("removed joystick " + std::to_string(jid) + ": " + j.joystick_name + - (j.is_gamepad ? "/" + j.gamepad_name : "")); - - // polling GLFW joystick state (in the main loop) checks whether the joystick is still available - // if the joystick turns out to be disconnected, GLFW triggers this disconnect-callback within the state polling callback - // this would lead to the size of the joystick map to change (invalidating memory) during the actual looping through joysticks! - // so here, instead of deleting it, we mark a joystick for deletion, and it gets deleted in the state polling loop - j.id = -1; + if (m_joysticks.count(jid) > 0) { + auto& j = m_joysticks.at(jid); + log("removed joystick " + std::to_string(jid) + ": " + j.joystick_name + + (j.is_gamepad ? "/" + j.gamepad_name : "")); + + // polling GLFW joystick state (in the main loop) checks whether the joystick is still available + // if the joystick turns out to be disconnected, GLFW triggers this disconnect-callback within the state polling callback + // this would lead to the size of the joystick map to change (invalidating memory) during the actual looping through joysticks! + // so here, instead of deleting it, we mark a joystick for deletion, and it gets deleted in the state polling loop + j.id = -1; + } } else { log_error("GLFW joystick event unknown: " + std::to_string(event)); } From 32dbfc39f625dfb37b66d84c31c14f571b320807 Mon Sep 17 00:00:00 2001 From: Sergej Geringer Date: Fri, 6 Oct 2023 13:30:09 +0200 Subject: [PATCH 4/6] View/RenderInput: decouple camera pose and projection data structs allows to overwrite either data field for view cameras without touching the other --- .../view/AbstractView_EventConsumption.cpp | 52 +++++++++---------- frontend/resources/include/RenderInput.h | 38 ++++++-------- frontend/resources/include/ViewRenderInputs.h | 9 ++-- frontend/services/vr_service/VR_Service.cpp | 41 ++++++++++----- 4 files changed, 75 insertions(+), 65 deletions(-) diff --git a/core/src/view/AbstractView_EventConsumption.cpp b/core/src/view/AbstractView_EventConsumption.cpp index 961ff2572f..fdbfae315d 100644 --- a/core/src/view/AbstractView_EventConsumption.cpp +++ b/core/src/view/AbstractView_EventConsumption.cpp @@ -106,37 +106,35 @@ void view_poke_rendering(AbstractViewInterface& view, megamol::frontend_resource bool camera_state_mutable_by_view = true; - if (renderinput.camera_view_projection_parameters_override.has_value()) { - auto& proj_parameters = renderinput.camera_view_projection_parameters_override.value(); - - auto& in_pose = proj_parameters.pose; - auto cam_pose = Camera::Pose{ + if (renderinput.camera_view_pose_parameters_override.has_value()) { + auto& in_pose = renderinput.camera_view_pose_parameters_override.value(); + camera.setPose(Camera::Pose{ in_pose.position, in_pose.direction, in_pose.up, - glm::cross(in_pose.direction, in_pose.up) // right, as computed by Camrea - }; + glm::cross(in_pose.direction, in_pose.up), // right, as computed by Camrea + }); + } - auto& in_proj = proj_parameters.projection; + if (renderinput.camera_view_projection_parameters_override.has_value()) { + auto& in_proj = renderinput.camera_view_projection_parameters_override.value(); switch (in_proj.type) { - case RenderInput::CameraViewProjectionParameters::ProjectionType::PERSPECTIVE: - camera = Camera{cam_pose, - Camera::PerspectiveParameters{ - in_proj.fovy, // FieldOfViewY fovy; //< vertical field of view - in_proj.aspect, // AspectRatio aspect; //< aspect ratio of the camera frustrum - in_proj.near_plane, // NearPlane near_plane; //< near clipping plane - in_proj.far_plane, // FarPlane far_plane; //< far clipping plane - tile // ImagePlaneTile image_plane_tile; //< tile on the image plane displayed by camera - }}; + case RenderInput::CameraProjection::ProjectionType::PERSPECTIVE: + camera.setPerspectiveProjection(Camera::PerspectiveParameters{ + in_proj.fovy, // FieldOfViewY fovy; //< vertical field of view + in_proj.aspect, // AspectRatio aspect; //< aspect ratio of the camera frustrum + in_proj.near_plane, // NearPlane near_plane; //< near clipping plane + in_proj.far_plane, // FarPlane far_plane; //< far clipping plane + tile, // ImagePlaneTile image_plane_tile; //< tile on the image plane displayed by camera + }); break; - case RenderInput::CameraViewProjectionParameters::ProjectionType::ORTHOGRAPHIC: - camera = Camera{cam_pose, - Camera::OrthographicParameters{ - in_proj - .fovy, // FrustrumHeight frustrum_height; //< vertical size of the orthographic frustrum in world space - in_proj.aspect, // AspectRatio aspect; //< aspect ratio of the camera frustrum - in_proj.near_plane, // NearPlane near_plane; //< near clipping plane - in_proj.far_plane, // FarPlane far_plane; //< far clipping plane - tile // ImagePlaneTile image_plane_tile; //< tile on the image plane displayed by camera - }}; + case RenderInput::CameraProjection::ProjectionType::ORTHOGRAPHIC: + camera.setOrthographicProjection(Camera::OrthographicParameters{ + in_proj + .fovy, // FrustrumHeight frustrum_height; //< vertical size of the orthographic frustrum in world space + in_proj.aspect, // AspectRatio aspect; //< aspect ratio of the camera frustrum + in_proj.near_plane, // NearPlane near_plane; //< near clipping plane + in_proj.far_plane, // FarPlane far_plane; //< far clipping plane + tile, // ImagePlaneTile image_plane_tile; //< tile on the image plane displayed by camera + }); break; } diff --git a/frontend/resources/include/RenderInput.h b/frontend/resources/include/RenderInput.h index d531cbc9c6..333a390908 100644 --- a/frontend/resources/include/RenderInput.h +++ b/frontend/resources/include/RenderInput.h @@ -32,29 +32,25 @@ struct RenderInput { // this is a rude copy-paste of the camera parameters from // Camera.h to avoid linking and including the core/view in the resources CMakeLists.txt // when things break or in doubt do as the Camera says or needs! the frontend is not here to be served, but to serve. - struct CameraViewProjectionParameters { - enum class ProjectionType { PERSPECTIVE, ORTHOGRAPHIC }; - - struct Pose { - glm::vec3 position; - glm::vec3 direction; - glm::vec3 up; - }; - - struct Projection { - ProjectionType type; - float - fovy; //< vertical field of view / orthographic frustrum_height: vertical size of the orthographic frustrum in world space - float aspect; //< aspect ratio of the camera frustrum - float near_plane; //< near clipping plane - float far_plane; //< far clipping plane - }; + struct CameraPose { + glm::vec3 position; + glm::vec3 direction; + glm::vec3 up; + }; + std::optional camera_view_pose_parameters_override = + std::nullopt; //< if camera pose is overridden, this view still needs to render in local resolution - Pose pose; - Projection projection; + struct CameraProjection { + enum class ProjectionType { PERSPECTIVE, ORTHOGRAPHIC }; + ProjectionType type; + float + fovy; //< vertical field of view / orthographic frustrum_height: vertical size of the orthographic frustrum in world space + float aspect; //< aspect ratio of the camera frustrum + float near_plane; //< near clipping plane + float far_plane; //< far clipping plane }; - std::optional camera_view_projection_parameters_override = - std::nullopt; //< if camera matrices are overridden, this view still needs to render in local resolution + std::optional camera_view_projection_parameters_override = + std::nullopt; //< if camera porjection is overridden, this view still needs to render in local resolution double instanceTime_sec = 0.0; //< monotone high resolution time in seconds since first frame rendering of some (any) view diff --git a/frontend/resources/include/ViewRenderInputs.h b/frontend/resources/include/ViewRenderInputs.h index f12db53d52..72dc22ed32 100644 --- a/frontend/resources/include/ViewRenderInputs.h +++ b/frontend/resources/include/ViewRenderInputs.h @@ -32,8 +32,10 @@ struct ViewRenderInputs : public frontend_resources::RenderInputsUpdate { std::function()> render_input_camera_handler = []() { return std::nullopt; }; - std::function()> - render_input_camera_parameters_handler = []() { return std::nullopt; }; + std::function()> + render_input_camera_pose_parameters_handler = []() { return std::nullopt; }; + std::function()> + render_input_camera_projection_parameters_handler = []() { return std::nullopt; }; void update() override { auto fbo_size = render_input_framebuffer_size_handler(); @@ -45,7 +47,8 @@ struct ViewRenderInputs : public frontend_resources::RenderInputsUpdate { render_input.local_tile_relative_end = {tile.tile_end_normalized.first, tile.tile_end_normalized.second}; render_input.camera_matrices_override = render_input_camera_handler(); - render_input.camera_view_projection_parameters_override = render_input_camera_parameters_handler(); + render_input.camera_view_projection_parameters_override = render_input_camera_projection_parameters_handler(); + render_input.camera_view_pose_parameters_override = render_input_camera_pose_parameters_handler(); } frontend::FrontendResource get_resource() override { diff --git a/frontend/services/vr_service/VR_Service.cpp b/frontend/services/vr_service/VR_Service.cpp index 3d05f1e57c..2d367713a2 100644 --- a/frontend/services/vr_service/VR_Service.cpp +++ b/frontend/services/vr_service/VR_Service.cpp @@ -509,17 +509,25 @@ bool megamol::frontend::VR_Service::KolabBW::add_entry_point(std::string const& }; }; - auto make_view_projection_parameters = [&](interop::CameraView const& iview, interop::CameraProjection const& iproj) - -> std::function()> { + auto make_view_projection_parameters = [&](interop::CameraProjection const& iproj) + -> std::function()> { return [&]() { - return std::make_optional(frontend_resources::RenderInput::CameraViewProjectionParameters{ - {// Pose - glm::vec3(toGlm(iview.eyePos)), - glm::normalize(glm::vec3(toGlm(iview.lookAtPos)) - glm::vec3(toGlm(iview.eyePos))), - glm::vec3(toGlm(iview.camUpDir))}, - {// Projection - frontend_resources::RenderInput::CameraViewProjectionParameters::ProjectionType::PERSPECTIVE, - iproj.fieldOfViewY_rad, iproj.aspect, iproj.nearClipPlane, iproj.farClipPlane}}); + return std::make_optional(frontend_resources::RenderInput::CameraProjection{ + frontend_resources::RenderInput::CameraProjection::ProjectionType::PERSPECTIVE, + iproj.fieldOfViewY_rad, + iproj.aspect, + iproj.nearClipPlane, + iproj.farClipPlane, + }); + }; + }; + + auto make_view_pose_parameters = [&](interop::CameraView const& iview) + -> std::function()> { + return [&]() { + return std::make_optional(frontend_resources::RenderInput::CameraPose{glm::vec3(toGlm(iview.eyePos)), + glm::normalize(glm::vec3(toGlm(iview.lookAtPos)) - glm::vec3(toGlm(iview.eyePos))), + glm::vec3(toGlm(iview.camUpDir))}); }; }; @@ -538,7 +546,8 @@ bool megamol::frontend::VR_Service::KolabBW::add_entry_point(std::string const& // it is ok to reference the pimpl data because view rendering and pimpl data updates DONT happen at the same time (see VR Service pre/postGraphRender) auto replace_ep_handlers = [&, fbo_size_handler, tile_handler](frontend_resources::EntryPoint& ep, - auto make_matrices, auto make_view_projection_parameters) { + auto make_matrices, auto make_view_projection_parameters, + auto make_view_pose_parameters) { // setting correct FBO size is important accessViewRenderInput(ep.entry_point_data).render_input_framebuffer_size_handler = fbo_size_handler; @@ -550,8 +559,10 @@ bool megamol::frontend::VR_Service::KolabBW::add_entry_point(std::string const& // enforcing view/projection matrices may actually lead to problems // so for now we pass the actual camera parametrization to the view - accessViewRenderInput(ep.entry_point_data).render_input_camera_parameters_handler = + accessViewRenderInput(ep.entry_point_data).render_input_camera_projection_parameters_handler = make_view_projection_parameters; + accessViewRenderInput(ep.entry_point_data).render_input_camera_pose_parameters_handler = + make_view_pose_parameters; pimpl.ep_handles_installed = true; }; @@ -566,9 +577,11 @@ bool megamol::frontend::VR_Service::KolabBW::add_entry_point(std::string const& // the actual render input lookup/update (calling the render input callbacks) // happens in ImagePresentation.RenderNextFrame() right before view rendering replace_ep_handlers(*pimpl.left_ep, make_matrices(pimpl.stereoCameraView.leftEyeView, pimpl.cameraProjection), - make_view_projection_parameters(pimpl.stereoCameraView.leftEyeView, pimpl.cameraProjection)); + make_view_projection_parameters(pimpl.cameraProjection), + make_view_pose_parameters(pimpl.stereoCameraView.leftEyeView)); replace_ep_handlers(*pimpl.right_ep, make_matrices(pimpl.stereoCameraView.rightEyeView, pimpl.cameraProjection), - make_view_projection_parameters(pimpl.stereoCameraView.rightEyeView, pimpl.cameraProjection)); + make_view_projection_parameters(pimpl.cameraProjection), + make_view_pose_parameters(pimpl.stereoCameraView.rightEyeView)); }); log("added entry point " + entry_point_name + " for Unity-KolabBW Stereo Rendering."); From b4e1336908b8e5b2ea14b5ec8dba60be8b6bf38c Mon Sep 17 00:00:00 2001 From: Sergej Geringer Date: Tue, 10 Oct 2023 16:39:32 +0200 Subject: [PATCH 5/6] Exotic Inputs Service: add camera controllers --- .../exotic_inputs/camera_controllers.cpp | 238 ++++++++++++++++++ .../exotic_inputs/camera_controllers.h | 120 +++++++++ 2 files changed, 358 insertions(+) create mode 100644 frontend/services/exotic_inputs/camera_controllers.cpp create mode 100644 frontend/services/exotic_inputs/camera_controllers.h diff --git a/frontend/services/exotic_inputs/camera_controllers.cpp b/frontend/services/exotic_inputs/camera_controllers.cpp new file mode 100644 index 0000000000..741acc3138 --- /dev/null +++ b/frontend/services/exotic_inputs/camera_controllers.cpp @@ -0,0 +1,238 @@ +#include "camera_controllers.h" + +#include +#include + +using namespace camera_controllers; + +/* + * The algorithms in the following manipulators namespace are derived from the thecam/thelib library. + * We provide the manipulators code according and subject to the original TheLib License. + * Authors of the TheLib Library seem to be: Sebastian Grottel, Christoph Müller (VISUS, Uni Stuttgart) + * + * The routines themselves may have been altered by the MegaMol team from the inital thecam/thelib code, + * and have been refactored for this codebase to fit into a data-driven camera pose manipulation paradigm. + * + * Other code in this file, outside of the manipulators namespace, stems from the MegaMol codebase + * or is original work in context of the camera manipulators/controllers refactoring and redesign. + */ + +namespace manipulators { +/* + * Copyright (C) 2016 TheLib Team (http://www.thelib.org/license) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of TheLib, TheLib Team, nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THELIB TEAM AS IS AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THELIB TEAM BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +Pose arcball(Pose pose, Action2D const rotation_rad, glm::vec3 const rotation_center) { + // split movement into horizontal and vertical (in camera space) + auto rx = rotation_rad.x; + auto ry = rotation_rad.y; + + // rotate horizontally + glm::quat rot_pitch = glm::angleAxis(rx, pose.up); + pose.right = glm::rotate(rot_pitch, pose.right); + pose.direction = glm::rotate(rot_pitch, pose.direction); + + // rotate vertically + glm::quat rot_yaw = glm::angleAxis(ry, -pose.right); + pose.direction = glm::rotate(rot_yaw, pose.direction); + pose.up = glm::rotate(rot_yaw, pose.up); + + // transform s.t. rotation center is origin + auto shifted_pos = pose.position - rotation_center; + shifted_pos = glm::rotate(rot_pitch, shifted_pos); + shifted_pos = glm::rotate(rot_yaw, shifted_pos); + + // transform back + pose.position = shifted_pos + glm::vec3(rotation_center); + + return pose; +} + +Pose translate_forward(Pose pose, Action1D const distance) { + pose.position += distance * pose.direction; + return pose; +} +Pose translate_horizontally(Pose pose, Action1D const distance) { + pose.position += distance * pose.right; + return pose; +} +Pose translate_vertically(Pose pose, Action1D const distance) { + pose.position += distance * pose.up; + return pose; +} + +} // namespace manipulators + +Pose arcball::apply(Pose pose, Action2D const rotation_rad) { + return manipulators::arcball(pose, rotation_rad, rotation_center); +} + +Pose orbit_altitude::apply(Pose pose, Action1D const move_distance) { + auto position = pose.position; + auto v = glm::normalize(rotation_center - position); + auto altitude = glm::length(rotation_center - position); + switch (movement) { + case Mode::Absolute: + pose.position = position - move_distance; + break; + case Mode::Relative_Factor: { + pose.position = position - (v * move_distance); + } break; + } + + return pose; +} + +Pose rotate::pitch(Pose pose, Action1D const rotation_rad) { + pose.direction = glm::rotate(pose.direction, rotation_rad, pose.right); + pose.up = glm::rotate(pose.up, rotation_rad, pose.right); + + return pose; +} +Pose rotate::yaw(Pose pose, Action1D const rotation_rad) { + auto up = fixed_world_up.has_value() ? fixed_world_up.value() : pose.up; + pose.direction = glm::rotate(pose.direction, rotation_rad, up); + pose.right = glm::rotate(pose.right, rotation_rad, up); + + return pose; +} +Pose rotate::roll(Pose pose, Action1D const rotation_rad) { + pose.up = glm::rotate(pose.up, rotation_rad, pose.direction); + pose.right = glm::rotate(pose.right, rotation_rad, pose.direction); + + return pose; +} +Pose rotate::apply(Pose pose, Action3D const pitch_yaw_roll_rad) { + auto first = pitch(pose, pitch_yaw_roll_rad.x); + auto second = yaw(first, pitch_yaw_roll_rad.y); + auto third = roll(second, pitch_yaw_roll_rad.z); + + return third; +} + +Pose translate::move_forward(Pose pose, Action1D const move_distance) { + return manipulators::translate_forward(pose, move_distance); +} +Pose translate::move_horizontally(Pose pose, Action1D const move_distance) { + return manipulators::translate_horizontally(pose, move_distance); +} +Pose translate::move_vertically(Pose pose, Action1D const move_distance) { + return manipulators::translate_vertically(pose, move_distance); +} +Pose translate::apply(Pose pose, Action3D const horizontally_vertically_forward_distance) { + auto first = move_horizontally(pose, horizontally_vertically_forward_distance.x); + auto second = move_vertically(first, horizontally_vertically_forward_distance.y); + auto third = move_forward(second, horizontally_vertically_forward_distance.z); + + return third; +} + +Pose turntable::apply(Pose pose, Action2D const rotation_rad) { + auto rx = rotation_rad.x; + auto ry = rotation_rad.y; + + // split movement into horizontal and vertical (in camera space) + glm::quat rot_lat; + glm::quat rot_lon; + + // rotate horizontally + rot_lon = glm::angleAxis(rx, glm::vec3(0.0, 1.0, 0.0)); + pose.right = glm::rotate(rot_lon, pose.right); + pose.direction = glm::rotate(rot_lon, pose.direction); + pose.up = glm::rotate(rot_lon, pose.up); + + // rotate vertically + rot_lat = glm::angleAxis(ry, -pose.right); + pose.direction = glm::rotate(rot_lat, pose.direction); + pose.up = glm::rotate(rot_lat, pose.up); + + // transform s.t. rotation center is origin + auto shifted_pos = pose.position - glm::vec3(rotation_center); + shifted_pos = glm::rotate(rot_lon, shifted_pos); + shifted_pos = glm::rotate(rot_lat, shifted_pos); + + // transform back + pose.position = shifted_pos + glm::vec3(rotation_center); + + return pose; +} + +Pose fps::apply(Pose pose, Action3D const horizontally_vertically_forward_distance, Action2D const pitch_yaw_rad) { + pose = translate{}.apply(pose, horizontally_vertically_forward_distance); + pose = rotate{}.apply(pose, {pitch_yaw_rad, 0.0f}); + return pose; +} + +Axis1D camera_controllers::to_axis1d(bool const b) { + return Axis1D{(float) b}; +} +Axis1D camera_controllers::to_axis1d(bool const min, bool const max) { + return (float) min * (-1.0f) + (float) max * 1.0f; +} + +Action1D camera_controllers::simple_screen_delta_to_rotation_rad(Axis1D const delta) { + const float rad_factor = glm::pi() / 2.f; // assume ~90 degrees fov + return delta * rad_factor; +} + +Action2D camera_controllers::simple_screen_delta_to_rotation_rad(Axis2D const delta) { + return { + simple_screen_delta_to_rotation_rad(delta.x), + simple_screen_delta_to_rotation_rad(delta.y), + }; +} + +Action3D camera_controllers::simple_screen_delta_to_rotation_rad(Axis3D const delta) { + return { + simple_screen_delta_to_rotation_rad(delta.x), + simple_screen_delta_to_rotation_rad(delta.y), + simple_screen_delta_to_rotation_rad(delta.z), + }; +} + +Action1D camera_controllers::screen_fov_to_rotation_rad(Axis1D const delta, float const fov_rad) { + // delta == 0 => return 0 + // delta == 1.0 => return +fov/2 + // delta == -1.0 => return -fov/2 + return (fov_rad * 0.5f) * delta; +} + +Action2D camera_controllers::screen_fov_to_rotation_rad(Axis2D const delta, glm::vec2 const fov_rad) { + return { + screen_fov_to_rotation_rad(delta.x, fov_rad.x), + screen_fov_to_rotation_rad(delta.y, fov_rad.y), + }; +} + +Action3D camera_controllers::screen_fov_to_rotation_rad(Axis3D const delta, glm::vec3 const fov_rad) { + return { + screen_fov_to_rotation_rad(delta.x, fov_rad.x), + screen_fov_to_rotation_rad(delta.y, fov_rad.y), + screen_fov_to_rotation_rad(delta.z, fov_rad.z), + }; +} diff --git a/frontend/services/exotic_inputs/camera_controllers.h b/frontend/services/exotic_inputs/camera_controllers.h new file mode 100644 index 0000000000..5b96a5167d --- /dev/null +++ b/frontend/services/exotic_inputs/camera_controllers.h @@ -0,0 +1,120 @@ +#pragma once + +#include "mmcore/view/Camera.h" + +#include + +namespace camera_controllers { + +// Input Concept: IAAC +// (Device ->) Inputs -> Axis -> Action -> Controller/Command (-> {Camera, Handler, ...}) + +// Axes are inputs derived from raw device inputs (e.g., which come from GLFW) +// Axes have a normalized range of values (while device inputs may have arbitrary values) +// and may be derived/constructed from raw device inputs in an application-specific way +// Example: keyboard keys W and S move should move the camera forward/backward +// The Axis1D with range [-1.0, 1.0] to represent the input to move in direction of the +// camera forward vector can be defined as 1.0 * key_pressed(W) - 1.0 * key_pressed(S) +// other input device sources for the same Axis1D semantics may come from controller sticks +// or motion sensors, each with inidivual min/max value ranges coming from the hardware + +/// Axis0D range is considered [false, true] +using Axis0D = bool; + +/// Axis values are considered to be in the range [-1.0f, 1.0f] +using Axis1D = float; + +/// Axis values are considered to be in the range [-1.0f, 1.0f] +using Axis2D = glm::vec2; + +/// Axis values are considered to be in the range [-1.0f, 1.0f] +using Axis3D = glm::vec3; + +Axis1D to_axis1d(bool const b); +Axis1D to_axis1d(bool const min, bool const max); + +// While Axes are considered as abstract inputs in a normalized range, +// Actions are Inputs interpreted in the context of the framework or scene, +// i.e. they have specific semantics in context of the things they are applied to. +// Actions encode data that serves as input to controllers. +// Controllers implement behaviour on scene objects or application functionality +// and are the final step to transform inputs to +// state changes in the program observable by the user. +// Example: Action1D to move the camera forward in the scene can be derived +// form an Axis1D in range [-1.0, 1.0] by means of a default distance the camera +// is supposed to move upon user inputs, i.e. a movement delta that fits the scale +// of the scene and user intentions. +// Action1D move_camera = axis1d_inputs * camera_step_size +// Camera::Pose new_pose = controller::camera_translation(old_pose, move_camera) +// i.e. scaling of Axes derived from device inputs needs to be done in +// context of the scale of the scene and thus is a different concept than Axes +using Action0D = bool; +using Action1D = float; +using Action2D = glm::vec2; +using Action3D = glm::vec3; + +Action1D simple_screen_delta_to_rotation_rad(Axis1D const delta); +Action2D simple_screen_delta_to_rotation_rad(Axis2D const delta); +Action3D simple_screen_delta_to_rotation_rad(Axis3D const delta); + +Action1D screen_fov_to_rotation_rad(Axis1D const delta, float const fov_rad); +Action2D screen_fov_to_rotation_rad(Axis2D const delta, glm::vec2 const fov_rad); +Action3D screen_fov_to_rotation_rad(Axis3D const delta, glm::vec3 const fov_rad); + +using Camera = megamol::core::view::Camera; +using Pose = Camera::Pose; + +struct arcball { + glm::vec3 rotation_center = {0.0f, 0.0f, 0.0f}; + + Pose apply(Pose pose, Action2D const rotation_rad); +}; + +struct orbit_altitude { + glm::vec3 rotation_center = {0.0f, 0.0f, 0.0f}; + + enum Mode { Absolute, Relative_Factor }; + Mode movement = Relative_Factor; + + Pose apply(Pose pose, Action1D const move_distance); +}; + +struct rotate { + std::optional fixed_world_up = std::nullopt; + + /// Rotates the camera around the right vector + Pose pitch(Pose pose, Action1D const rotation_rad); + + /// Rotates the camera around the up vector + Pose yaw(Pose pose, Action1D const rotation_rad); + + /// Rotates the camera around the view vector + Pose roll(Pose pose, Action1D const rotation_rad); + + Pose apply(Pose pose, Action3D const pitch_yaw_roll_rad); +}; + +struct translate { + /// Move the camera in view direction + Pose move_forward(Pose pose, Action1D const move_distance); + + /// Move the camera along its right vector. + Pose move_horizontally(Pose pose, Action1D const move_distance); + + /// Move the camera along its up vector. + Pose move_vertically(Pose pose, Action1D const move_distance); + + Pose apply(Pose pose, Action3D const horizontally_vertically_forward_distance); +}; + +struct turntable { + glm::vec3 rotation_center = {0.0f, 0.0f, 0.0f}; + + Pose apply(Pose pose, Action2D const rotation_rad); +}; + +struct fps { + Pose apply(Pose pose, Action3D const horizontally_vertically_forward_distance, Action2D const pitch_yaw_rad); +}; + +}; // namespace camera_controllers From f42d9c2ce5a05f5cf550806659361535a8d03f73 Mon Sep 17 00:00:00 2001 From: Sergej Geringer Date: Fri, 6 Oct 2023 13:39:30 +0200 Subject: [PATCH 6/6] Exocit Inputs Service: manipulate view3d camera pose using gamepad --- .../exotic_inputs/ExoticInputs_Service.cpp | 253 +++++++++++++++++- .../exotic_inputs/ExoticInputs_Service.hpp | 19 ++ 2 files changed, 271 insertions(+), 1 deletion(-) diff --git a/frontend/services/exotic_inputs/ExoticInputs_Service.cpp b/frontend/services/exotic_inputs/ExoticInputs_Service.cpp index 364be09aa3..776c319c14 100644 --- a/frontend/services/exotic_inputs/ExoticInputs_Service.cpp +++ b/frontend/services/exotic_inputs/ExoticInputs_Service.cpp @@ -8,14 +8,19 @@ #include "ExoticInputs_Service.hpp" #include "GamepadState.h" +#include "ModuleGraphSubscription.h" #include "GUIRegisterWindow.h" // register UI window for remote control +#include "camera_controllers.h" #include "imgui_stdlib.h" +#include "mmcore/Module.h" +#include "mmcore/view/AbstractViewInterface.h" // local logging wrapper for your convenience until central MegaMol logger established #include "mmcore/utility/log/Log.h" #include +#include static const std::string service_name = "ExoticInputs_Service: "; static void log(std::string const& text) { @@ -63,6 +68,7 @@ bool ExoticInputs_Service::init(const Config& config) { m_requestedResourcesNames = { "optional", "optional", + frontend_resources::MegaMolGraph_SubscriptionRegistry_Req_Name, }; log("initialized successfully"); @@ -82,12 +88,248 @@ const std::vector ExoticInputs_Service::getRequestedResourceNames() void ExoticInputs_Service::setRequestedResources(std::vector resources) { m_requestedResourceReferences = resources; + frontend_resources::ModuleGraphSubscription subscription("Exotic_Inputs"); + + //subscription.EnableEntryPoint = [&](core::ModuleInstance_t const& module_inst) { + //}; + //subscription.DisableEntryPoint = [&](core::ModuleInstance_t const& module_inst) { + //}; + + subscription.AddModule = [&](core::ModuleInstance_t const& module_inst) { + auto& view_module_name = module_inst.request.id; + // this cast will never fail but the modulePtr should be valid nonetheless + // no need to check right entry point because they represent the same view module + auto* ptr = static_cast(module_inst.modulePtr.get()); + if (!ptr) { + log_error("entry point " + view_module_name + + " does not seem to have a valid megamol::core::Module* (is nullptr)."); + return true; + } + + // if the entry point is not a 3d view there is no point in doing stereo for it + const auto* view = dynamic_cast(ptr); + if (view == nullptr || view->GetViewDimension() != core::view::AbstractViewInterface::ViewDimension::VIEW_3D) { + log_error("entry point " + view_module_name + + " does not seem to be a supported 3D View Type. Not using it to inject camera manipulation via " + "exotic inputs."); + return true; + } + + m_view3d_modules.emplace(view_module_name, ptr); + + if (!m_controlled_view3d.has_value()) + m_controlled_view3d = m_view3d_modules.begin()->first; + + return true; + }; + + subscription.DeleteModule = [&](core::ModuleInstance_t const& module_inst) { + auto id = module_inst.request.id; + + if (m_view3d_modules.count(module_inst.request.id)) { + m_view3d_modules.erase(id); + + if (m_controlled_view3d == id) { + m_controlled_view3d = + m_view3d_modules.empty() ? std::nullopt : std::make_optional(m_view3d_modules.begin()->first); + } + } + + return true; + }; + + subscription.RenameModule = [&](std::string const& old_name, std::string const& new_name, + core::ModuleInstance_t const& module_inst) { + auto old_id = old_name; + auto new_id = new_name; + + if (old_id != new_id && m_view3d_modules.count(old_id)) { + + auto v = m_view3d_modules.at(old_id); + m_view3d_modules.erase(old_id); + m_view3d_modules.emplace(new_id, v); + + if (m_controlled_view3d == old_id) { + m_controlled_view3d = new_id; + } + } + + return true; + }; + + //subscription.AddParameters = + // [&](std::vector const& param_slots) { + // }; + //subscription.RemoveParameters = + // [&](std::vector const& param_slots) { + // }; + //subscription.ParameterChanged = + // [&](megamol::frontend_resources::ModuleGraphSubscription::ParamSlotPtr const& param_slot, + // std::string const& new_value) { + // }; + //subscription.ParameterPresentationChanged = + // [&](megamol::frontend_resources::ModuleGraphSubscription::ParamSlotPtr const& param_slot) { + // }; + //subscription.AddCall = [&](core::CallInstance_t const& call_inst) { + //}; + //subscription.DeleteCall = [&](core::CallInstance_t const& call_inst) { + //}; + + auto& megamolgraph_subscription = const_cast( + m_requestedResourceReferences[2].getResource()); + megamolgraph_subscription.subscribe(subscription); + gamepad_window(); } void ExoticInputs_Service::updateProvidedResources() {} -void ExoticInputs_Service::digestChangedRequestedResources() {} +void ExoticInputs_Service::digestChangedRequestedResources() { + auto maybe_gamepad_resource = + m_requestedResourceReferences[0].getOptionalResource(); + + auto threshold = [&](const float f) -> float { return (std::fabs(f) < m_axis_threshold) ? (0.0f) : (f); }; + auto thresholdv2 = [&](const glm::vec2 v) -> glm::vec2 { return glm::vec2(threshold(v.x), threshold(v.y)); }; + auto thresholdv3 = [&](const glm::vec3 v) -> glm::vec3 { + return glm::vec3(threshold(v.x), threshold(v.y), threshold(v.z)); + }; + auto norm = [](const float f) -> float { return (f + 1.0f) * 0.5f; }; + + auto apply_pose_controls = [&](const camera_controllers::Pose pose, const PoseManipulator mode, + const megamol::frontend_resources::GamepadState pad, const float scale, + const glm::vec3 center) -> camera_controllers::Pose { + using Pad = megamol::frontend_resources::GamepadState; + + const glm::vec2 stick_left = thresholdv2(glm::vec2{pad.axis(Pad::Axis::LEFT_X), pad.axis(Pad::Axis::LEFT_Y)}); + const glm::vec2 stick_right = + thresholdv2(glm::vec2{pad.axis(Pad::Axis::RIGHT_X), pad.axis(Pad::Axis::RIGHT_Y)}); + const float vertial = threshold( + norm(pad.axis(Pad::Axis::LEFT_TRIGGER)) * (-1.0f) + norm(pad.axis(Pad::Axis::RIGHT_TRIGGER)) * (1.0f)); + + const float rad_per_axis_unit = 0.1f; + const float trans_per_axis_unit = 0.05f * scale; + + const glm::vec2 rotation = rad_per_axis_unit * (stick_right * glm::vec2{-1.0f, 1.0f}); + + const glm::vec3 translation = trans_per_axis_unit * glm::vec3{stick_left.x, vertial, -stick_left.y}; + + const glm::vec3 rotation_center = center; + const float orbit_distance = stick_left.y * trans_per_axis_unit; + + using namespace camera_controllers; + switch (mode) { + case PoseManipulator::Arcball: + return arcball{rotation_center}.apply( + orbit_altitude{rotation_center, orbit_altitude::Mode::Relative_Factor}.apply(pose, orbit_distance), + rotation); + break; + //case PoseManipulator::Turntable: + // return turntable{}.apply( + // orbit_altitude{rotation_center, orbit_altitude::Mode::Relative_Factor}.apply(pose, orbit_distance), + // rotation); + //break; + case PoseManipulator::FPS: + return fps{}.apply(pose, translation, glm::vec2{-rotation.y, rotation.x} * 0.1f); + break; + default: + break; + } + return pose; + }; + + if (!m_controlled_view3d.has_value()) { + return; + } + + auto view3d_ptr = m_view3d_modules.at(m_controlled_view3d.value()); + + auto* view = const_cast( + dynamic_cast(static_cast(view3d_ptr))); + + if (!view) { + log_error("view reference in exotic inputs service did not resolve to View3D"); + return; + } + + auto bbox = view->GetBoundingBoxes(); + auto center = glm::vec3{bbox.BoundingBox().CalcCenter().GetX(), bbox.BoundingBox().CalcCenter().GetY(), + bbox.BoundingBox().CalcCenter().GetZ()}; + auto scale = view->GetBoundingBoxes().BoundingBox().LongestEdge(); + + if (!maybe_gamepad_resource.has_value()) { + return; + } + + auto& connected_gamepads = maybe_gamepad_resource.value().get(); + + // to avoid switching camera mode every frame when button pressed for several frames, use this counter + static size_t mode_changed = 0; + mode_changed++; + const size_t button_threshold = 60 * 1; // allow switching mode roughly every second + + for (const auto& pad_ : connected_gamepads.gamepads) { + auto& pad = pad_.get(); + + // switch active entry point and camera control + if ((pad.pressed(megamol::frontend_resources::GamepadState::Button::DPAD_LEFT) || + pad.pressed(megamol::frontend_resources::GamepadState::Button::DPAD_RIGHT)) && + mode_changed > button_threshold) { + mode_changed = 0; + + if (m_controlled_view3d.has_value() && m_view3d_modules.size() > 1) { + auto it = m_view3d_modules.find(m_controlled_view3d.value()); + + if (pad.pressed(megamol::frontend_resources::GamepadState::Button::DPAD_LEFT)) { + if (it == m_view3d_modules.begin()) { + it = (--m_view3d_modules.end()); + } else { + it--; + } + } + + if (pad.pressed(megamol::frontend_resources::GamepadState::Button::DPAD_RIGHT)) { + if ((++it) == m_view3d_modules.end()) { + it = m_view3d_modules.begin(); + } + } + + m_controlled_view3d = it->first; + } + } + + // reset camera via view + if (pad.pressed(megamol::frontend_resources::GamepadState::Button::LEFT_BUMPER) && + mode_changed > button_threshold) { + mode_changed = 0; + + auto move_by = scale * 3; + + glm::vec3 position = {center.x - move_by, center.y, center.z}; + glm::vec3 forward = glm::normalize(center - position); + glm::vec3 up = {0.0f, 1.0f, 0.0f}; + megamol::core::view::Camera::Pose new_pose(position, forward, up, glm::cross(forward, up)); + + auto cam = view->GetCamera(); + cam.setPose(new_pose); + view->SetCamera(cam); + } + + // switch camera control mode + if (pad.pressed(megamol::frontend_resources::GamepadState::Button::RIGHT_BUMPER) && + mode_changed > button_threshold) { + mode_changed = 0; + + m_manipulation_mode = static_cast((static_cast(m_manipulation_mode) + 1) % + static_cast(PoseManipulator::COUNT)); + } + + auto camera = view->GetCamera(); + auto in_pose = camera.getPose(); + auto out_pose = apply_pose_controls(in_pose, m_manipulation_mode, pad, scale, center); + camera.setPose(out_pose); + view->SetCamera(camera); + } +} void ExoticInputs_Service::resetProvidedResources() {} @@ -127,6 +369,15 @@ void ExoticInputs_Service::gamepad_window() const { i++; } ImGui::NewLine(); + + i = 0; + for (auto hat : gamepad.get().hats) { + ImGui::Text(("Hat " + std::to_string(i) + ": " + std::to_string(hat)).c_str()); + ImGui::SameLine(); + i++; + } + ImGui::NewLine(); + ImGui::Separator(); } }); diff --git a/frontend/services/exotic_inputs/ExoticInputs_Service.hpp b/frontend/services/exotic_inputs/ExoticInputs_Service.hpp index 6858f305dd..660cf346b1 100644 --- a/frontend/services/exotic_inputs/ExoticInputs_Service.hpp +++ b/frontend/services/exotic_inputs/ExoticInputs_Service.hpp @@ -9,6 +9,9 @@ #include "AbstractFrontendService.hpp" +#include +#include + namespace megamol { namespace frontend { @@ -53,6 +56,22 @@ class ExoticInputs_Service final : public AbstractFrontendService { std::vector m_requestedResourceReferences; void gamepad_window() const; + + enum class PoseManipulator { + Arcball = 0, + //Turntable, + FPS, + COUNT, + }; + PoseManipulator m_manipulation_mode = PoseManipulator::Arcball; + + float m_axis_threshold = 0.05f; + + // all known view3d entry points + std::map m_view3d_modules; + + // the entry point currently controlled + std::optional m_controlled_view3d = std::nullopt; }; } // namespace frontend