diff --git a/cmake/dependencies/imgui-CMakeLists.txt.in b/cmake/dependencies/imgui-CMakeLists.txt.in new file mode 100644 index 0000000..64f5a68 --- /dev/null +++ b/cmake/dependencies/imgui-CMakeLists.txt.in @@ -0,0 +1,57 @@ +project(imgui LANGUAGES CXX) +if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) + # If top level project + SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib/${CMAKE_BUILD_TYPE}/) +else() + # If called via add_subdirectory() + SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../lib/${CMAKE_BUILD_TYPE}/) +endif() + +# Glob for sources +file(GLOB imgui_sources LIST_DIRECTORIES false ${PROJECT_SOURCE_DIR}/imgui/*.cpp ${PROJECT_SOURCE_DIR}/imgui/*.h) + +# Include back-end specific sources +set(imgui_sources ${imgui_sources} + ${PROJECT_SOURCE_DIR}/imgui/backends/imgui_impl_sdl.h + ${PROJECT_SOURCE_DIR}/imgui/backends/imgui_impl_sdl.cpp + ${PROJECT_SOURCE_DIR}/imgui/backends/imgui_impl_opengl3.cpp + ${PROJECT_SOURCE_DIR}/imgui/backends/imgui_impl_opengl3.h + ${PROJECT_SOURCE_DIR}/imgui/backends/imgui_impl_opengl3_loader.h +) +if(MSVC) +set(imgui_sources ${imgui_sources} + ${PROJECT_SOURCE_DIR}/imgui/backends/imgui_impl_win32.h + ${PROJECT_SOURCE_DIR}/imgui/backends/imgui_impl_win32.cpp +) +endif() + +# Depends on the cpp and header files in case of changes +add_library(imgui STATIC ${imgui_sources}) + + +# Specify the include directory, to be forwared to targets which link against the imgui target. +# Mark this as SYSTEM INTERFACE, so that it does not result in compiler warnings being generated for dependent projects. +# For our use case, this is up a folder so we can use imgui/imgui.h as the include, by resolving the relative path to get an abs path +get_filename_component(imgui_inc_dir ${imgui_SOURCE_DIR} REALPATH) +target_include_directories("${PROJECT_NAME}" SYSTEM INTERFACE ${imgui_SOURCE_DIR}/imgui ${imgui_SOURCE_DIR}) + +# Because we're using a nested dir rather than root, specify include path +target_include_directories("${PROJECT_NAME}" PUBLIC ${imgui_SOURCE_DIR}/imgui) +# Also depends on SDL for backend +target_include_directories("${PROJECT_NAME}" SYSTEM PRIVATE ${SDL2_INCLUDE_DIRS}) + +# Add some compile time definitions +target_compile_definitions(imgui INTERFACE $<$:IMGUI_DEBUG>) +set_target_properties(imgui PROPERTIES + COMPILE_DEFINITIONS "IMGUI_EXPORT" +) + +# Pic is sensible for any library +set_property(TARGET imgui PROPERTY POSITION_INDEPENDENT_CODE ON) + +# Suppress warnigns from this target. (Not currently required) +#include(${CMAKE_CURRENT_LIST_DIR}/../warnings.cmake) +#DisableCompilerWarnings(TARGET imgui) + +# Create an alias target for imgui to namespace it / make it more like other modern cmake +add_library(ImGui::ImGui ALIAS imgui) \ No newline at end of file diff --git a/cmake/dependencies/imgui.cmake b/cmake/dependencies/imgui.cmake new file mode 100644 index 0000000..77352da --- /dev/null +++ b/cmake/dependencies/imgui.cmake @@ -0,0 +1,39 @@ +######### +# imgui # +######### + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/modules/ ${CMAKE_MODULE_PATH}) +include(FetchContent) + +cmake_policy(SET CMP0079 NEW) + +# Change the source_dir to allow inclusion via imgui/imgui.h rather than imgui.h +FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG v1.88 + GIT_SHALLOW 1 + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/imgui-src/imgui + GIT_PROGRESS ON + # UPDATE_DISCONNECTED ON +) + +# imgui does not include anything + +# @todo - try finding the package first, assuming it sets system correctly when used. +FetchContent_GetProperties(imgui) +if(NOT imgui_POPULATED) + FetchContent_Populate(imgui) + + # The imgui repository does not include a CMakeLists.txt (or similar) + # Create our own cmake target for imgui. + + # If the target does not already exist, add it. + # @todo - make this far more robust. + if(NOT TARGET imgui) + # make a dynamically generated CMakeLists.txt which can be add_subdirectory'd instead, so that the .vcxproj goes in a folder. Just adding a project doesn't work. + configure_file(${CMAKE_CURRENT_LIST_DIR}/imgui-CMakeLists.txt.in ${FETCHCONTENT_BASE_DIR}/imgui-src/CMakeLists.txt @ONLY) + # We've now created CMakeLists.txt so we can use MakeAvailable + add_subdirectory(${imgui_SOURCE_DIR}/.. ${imgui_BINARY_DIR}) + endif() +endif() diff --git a/include/flamegpu/visualiser/FLAMEGPU_Visualisation.h b/include/flamegpu/visualiser/FLAMEGPU_Visualisation.h index 3d118fb..3b0f1da 100644 --- a/include/flamegpu/visualiser/FLAMEGPU_Visualisation.h +++ b/include/flamegpu/visualiser/FLAMEGPU_Visualisation.h @@ -3,6 +3,7 @@ #include #include +#include #include "flamegpu/visualiser/config/TexBufferConfig.h" @@ -35,11 +36,19 @@ class FLAMEGPU_Visualisation { const std::map& core_tex_buffers, const std::multimap& tex_buffers) { updateAgentStateBuffer(agent_name.c_str(), state_name.c_str(), buffLen, core_tex_buffers, tex_buffers); } + /** + * Provide an environment property, so it can be displayed + */ + void registerEnvironmentProperty(const std::string& property_name, void* ptr, std::type_index type, unsigned int elements, bool is_const); /** * Update the UI step counter * @note When this value is first set non-0, the visualiser assumes sim has begun executing */ void setStepCount(const unsigned int stepCount); + /** + * Provide the random seed, so it can be displayed in the debug menu + */ + void setRandomSeed(uint64_t randomSeed); /* * Start visualiser in background thread */ diff --git a/include/flamegpu/visualiser/config/ImGuiWidgets.h b/include/flamegpu/visualiser/config/ImGuiWidgets.h new file mode 100644 index 0000000..55423c5 --- /dev/null +++ b/include/flamegpu/visualiser/config/ImGuiWidgets.h @@ -0,0 +1,263 @@ +#ifndef INCLUDE_FLAMEGPU_VISUALISER_CONFIG_IMGUIWIDGETS_H_ +#define INCLUDE_FLAMEGPU_VISUALISER_CONFIG_IMGUIWIDGETS_H_ + +#include + +#include +#include +#include +#include // Not actually required, linter just thinks initialiser for max is fn call + +namespace flamegpu { +namespace visualiser { + +/** + * Template for converting a type to the corresponding ImGui type enum + * Useful when summing unknown values + * e.g. sum_input_t::result_t == double + * e.g. sum_input_t::result_t == uint64_t + */ +template struct imgui_type; +/** + * @see imgui_type + */ +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_S8; static constexpr const char* fmt = "%c"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_S8; static constexpr const char* fmt = "%hhd"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_U8; static constexpr const char* fmt = "%hhu"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_S16; static constexpr const char* fmt = "%hd"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_U16; static constexpr const char* fmt = "%hu"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_S32; static constexpr const char* fmt = "%d"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_U32; static constexpr const char* fmt = "%u"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_S64; static constexpr const char* fmt = "%lld"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_U64; static constexpr const char* fmt = "%llu"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_Float; static constexpr const char* fmt = "%g"; }; +template <> struct imgui_type { static constexpr ImGuiDataType_ t = ImGuiDataType_Double; static constexpr const char* fmt = "%g"; }; + +/** + * The most generic abstract ImGui element superclass + * All other elements inherit from this + */ +struct PanelElement { + virtual ~PanelElement() = default; + /** + * Triggers the underlying call to ImGui + */ + virtual bool addToImGui() = 0; + /** + * Provides copy construction behaviour + */ + virtual std::unique_ptr clone() const = 0; +}; +/** + * Separator element, creates a horizontal line between elements + */ +struct SeparatorElement : PanelElement { + /** + * Constructor + */ + SeparatorElement() { } + bool addToImGui() override { ImGui::Separator(); return false; } + [[nodiscard]] std::unique_ptr clone() const override { return std::unique_ptr(new SeparatorElement()); } +}; +/** + * Merely denotes that return value should be treated differently + */ +struct SectionElement : PanelElement { }; +/** + * Collapsible header element, creates a titled section which can be collapsed + */ +struct HeaderElement : SectionElement { + /** + * Constructor + * @param _text Text displayed in the section header + * @param _begin_open If true, the section will not begin in a collapsed state + */ + explicit HeaderElement(const std::string& _text, const bool _begin_open) + : text(_text) + , begin_open(_begin_open) { } + bool addToImGui() override { return ImGui::CollapsingHeader(text.c_str(), begin_open ? ImGuiTreeNodeFlags_DefaultOpen : 0); } + [[nodiscard]] std::unique_ptr clone() const override { return std::unique_ptr(new HeaderElement(text, begin_open)); } + std::string text; + bool begin_open; +}; +/** + * Ends a section, unhiding following elements + */ +struct EndSectionElement : SectionElement { + /** + * Constructor + */ + EndSectionElement() { } + bool addToImGui() override { return true; } + [[nodiscard]] std::unique_ptr clone() const override { return std::unique_ptr(new EndSectionElement()); } + std::string text; +}; +/** + * Label element, only contains a string used to hold it's text to be displayed + */ +struct LabelElement : PanelElement { + /** + * Constructor + * @param _text Text displayed on the label + */ + explicit LabelElement(const std::string &_text) + : text(_text) { } + bool addToImGui() override { ImGui::Text("%s", text.c_str()); return false; } + [[nodiscard]] std::unique_ptr clone() const override { return std::unique_ptr(new LabelElement(text)); } + std::string text; +}; +/** + * Environment property modifier generic + * All environment properties inherit from this + */ +struct EnvPropertyElement : PanelElement { + /** + * Constructor + * @param _name Name of the environment property + * @param _index Index of the environment property element (0 if the property is not an array property) + * @param _ptr_offset Pointer offset from the environment property origin (as this depending on the index and type size, 0 if the property is not an array property) + * @note _ptr_offset should be calculated using the type information available to the subclass + */ + explicit EnvPropertyElement(const std::string& _name, unsigned int _index, unsigned int _ptr_offset) + : data_ptr(nullptr) + , name(_name) + , index(_index) + , ptr_offset(_ptr_offset) { } + void* data_ptr; + std::string name; + unsigned int index; + unsigned int ptr_offset; + bool is_const = false; + bool addToImGui() override = 0; + std::unique_ptr clone() const override = 0; + const std::string& getName() const { return name; } + void setPtr(void *ptr) { data_ptr = static_cast(ptr) + ptr_offset; } + void setConst(bool _is_const) { is_const = _is_const; } + void setArray() { name += "[" + std::to_string(index) +"]"; } +}; +/** + * ImGui slider element for an environment property + */ +template +struct EnvPropertySlider : EnvPropertyElement { + /** + * Constructor + * @param _name Name of the environment property + * @param _index Index of the environment property element (0 if the property is not an array property) + * @param _min Minimum value of the slider + * @param _max Maximum value of the slider + */ + EnvPropertySlider(const std::string& _name, unsigned int _index, T _min, T _max) + : EnvPropertyElement(_name, _index, _index * sizeof(T)) + , min(_min) + , max(_max) { } + T min, max; + bool addToImGui() override { + if (this->data_ptr) { + if (this->is_const) ImGui::BeginDisabled(); + const bool rtn = ImGui::SliderScalar(this->name.c_str(), imgui_type::t, this->data_ptr, &this->min, &this->max, imgui_type::fmt, ImGuiSliderFlags_AlwaysClamp); + if (this->is_const) ImGui::EndDisabled(); + return rtn; + } + ImGui::Text("Loading...%s", this->name.c_str()); + return false; + } + [[nodiscard]] std::unique_ptr clone() const override { return std::unique_ptr(new EnvPropertySlider(this->name, this->index, this->min, this->max)); } +}; +/** + * ImGui drag element for an environment property + */ +template +struct EnvPropertyDrag : EnvPropertyElement { + /** + * Constructor + * @param _name Name of the environment property + * @param _index Index of the environment property element (0 if the property is not an array property) + * @param _min Minimum value that can be set + * @param _max Maximum value that can be set + * @param _speed Amount the value changes per pixel dragged + */ + EnvPropertyDrag(const std::string& _name, unsigned int _index, T _min, T _max, float _speed) + : EnvPropertyElement(_name, _index, _index * sizeof(T)) + , min(_min) + , max(_max) + , speed(_speed) { } + T min, max; + float speed; + bool addToImGui() override { + if (this->data_ptr) { + if (this->is_const) ImGui::BeginDisabled(); + const bool rtn = ImGui::DragScalar(this->name.c_str(), imgui_type::t, this->data_ptr, this->speed, &this->min, &this->max, imgui_type::fmt, ImGuiSliderFlags_AlwaysClamp); + if (this->is_const) ImGui::EndDisabled(); + return rtn; + } + ImGui::Text("Loading...%s", this->name.c_str()); + return false; + } + [[nodiscard]] std::unique_ptr clone() const override { return std::unique_ptr(new EnvPropertyDrag(this->name, this->index, this->min, this->max, this->speed)); } +}; +/** + * ImGui input with +/- step buttons for an environment property + */ +template +struct EnvPropertyInput : EnvPropertyElement { + /** + * Constructor + * @param _name Name of the environment property + * @param _index Index of the environment property element (0 if the property is not an array property) + * @param _step Change per button click + * @param _step_fast Change per tick when holding button (?) + */ + EnvPropertyInput(const std::string& _name, unsigned int _index, T _step, T _step_fast) + : EnvPropertyElement(_name, _index, _index * sizeof(T)) + , step(_step) + , step_fast(_step_fast) { } + T step, step_fast; + bool addToImGui() override { + if (this->data_ptr) { + if (this->is_const) ImGui::BeginDisabled(); + const bool rtn = ImGui::InputScalar(this->name.c_str(), imgui_type::t, this->data_ptr, &this->step, &this->step_fast, imgui_type::fmt, ImGuiInputTextFlags_CallbackCompletion); + if (this->is_const) ImGui::EndDisabled(); + return rtn; + } + ImGui::Text("Loading...%s", this->name.c_str()); + return false; + } + [[nodiscard]] std::unique_ptr clone() const override { return std::unique_ptr(new EnvPropertyInput(this->name, this->index, this->step, this->step_fast)); } +}; +/** + * ImGui checkbox for integer type environment properties + */ +template +struct EnvPropertyToggle : EnvPropertyElement { + /** + * Constructor + * @param _name Name of the environment property + * @param _index Index of the environment property element (0 if the property is not an array property) + */ + explicit EnvPropertyToggle(const std::string& _name, unsigned int _index) + : EnvPropertyElement(_name, _index, _index * sizeof(T)) + , data(false) { } + bool data; + bool addToImGui() override { + if (this->data_ptr) { + if (is_const) ImGui::BeginDisabled(); + // How to sync data + if (ImGui::Checkbox(this->name.c_str(), &data)) { + *static_cast(data_ptr) = static_cast(data ? 1 : 0); + return true; + } + // Really not sure that this sync will work as desired if the model updates the value independently + data = *static_cast(data_ptr); + if (is_const) ImGui::EndDisabled(); + return false; + } + ImGui::Text("Loading...%s", this->name.c_str()); + return false; + } + [[nodiscard]] std::unique_ptr clone() const override { return std::unique_ptr(new EnvPropertyToggle(this->name, this->index)); } +}; +} // namespace visualiser +} // namespace flamegpu + +#endif // INCLUDE_FLAMEGPU_VISUALISER_CONFIG_IMGUIWIDGETS_H_ diff --git a/include/flamegpu/visualiser/config/ModelConfig.h b/include/flamegpu/visualiser/config/ModelConfig.h index 351c1e3..a91bdcf 100644 --- a/include/flamegpu/visualiser/config/ModelConfig.h +++ b/include/flamegpu/visualiser/config/ModelConfig.h @@ -3,9 +3,11 @@ #include #include +#include #include #include "LineConfig.h" +#include "PanelConfig.h" namespace flamegpu { namespace visualiser { @@ -123,6 +125,10 @@ struct ModelConfig { * Store of user defined line renderings */ std::list> lines; + /** + * Store of user defined UI panels + */ + std::map> panels; /** * Notify visualisation that it's running under python * This mostly just allows the logos to be swapped diff --git a/include/flamegpu/visualiser/config/PanelConfig.h b/include/flamegpu/visualiser/config/PanelConfig.h new file mode 100644 index 0000000..abf704c --- /dev/null +++ b/include/flamegpu/visualiser/config/PanelConfig.h @@ -0,0 +1,47 @@ +#ifndef INCLUDE_FLAMEGPU_VISUALISER_CONFIG_PANELCONFIG_H_ +#define INCLUDE_FLAMEGPU_VISUALISER_CONFIG_PANELCONFIG_H_ + +#include +#include +#include + +#include "ImGuiWidgets.h" + +namespace flamegpu { +namespace visualiser { + +/** + * Represents the user-specified elements to be shown within an ImGui panel as part of the visualisation's UI + */ +struct PanelConfig { + /** + * Constructor, only a name is specified, elements can be added later + * @_title Name of the panel to be displayed in the title bar + */ + explicit PanelConfig(const std::string &_title) + : title(_title) { } + /** + * Copy constructor + * @other Other PanelConfig to be copied + */ + explicit PanelConfig(const PanelConfig& other) + : title(other.title) { + for (const auto &e : other.ui_elements) { + this->ui_elements.push_back(e->clone()); + } + } + /** + * Name of the panel + * This will normally be shown in the title_bar at the top of the panel + */ + std::string title; + /** + * Elements will be applied in order + */ + std::list> ui_elements; +}; + +} // namespace visualiser +} // namespace flamegpu + +#endif // INCLUDE_FLAMEGPU_VISUALISER_CONFIG_PANELCONFIG_H_ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee42dd3..9e3da5c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,8 +53,10 @@ SET(VISUALISER_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/../include/flamegpu/visualiser/config/AgentStateConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/../include/flamegpu/visualiser/config/ModelConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/../include/flamegpu/visualiser/config/LineConfig.h + ${CMAKE_CURRENT_SOURCE_DIR}/../include/flamegpu/visualiser/config/PanelConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/../include/flamegpu/visualiser/config/Stock.h ${CMAKE_CURRENT_SOURCE_DIR}/../include/flamegpu/visualiser/config/TexBufferConfig.h + ${CMAKE_CURRENT_SOURCE_DIR}/../include/flamegpu/visualiser/config/ImGuiWidgets.h ) # Prepare list of source files SET(VISUALISER_SRC @@ -107,6 +109,7 @@ SET(VISUALISER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/texture/Texture2D_Multisample.h ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/texture/TextureBuffer.h ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/texture/TextureCubeMap.h + ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/ui/ImGuiPanel.h ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/ui/Overlay.h ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/ui/SplashScreen.h ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/ui/Sprite2D.h @@ -140,6 +143,7 @@ SET(VISUALISER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/texture/Texture2D_Multisample.cpp ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/texture/TextureBuffer.cu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/texture/TextureCubeMap.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/ui/ImGuiPanel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/ui/Overlay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/ui/SplashScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/flamegpu/visualiser/ui/Sprite2D.cpp @@ -200,6 +204,8 @@ include(${CMAKE_CURRENT_LIST_DIR}/../cmake/dependencies/fontconfig.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../cmake/dependencies/freetype.cmake) # DevIL include(${CMAKE_CURRENT_LIST_DIR}/../cmake/dependencies/devil.cmake) +# imgui +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/dependencies/imgui.cmake) # GCC requires -lpthreads for std::thread set(CMAKE_THREAD_PREFER_PTHREAD TRUE) @@ -242,6 +248,7 @@ endif() target_include_directories("${PROJECT_NAME}" SYSTEM PRIVATE ${glm_INCLUDE_DIRS}) target_include_directories("${PROJECT_NAME}" SYSTEM PRIVATE ${SDL2_INCLUDE_DIRS}) target_include_directories("${PROJECT_NAME}" SYSTEM PRIVATE ${GLEW_INCLUDE_DIRS}) +target_include_directories("${PROJECT_NAME}" SYSTEM PRIVATE ${IMGUI_INCLUDE_DIRS}) if (FREETYPE_FOUND) # Only use this if we aren't building it ourselves target_include_directories("${PROJECT_NAME}" SYSTEM PRIVATE ${FREETYPE_INCLUDE_DIRS}) endif () @@ -281,6 +288,7 @@ else() endif() target_link_libraries("${PROJECT_NAME}" freetype) target_link_libraries("${PROJECT_NAME}" "${IL_LIBRARIES}") +target_link_libraries("${PROJECT_NAME}" ImGui::ImGui) target_link_libraries("${PROJECT_NAME}" OpenGL::GL) target_link_libraries("${PROJECT_NAME}" Threads::Threads) if(TARGET Fontconfig::Fontconfig) @@ -362,4 +370,7 @@ if (CMAKE_USE_FOLDERS) if (TARGET resources) set_property(TARGET "resources" PROPERTY FOLDER "FLAMEGPU/Dependencies") endif() + if (TARGET imgui) + set_property(TARGET "imgui" PROPERTY FOLDER "FLAMEGPU/Dependencies") + endif() endif() diff --git a/src/flamegpu/visualiser/FLAMEGPU_Visualisation.cpp b/src/flamegpu/visualiser/FLAMEGPU_Visualisation.cpp index 530d979..877c74b 100644 --- a/src/flamegpu/visualiser/FLAMEGPU_Visualisation.cpp +++ b/src/flamegpu/visualiser/FLAMEGPU_Visualisation.cpp @@ -37,9 +37,15 @@ void FLAMEGPU_Visualisation::updateAgentStateBuffer(const char *agent_name, cons const std::map& core_tex_buffers, const std::multimap& tex_buffers) { vis->updateAgentStateBuffer(agent_name, state_name, buffLen, core_tex_buffers, tex_buffers); } +void FLAMEGPU_Visualisation::registerEnvironmentProperty(const std::string& property_name, void* ptr, const std::type_index type, const unsigned int elements, const bool is_const) { + vis->registerEnvironmentProperty(property_name, ptr, type, elements, is_const); +} void FLAMEGPU_Visualisation::setStepCount(const unsigned int stepCount) { vis->setStepCount(stepCount); } +void FLAMEGPU_Visualisation::setRandomSeed(const uint64_t randomSeed) { + vis->setRandomSeed(randomSeed); +} void FLAMEGPU_Visualisation::start() { vis->start(); } diff --git a/src/flamegpu/visualiser/Visualiser.cpp b/src/flamegpu/visualiser/Visualiser.cpp index a27f0d9..3fdaeef 100644 --- a/src/flamegpu/visualiser/Visualiser.cpp +++ b/src/flamegpu/visualiser/Visualiser.cpp @@ -1,5 +1,9 @@ #include "flamegpu/visualiser/Visualiser.h" +#include +#include +#include + #include #include #include @@ -116,7 +120,6 @@ Visualiser::Visualiser(const ModelConfig& modelcfg) , fpsDisplay(nullptr) , stepDisplay(nullptr) , spsDisplay(nullptr) - , debugMenu(nullptr) , modelConfig(modelcfg) , lighting(nullptr) , gamepad(nullptr) @@ -143,11 +146,9 @@ Visualiser::Visualiser(const ModelConfig& modelcfg) stepDisplay->setUseAA(false); hud->add(stepDisplay, HUD::AnchorV::South, HUD::AnchorH::East, glm::ivec2(0, 0), INT_MAX - 1); } - { // Debug menu - debugMenu = std::make_shared("", 16, glm::vec3(1.0f), fonts::findFont({ "Consolas", "Arial" }, fonts::GenericFontFamily::SANS).c_str()); - debugMenu->setBackgroundColor(glm::vec4(*reinterpret_cast(&modelcfg.clearColor[0]), 0.65f)); - debugMenu->setVisible(false); - hud->add(debugMenu, HUD::AnchorV::North, HUD::AnchorH::West, glm::ivec2(0, 0), INT_MAX); + { // ImGui Panels + imguiPanel = std::make_shared(modelcfg.panels, *this); + hud->add(imguiPanel, HUD::AnchorV::North, HUD::AnchorH::West, glm::ivec2(0, 0), INT_MAX-2); } lines = std::make_shared(); lines->setViewMatPtr(camera->getViewMatPtr()); @@ -363,6 +364,10 @@ void Visualiser::render() { this->lighting->getPointLight(0).Position(this->camera->getEye()); // handle each event on the queue while (SDL_PollEvent(&e) != 0) { + if (!SDL_GetRelativeMouseMode()) { + // Assume ImGUI is top level, allow it first chance on IO if mouse isn't locked for movement + ImGui_ImplSDL2_ProcessEvent(&e); + } switch (e.type) { case SDL_QUIT: continueRender = false; @@ -372,7 +377,7 @@ void Visualiser::render() { resizeWindow(); break; case SDL_KEYDOWN: - { + if (!ImGui::GetIO().WantCaptureKeyboard) { int x = 0; int y = 0; SDL_GetMouseState(&x, &y); @@ -385,7 +390,8 @@ void Visualiser::render() { this->handleMouseMove(e.motion.xrel, e.motion.yrel); break; case SDL_MOUSEBUTTONDOWN: - this->toggleMouseMode(); + if (!ImGui::GetIO().WantCaptureMouse) + this->toggleMouseMode(); break; case SDL_CONTROLLERDEVICEADDED: this->addController(e.cdevice); @@ -408,7 +414,6 @@ void Visualiser::render() { this->queryControllerAxis(frameTime); // Update lighting lighting->update(); - updateDebugMenu(); // Render render_buffer->use(); for (auto &sm : staticModels) @@ -608,7 +613,10 @@ void Visualiser::updateAgentStateBuffer(const std::string &agent_name, const std } } } - +void Visualiser::registerEnvironmentProperty(const std::string& property_name, void* ptr, std::type_index /*type*/, unsigned int elements, bool is_const) { + // Construct an (ordered) map of the registered properties + imguiPanel->registerProperty(property_name, ptr, is_const, elements > 1); +} // Items taken from sdl_exp bool Visualiser::init() { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) { @@ -673,6 +681,21 @@ bool Visualiser::init() { screenshot_buffer = std::make_shared(FBAFactory::ManagedColorRenderBufferRGBA(), FBAFactory::ManagedDepthRenderBuffer(), FBAFactory::Disabled(), 1, 1.0f, true, *reinterpret_cast(modelConfig.clearColor)); setMSAA(this->msaaState); + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); (void)io; + // io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + // io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + io.IniFilename = nullptr; // Don't automatically save ImGui panel position/state to file + // Setup Dear ImGui style + ImGui::StyleColorsDark(); + // ImGui::StyleColorsLight(); + + // Setup Platform/Renderer backends + ImGui_ImplSDL2_InitForOpenGL(window, this->context); + ImGui_ImplOpenGL3_Init(); + // Setup the projection matrix this->resizeWindow(); GL_CHECK(); @@ -693,6 +716,9 @@ void Visualiser::resizeWindow() { glm::ivec2 tDims; SDL_GL_GetDrawableSize(this->window, &tDims.x, &tDims.y); this->windowDims = tDims; + // Setup display size (every frame to accommodate for window resizing) + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(tDims.x), static_cast(tDims.y)); } // Get the view frustum using GLM. Alternatively glm::perspective could be used. this->projMat = glm::perspectiveFov( @@ -712,7 +738,7 @@ void Visualiser::deallocateGLObjects() { fpsDisplay.reset(); stepDisplay.reset(); spsDisplay.reset(); - debugMenu.reset(); + imguiPanel.reset(); this->hud->clear(); // Don't clear the map, as update buffer methods might still be called for (auto &as : agentStates) { @@ -721,6 +747,11 @@ void Visualiser::deallocateGLObjects() { this->lines.reset(); render_buffer.reset(); screenshot_buffer.reset(); + + // Release imgui state + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplSDL2_Shutdown(); + ImGui::DestroyContext(); } void Visualiser::close() { @@ -809,8 +840,12 @@ void Visualiser::handleKeypress(SDL_Keycode keycode, int /*x*/, int /*y*/) { this->hud->reload(); break; case SDLK_F1: - if (this->debugMenu) - this->debugMenu->setVisible(!this->debugMenu->getVisible()); + if (this->imguiPanel) + this->imguiPanel->toggleDebugMenuVisible(); + break; + case SDLK_F2: + if (this->imguiPanel) + this->imguiPanel->toggleUIVisible(); break; case SDLK_p: if (this->pause_guard) { @@ -894,6 +929,9 @@ void Visualiser::setStepCount(const unsigned int &_stepCount) { this->lastStepCount = _stepCount; } } +void Visualiser::setRandomSeed(const uint64_t _randomSeed) { + randomSeed = _randomSeed; +} // Overrides unsigned Visualiser::getWindowWidth() const { return windowDims.x; @@ -1100,44 +1138,6 @@ void Visualiser::setWindowIcon() { if (surface) SDL_SetWindowIcon(window, surface.get()); } -void Visualiser::updateDebugMenu() { - if (debugMenu && debugMenu->getVisible()) { - std::stringstream ss; - ss << std::setprecision(3); - ss << std::fixed; // To stop camera floats from changing box width - ss << "===Debug Menu===" << "\n"; - const glm::vec3 eye = camera->getEye(); - const glm::vec3 look = camera->getLook(); - const glm::vec3 up = camera->getUp(); - ss << "Camera Location: (" << eye.x << ", " << eye.y << ", " << eye.z << ")" "\n"; - ss << "Camera Direction: (" << look.x << ", " << look.y << ", " << look.z << ")" "\n"; - ss << "Camera Up: (" << up.x << ", " << up.y << ", " << up.z << ")" "\n"; - ss << "MSAA: " << (this->msaaState ? "On" : "Off") << "\n"; - switch (this->fpsStatus) { - case 2: - ss << "Display FPS: Show All" << "\n"; - break; - case 1: - ss << "Display FPS: Show Step Count" << "\n"; - break; - default: - ss << "Display FPS: Off" << "\n"; - } - ss << "Display Lines: " << (this->renderLines ? "On" : "Off") << "\n"; - ss << "Agent Populations:" << "\n"; - for (const auto &as : agentStates) { - if (debugMenu_showStateNames) { - ss << " " << as.first.first << "(" << as.first.second << "): " << as.second.requiredSize << "\n"; - } else { - ss << " " << as.first.first << ": " << as.second.requiredSize << "\n"; - } - } - - // Last item (don't end \n) - ss << "Pause Simulation: " << (this->pause_guard ? "On" : "Off"); - debugMenu->setString(ss.str().c_str()); - } -} } // namespace visualiser } // namespace flamegpu diff --git a/src/flamegpu/visualiser/Visualiser.h b/src/flamegpu/visualiser/Visualiser.h index 1bf29ee..924020f 100644 --- a/src/flamegpu/visualiser/Visualiser.h +++ b/src/flamegpu/visualiser/Visualiser.h @@ -2,6 +2,7 @@ #define SRC_FLAMEGPU_VISUALISER_VISUALISER_H_ #include +#undef main // SDL breaks the regular main entry point, this fixes #include #include @@ -12,7 +13,7 @@ #include #include #include -#undef main // SDL breaks the regular main entry point, this fixes +#include #define GLM_FORCE_NO_CTOR_INIT #include @@ -20,6 +21,7 @@ #include "flamegpu/visualiser/Entity.h" #include "flamegpu/visualiser/HUD.h" #include "flamegpu/visualiser/ui/Text.h" +#include "flamegpu/visualiser/ui/ImGuiPanel.h" #include "flamegpu/visualiser/camera/NoClipCamera.h" #include "flamegpu/visualiser/config/AgentStateConfig.h" #include "flamegpu/visualiser/config/TexBufferConfig.h" @@ -40,6 +42,7 @@ class LightsBuffer; * This is the main class of the visualisation, hosting the window and render loop */ class Visualiser : public ViewportExt { + friend class ImGuiPanel; typedef std::pair NamePair; struct NamePairHash { size_t operator()(const NamePair &k) const { return std::hash()(k.first) ^ (std::hash()(k.second) << 1); } @@ -117,6 +120,10 @@ class Visualiser : public ViewportExt { */ void updateAgentStateBuffer(const std::string &agent_name, const std::string &state_name, const unsigned int buffLen, const std::map& _core_tex_buffers, const std::multimap& _tex_buffers); + /** + * Provide the env_cache ptr for the specified environment property, for visualisation + */ + void registerEnvironmentProperty(const std::string& property_name, void* ptr, std::type_index type, unsigned int elements, bool is_const); private: void run(); @@ -232,9 +239,12 @@ class Visualiser : public ViewportExt { * @param stepCount The step value to be displayed */ void setStepCount(const unsigned int &stepCount); + /** + * Sets the value to be rendered for random seed in the debug menu + */ + void setRandomSeed(uint64_t randomSeed); private: - void updateDebugMenu(); SDL_Window *window; SDL_Rect windowedBounds; SDL_GLContext context; @@ -297,13 +307,17 @@ class Visualiser : public ViewportExt { */ unsigned int previousStepTime = 0, currentStepTime, stepCount = 0, lastStepCount = 0; /** - * Displays internal status info to screen + * Handles the ImGui supported rendering of the debug menu and user-specified UI panels */ - std::shared_ptr debugMenu; + std::shared_ptr imguiPanel; /** * If set true, we don't display agent states in debug menu because agents only have one state. */ bool debugMenu_showStateNames = false; + /** + * Random seed, displayed in debugMenu + */ + uint64_t randomSeed = 0; /** * Steps equivalent of FPS. * Calculated in the wrong thread, so we update it whenever FPS updates diff --git a/src/flamegpu/visualiser/config/ModelConfig.cpp b/src/flamegpu/visualiser/config/ModelConfig.cpp index 1e182fc..c483134 100644 --- a/src/flamegpu/visualiser/config/ModelConfig.cpp +++ b/src/flamegpu/visualiser/config/ModelConfig.cpp @@ -46,6 +46,7 @@ ModelConfig &ModelConfig::operator=(const ModelConfig &other) { isPython = other.isPython; // staticModels // lines + // panels return *this; } diff --git a/src/flamegpu/visualiser/ui/ImGuiPanel.cpp b/src/flamegpu/visualiser/ui/ImGuiPanel.cpp new file mode 100644 index 0000000..b04416c --- /dev/null +++ b/src/flamegpu/visualiser/ui/ImGuiPanel.cpp @@ -0,0 +1,149 @@ +#include +#include +#include + +#include +#include +#include +#include // for PRIu64 (cross-platform uint64_t format specifier) + +#include "flamegpu/visualiser/Visualiser.h" +#include "flamegpu/visualiser/ui/ImGuiPanel.h" +#include "flamegpu/visualiser/shader/Shaders.h" + +namespace flamegpu { +namespace visualiser { + +ImGuiPanel::ImGuiPanel(const std::map>& cfgs, const Visualiser& _vis) + : Overlay(std::make_shared(Stock::Shaders::SPRITE2D)) + , vis(_vis) { + for (const auto &cfg : cfgs) + configs.push_back(*cfg.second); + // Start the Dear ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDL2_NewFrame(); // This can't be called in the render thread, as it tries to grab the window dimensions via SDL +} +void ImGuiPanel::reload() { + first_render = 0; +} +void ImGuiPanel::drawPanel() { + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ImVec2 prev_window_size(0, 0), prev_window_pos(main_viewport->WorkPos.x, main_viewport->WorkPos.y); + for (const auto &config : configs) { + // Translucent background + ImGui::SetNextWindowBgAlpha(150.0f / 255.0f); + // Size panel to fit it's content + ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_Always); + if (first_render < 2) { + // Manual set once condition, as I can't get ImGuiCond to play nicely here for multiple panels + // Position multiple panels so that they don't overlap + ImGui::SetNextWindowPos(ImVec2(prev_window_pos.x + prev_window_size.x + 5, main_viewport->WorkPos.y + 5), ImGuiCond_Always); + } + + // Actually start creating the panel + if (!ImGui::Begin(config.title.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) { + prev_window_size = ImGui::GetWindowSize(); + prev_window_pos = ImGui::GetWindowPos(); + // Early out if the window is collapsed, as an optimization. + ImGui::End(); + return; + } + + // Give items a fixed width (I think this i supposed to be text, but possibly conflicts with auto window resize) + ImGui::PushItemWidth(ImGui::GetFontSize() * 15); + + // Add the user defined items in order + bool open = true; + for (auto& e : config.ui_elements) { + if (auto a = dynamic_cast(e.get())) { + open = a->addToImGui(); + } else if (open) { + e->addToImGui(); + } + } + + // Finalise the panel + ImGui::PopItemWidth(); + prev_window_size = ImGui::GetWindowSize(); + prev_window_pos = ImGui::GetWindowPos(); + ImGui::End(); + } + first_render += first_render < 2 ? 1: 0; // Auto size is not calculated until it's been drawn, so wait a frame to fix it +} +void ImGuiPanel::drawDebugPanel() const { + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x + 5, main_viewport->WorkPos.y + 5), ImGuiCond_Once); + // Translucent background + ImGui::SetNextWindowBgAlpha(150.0f / 255.0f); + // Size panel to fit it's content (might fix this width so it doesn't jump about with camera mvmt) + ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_Always); + + // Actually start creating the panel + ImGui::Begin("Debug Menu", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + // Give items a fixed width (I think this i supposed to be text, but possibly conflicts with auto window resize) + ImGui::PushItemWidth(ImGui::GetFontSize() * 15); + + ImGui::Text("Initial Random Seed: %" PRIu64, vis.randomSeed); + const glm::vec3 eye = vis.camera->getEye(); + const glm::vec3 look = vis.camera->getLook(); + const glm::vec3 up = vis.camera->getUp(); + ImGui::Text("Camera Location : (% .3f, % .3f, % .3f)", eye.x, eye.y, eye.z); + ImGui::Text("Camera Direction : (% .3f, % .3f, % .3f)", look.x, look.y, look.z); + ImGui::Text("Camera Up : (% .3f, % .3f, % .3f)", up.x, up.y, up.z); + ImGui::Text("MSAA: %s", (vis.msaaState ? "On" : "Off")); + switch (vis.fpsStatus) { + case 2: + ImGui::Text("Display FPS: Show All"); + break; + case 1: + ImGui::Text("Display FPS: Show Step Count"); + break; + default: + ImGui::Text("Display FPS: Off"); + } + ImGui::Text("Pause Simulation: %s", (vis.pause_guard ? "On" : "Off")); + ImGui::Separator(); + ImGui::Text("Agent Populations:"); + for (const auto& as : vis.agentStates) { + if (vis.debugMenu_showStateNames) { + ImGui::BulletText("%s (%s): %u", as.first.first.c_str(), as.first.second.c_str(), as.second.requiredSize); + } else { + ImGui::BulletText("%s: %u", as.first.first.c_str(), as.second.requiredSize); + } + } + + // Finalise the panel + ImGui::PopItemWidth(); + ImGui::End(); +} +void ImGuiPanel::render(const glm::mat4*, const glm::mat4*, GLuint fbo) { + ImGui::NewFrame(); + // bool is_true = true; + // ImGui::ShowDemoWindow(&is_true); + if (ui_visible) drawPanel(); + if (debug_menu_visible) drawDebugPanel(); + // This renders the interface to a host texture + ImGui::Render(); + // This presumably copies the host texture to a GPU texture and renders it + // imgui wants to be it's own HUD, so we kind of need to give it a full screen overlay to draw into + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + // SDL_GL_SwapWindow(window); +} +void ImGuiPanel::registerProperty(const std::string& name, void* ptr, bool is_const, bool is_array) { + for (const auto& config : configs) { + for (auto& e : config.ui_elements) { + if (auto a = dynamic_cast(e.get())) { + if (a->getName() == name) { + if (is_const) a->setConst(true); + if (is_array) a->setArray(); + a->setPtr(ptr); + } + } + } + } + first_render = 0; // As these can change window width +} + +} // namespace visualiser +} // namespace flamegpu diff --git a/src/flamegpu/visualiser/ui/ImGuiPanel.h b/src/flamegpu/visualiser/ui/ImGuiPanel.h new file mode 100644 index 0000000..4b03aea --- /dev/null +++ b/src/flamegpu/visualiser/ui/ImGuiPanel.h @@ -0,0 +1,94 @@ +#ifndef SRC_FLAMEGPU_VISUALISER_UI_IMGUIPANEL_H_ +#define SRC_FLAMEGPU_VISUALISER_UI_IMGUIPANEL_H_ + +#include +#include +#include +#include + +#include "flamegpu/visualiser/config/PanelConfig.h" +#include "flamegpu/visualiser/ui/Overlay.h" + +namespace flamegpu { +namespace visualiser { + +class Visualiser; + +/** + * Class for rendering all ImGui user interfaces to the HUD + * + * ImGui draws to the whole screen space, so we handle them all at the same time for ease of input handling + */ +class ImGuiPanel : public Overlay { + public: + /** + * Default constructor, currently creates a debugging panel thing + * @param cfgs Map of configs specifying how they should be constructed + * @param _vis Handle to the visualiser so that data can be probed by the debug panel + */ + explicit ImGuiPanel(const std::map> &cfgs, const Visualiser& _vis); + /** + * Only resets first_render flag, as ImGui runs in immediate mode + */ + void reload() override; + /** + * Renders the desired panels using ImGui + * Arguments are ignored, ImGui detects and sets these internally + */ + void render(const glm::mat4*, const glm::mat4*, GLuint) override; + /** + * This must be called for every environment property stored within configs + * It provides the host cache pointer and constant status of each property + * @param name Name of the environment property + * @param ptr Pointer to the environment properties data within the host cache + * @param is_const True if the environment property should not be changed + * @param is_array True if the environment property's name should reflect that it's an array within the UI + */ + void registerProperty(const std::string &name, void *ptr, bool is_const, bool is_array); + /** + * Toggle visibility of the debug menu + * @note If debug menu is made visible, the user specified panels are hidden + */ + void toggleDebugMenuVisible() { debug_menu_visible = !debug_menu_visible; if (debug_menu_visible) { ui_visible = false; }} + /** + * Toggle visibility of the user specified panels + * @note If the user specified panels are made visible, the debug menu is hidden + */ + void toggleUIVisible() { ui_visible = !ui_visible; if (ui_visible) { debug_menu_visible = false; }} + + private: + /** + * If true render() triggers drawDebugPanel() + */ + bool debug_menu_visible = false; + /** + * If true render() triggers drawPanel() + */ + bool ui_visible = true; + /** + * Calls ImGui to draw the user specified panels from configs + */ + void drawPanel(); + /** + * Calls ImGui to draw the debug panel + */ + void drawDebugPanel() const; + /** + * Causes panels to be automatically positioned + * Takes 2 frames to process, due to auto sizing + */ + unsigned char first_render; + /** + * A copy of the panel configurations passed to the constructor + * These are maintaned as they must be passed to ImGui prior to each frame being rendered + */ + std::list configs; + /** + * Used by drawDebugPanel() to access visualisation properties + */ + const Visualiser &vis; +}; + +} // namespace visualiser +} // namespace flamegpu +#endif // SRC_FLAMEGPU_VISUALISER_UI_IMGUIPANEL_H_ diff --git a/src/flamegpu/visualiser/ui/Overlay.h b/src/flamegpu/visualiser/ui/Overlay.h index 56d8068..17d923f 100644 --- a/src/flamegpu/visualiser/ui/Overlay.h +++ b/src/flamegpu/visualiser/ui/Overlay.h @@ -43,7 +43,7 @@ class Overlay { * @param proj The projection matrix * @param fbo The buffer object holding the face indices */ - void render(const glm::mat4 *mv, const glm::mat4 *proj, GLuint fbo); + virtual void render(const glm::mat4 *mv, const glm::mat4 *proj, GLuint fbo); unsigned int getWidth() const { return dimensions.x; } unsigned int getHeight() const { return dimensions.y;} std::shared_ptr getShaders() const { return shaders; } diff --git a/src/flamegpu/visualiser/ui/Sprite2D.h b/src/flamegpu/visualiser/ui/Sprite2D.h index bc8acf8..10a97ad 100644 --- a/src/flamegpu/visualiser/ui/Sprite2D.h +++ b/src/flamegpu/visualiser/ui/Sprite2D.h @@ -3,7 +3,7 @@ #include #include "flamegpu/visualiser/texture/Texture2D.h" -#include "Overlay.h" +#include "flamegpu/visualiser/ui/Overlay.h" namespace flamegpu { namespace visualiser {