From 9ab1598738fbf871f9fb8903d4e595ad1c88f11d Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Mon, 14 Oct 2024 20:43:03 +0800 Subject: [PATCH 01/20] cmake: updated app build condition --- CMakeLists.txt | 116 +++++++++++++++++++++++------------------ src/app/CMakeLists.txt | 32 ++++++++---- 2 files changed, 85 insertions(+), 63 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b2c3ae6..9ef0b93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,35 +1,47 @@ cmake_minimum_required(VERSION 3.16.0) find_program(CCACHE_PROGRAM ccache) -if(CCACHE_PROGRAM) - message(STATUS "Found ccache") - set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}") -endif() +if (CCACHE_PROGRAM) + message(STATUS "Found ccache") + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}") +endif () project(quickviz VERSION 0.2.0) +message(STATUS "------------------------------------------------") +if (PROJECT_NAME STREQUAL CMAKE_PROJECT_NAME) + set(BUILD_AS_MODULE OFF) + message(STATUS "Build quickviz as a standalone project") +else () + set(BUILD_AS_MODULE ON) + message(STATUS "Build quickviz as a module") +endif () +message(STATUS "------------------------------------------------") + ## Project Options +option(BUILD_QUICKVIZ_APP "Build quickviz app" OFF) option(BUILD_TESTING "Build tests" ON) +option(QUICKVIZ_DEV_MODE "Development mode forces building tests" OFF) option(STATIC_CHECK "Perform static check" OFF) option(IMVIEW_WITH_GLAD "Integrate glad into imview" ON) ## Check if code compiles on x86_64 platform -if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") - set(BUILD_ON_X86_64 TRUE) -else() - set(BUILD_ON_X86_64 FALSE) -endif() +if (CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + set(BUILD_ON_X86_64 TRUE) +else () + set(BUILD_ON_X86_64 FALSE) +endif () ## Generate symbols for IDE indexer set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -if(STATIC_CHECK) - find_program(CPPCHECK cppcheck) - if(CPPCHECK) - message(STATUS "Found cppcheck") - set(CMAKE_CXX_CPPCHECK cppcheck;--std=c++11;--enable=all) - endif() -endif() +if (STATIC_CHECK) + find_program(CPPCHECK cppcheck) + if (CPPCHECK) + message(STATUS "Found cppcheck") + set(CMAKE_CXX_CPPCHECK cppcheck;--std=c++11;--enable=all) + endif () +endif () ## Additional cmake module path # include(${CMAKE_BINARY_DIR}/conan_paths.cmake) @@ -44,14 +56,14 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) ## Chosse build type set(default_build_type "Release") -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) +if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to '${default_build_type}' as none was specified.") set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING "Choose the type of build." FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Release" "MinSizeRel" "RelWithDebInfo") -endif() + "Debug" "Release" "MinSizeRel" "RelWithDebInfo") +endif () ## Use GNUInstallDirs to install libraries into correct locations on all platforms. include(GNUInstallDirs) @@ -59,24 +71,30 @@ include(GNUInstallDirs) ## Put all binary files into /bin and libraries into /lib set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) +if (QUICKVIZ_DEV_MODE) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) +else () + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) +endif () + +if(QUICKVIZ_DEV_MODE) + message(STATUS "quickviz development mode enabled") +endif() # Build tests -if(PROJECT_NAME STREQUAL CMAKE_PROJECT_NAME AND BUILD_TESTING) - enable_testing() - include(GoogleTest) - # add_subdirectory(tests) - message(STATUS "Tests will be built") -else() - message(STATUS "Tests will not be built") -endif() +if (((NOT BUILD_AS_MODULE) AND BUILD_TESTING) OR QUICKVIZ_DEV_MODE) + set(BUILD_TESTING ON) + enable_testing() + include(GoogleTest) + message(STATUS "quickviz tests will be built") +else () + set(BUILD_TESTING OFF) + message(STATUS "quickviz test will not be built") +endif () ## Add source directory add_subdirectory(src) -#add_library(quickviz INTERFACE) -#target_link_libraries(quickviz INTERFACE imcore imview cvdraw) - ## Installation setup message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX} with 'make install'") @@ -84,27 +102,21 @@ message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX} with 'make set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries") set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables") set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files") -if(WIN32 AND NOT CYGWIN) - set(DEF_INSTALL_CMAKEDIR CMake) -else() - set(DEF_INSTALL_CMAKEDIR share/cmake/${PROJECT_NAME}) -endif() +if (WIN32 AND NOT CYGWIN) + set(DEF_INSTALL_CMAKEDIR CMake) +else () + set(DEF_INSTALL_CMAKEDIR share/cmake/${PROJECT_NAME}) +endif () set(INSTALL_CMAKEDIR ${DEF_INSTALL_CMAKEDIR} CACHE PATH "Installation directory for CMake files") # print installation path to user -foreach(p LIB BIN INCLUDE CMAKE) - file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path) - message(STATUS "Installation path for ${p}: ${_path}") - unset(_path) -endforeach() - -# targets to install -install(TARGETS quickviz - EXPORT quickvizTargets - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib - RUNTIME DESTINATION bin - INCLUDES DESTINATION include) +foreach (p LIB BIN INCLUDE CMAKE) + file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path) + message(STATUS "Installation path for ${p}: ${_path}") + unset(_path) +endforeach () + +# targets to install defined in each module # export target configuration include(CMakePackageConfigHelpers) @@ -120,8 +132,8 @@ install(EXPORT quickvizTargets configure_file(cmake/quickvizConfig.cmake.in quickvizConfig.cmake @ONLY) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/quickvizConfig.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/quickvizConfigVersion.cmake" - DESTINATION lib/cmake/quickviz) + "${CMAKE_CURRENT_BINARY_DIR}/quickvizConfigVersion.cmake" + DESTINATION lib/cmake/quickviz) # Packaging support set(CPACK_PACKAGE_NAME "quickviz") @@ -136,7 +148,7 @@ set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") set(CPACK_GENERATOR "DEB") set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) -set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Ruixiang Du") +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Ruixiang Du") set(CPACK_DEBIAN_PACKAGE_DEPENDS "libgl1-mesa-dev, libglfw3-dev, libcairo2-dev") set(CPACK_SOURCE_IGNORE_FILES .git dist .*build.* \\\\.DS_Store) include(CPack) \ No newline at end of file diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 3480a0b..cbe2ab5 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,11 +1,21 @@ -add_executable(quickviz main.cpp - # components - quickviz_application.cpp - # panels - panels/main_docking_panel.cpp - panels/config_panel.cpp - panels/scene_panel.cpp - panels/config_panel.cpp - panels/console_panel.cpp) -target_link_libraries(quickviz PRIVATE imview) -target_include_directories(quickviz PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +if (BUILD_QUICKVIZ_APP) + message(STATUS "Build quickviz application") + add_executable(quickviz main.cpp + # components + quickviz_application.cpp + # panels + panels/main_docking_panel.cpp + panels/config_panel.cpp + panels/scene_panel.cpp + panels/config_panel.cpp + panels/console_panel.cpp) + target_link_libraries(quickviz PRIVATE imview) + target_include_directories(quickviz PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + + install(TARGETS quickviz + EXPORT quickvizTargets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin + INCLUDES DESTINATION include) +endif () \ No newline at end of file From f4f7fd2be2b8a22d4465f10bcb0c24750d6a5867 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Tue, 15 Oct 2024 23:42:15 +0800 Subject: [PATCH 02/20] buffer: updated ring buffer comments --- .../include/imview/buffer/ring_buffer.hpp | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/imview/include/imview/buffer/ring_buffer.hpp b/src/imview/include/imview/buffer/ring_buffer.hpp index 8cc43d4..9c5f48e 100644 --- a/src/imview/include/imview/buffer/ring_buffer.hpp +++ b/src/imview/include/imview/buffer/ring_buffer.hpp @@ -2,6 +2,7 @@ * ring_buffer.hpp * * Created on: Dec 08, 2019 22:22 + * Updated on: Oct 2024 * Description: * * Requirements: @@ -20,15 +21,20 @@ * ^ ^ * R W * - * - Buffer gets full when last element X is inserted - * [X][D][D][D]...[D] - * ^ - * W/R (W>R) + * - Buffer gets full when last element X is inserted and W moves to N. + * The last position N will not be used for data storage because otherwise + * W will move to the same position with R after the write and we cannot + * differentiate between empty and full. + * [D][D][D][D]...[X][N] + * ^ ^ + * R W * - * - Buffer data overwritten by new element Y after getting full - * [X][Y][D][D]...[D] - * ^ - * W/R (W>R) + * - Buffer data pointed by R is overwritten (? not readable) when a new element + * Y is inserted after the buffer is full. In other words, the R index is pushed + * forward by one position after the buffer is full. + * [?][D][D][D]...[X][Y] + * ^ ^ + * W R * * To differentiate between empty and full, one slot is always left empty before * the read index. This is why the buffer size is N-1. From 9fa77fa6b811fa608e51f2c7c7c1e562386dd3b7 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Tue, 29 Oct 2024 23:48:56 +0800 Subject: [PATCH 03/20] widget: added gl widget but not working yet --- src/app/panels/config_panel.cpp | 2 +- src/app/panels/console_panel.cpp | 2 +- src/app/panels/scene_panel.cpp | 12 ++- src/imview/CMakeLists.txt | 6 +- .../include/imview/widget/cairo_widget.hpp | 4 +- .../include/imview/widget/cv_image_widget.hpp | 4 +- .../{cairo => details}/cairo_context.hpp | 0 .../widget/{cairo => details}/cairo_draw.hpp | 0 .../imview/widget/details/gl_frame_buffer.hpp | 49 ++++++++++ .../include/imview/widget/gl_scene_widget.hpp | 35 +++++++ src/imview/src/widget/cv_image_widget.cpp | 1 + .../{cairo => details}/cairo_context.cpp | 2 +- .../widget/{cairo => details}/cairo_draw.cpp | 2 +- .../src/widget/details/gl_frame_buffer.cpp | 91 +++++++++++++++++++ src/imview/src/widget/gl_scene_widget.cpp | 46 ++++++++++ src/imview/test/CMakeLists.txt | 3 + src/imview/test/feature/CMakeLists.txt | 3 + .../test/feature/test_gl_scene_widget.cpp | 50 ++++++++++ src/imview/test/test_framebuffer.cpp | 60 ++++++++++++ 19 files changed, 360 insertions(+), 12 deletions(-) rename src/imview/include/imview/widget/{cairo => details}/cairo_context.hpp (100%) rename src/imview/include/imview/widget/{cairo => details}/cairo_draw.hpp (100%) create mode 100644 src/imview/include/imview/widget/details/gl_frame_buffer.hpp create mode 100644 src/imview/include/imview/widget/gl_scene_widget.hpp rename src/imview/src/widget/{cairo => details}/cairo_context.cpp (98%) rename src/imview/src/widget/{cairo => details}/cairo_draw.cpp (98%) create mode 100644 src/imview/src/widget/details/gl_frame_buffer.cpp create mode 100644 src/imview/src/widget/gl_scene_widget.cpp create mode 100644 src/imview/test/feature/test_gl_scene_widget.cpp create mode 100644 src/imview/test/test_framebuffer.cpp diff --git a/src/app/panels/config_panel.cpp b/src/app/panels/config_panel.cpp index ab35f8b..115503b 100644 --- a/src/app/panels/config_panel.cpp +++ b/src/app/panels/config_panel.cpp @@ -14,7 +14,7 @@ namespace quickviz { ConfigPanel::ConfigPanel(std::string name) : Panel(name) { this->SetAutoLayout(false); this->SetNoResize(true); - this->SetNoMove(true); +// this->SetNoMove(true); this->SetWindowNoMenuButton(); } diff --git a/src/app/panels/console_panel.cpp b/src/app/panels/console_panel.cpp index 26eaa84..4eaa04b 100644 --- a/src/app/panels/console_panel.cpp +++ b/src/app/panels/console_panel.cpp @@ -14,7 +14,7 @@ namespace quickviz { ConsolePanel::ConsolePanel(std::string name) : Panel(name) { this->SetAutoLayout(false); this->SetNoResize(true); - this->SetNoMove(true); +// this->SetNoMove(true); this->SetWindowNoMenuButton(); } diff --git a/src/app/panels/scene_panel.cpp b/src/app/panels/scene_panel.cpp index af7c71c..9f358ca 100644 --- a/src/app/panels/scene_panel.cpp +++ b/src/app/panels/scene_panel.cpp @@ -8,13 +8,14 @@ #include "panels/scene_panel.hpp" +#include "glad/glad.h" #include "imview/fonts.hpp" namespace quickviz { ScenePanel::ScenePanel(std::string name) : Panel(name) { this->SetAutoLayout(false); this->SetNoResize(true); - this->SetNoMove(true); +// this->SetNoMove(true); this->SetWindowNoMenuButton(); } @@ -27,6 +28,15 @@ void ScenePanel::Draw() { 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); ImGui::PopStyleColor(); ImGui::PopFont(); + + glEnable(GL_SCISSOR_TEST); + glViewport(x_, y_, width_, height_); + glScissor(x_, y_, width_, height_); + + glClearColor(0.5, 0, 0, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + + glDisable(GL_SCISSOR_TEST); } End(); } diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index 293a655..ae9cb2b 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -26,9 +26,11 @@ add_library(imview src/widget/cv_image_widget.cpp src/widget/buffered_cv_image_widget.cpp src/widget/cairo_widget.cpp - src/widget/cairo/cairo_context.cpp - src/widget/cairo/cairo_draw.cpp + src/widget/details/cairo_context.cpp + src/widget/details/cairo_draw.cpp src/widget/rt_line_plot_widget.cpp + src/widget/gl_scene_widget.cpp + src/widget/details/gl_frame_buffer.cpp # data buffer src/buffer/buffer_registry.cpp # event handling diff --git a/src/imview/include/imview/widget/cairo_widget.hpp b/src/imview/include/imview/widget/cairo_widget.hpp index 4a0ec2f..7422f34 100644 --- a/src/imview/include/imview/widget/cairo_widget.hpp +++ b/src/imview/include/imview/widget/cairo_widget.hpp @@ -21,8 +21,8 @@ #include "imgui.h" #include "imview/panel.hpp" -#include "imview/widget/cairo/cairo_context.hpp" -#include "imview/widget/cairo/cairo_draw.hpp" +#include "imview/widget/details/cairo_context.hpp" +#include "imview/widget/details/cairo_draw.hpp" namespace quickviz { class CairoWidget : public Panel { diff --git a/src/imview/include/imview/widget/cv_image_widget.hpp b/src/imview/include/imview/widget/cv_image_widget.hpp index 2997ce4..d1075c2 100644 --- a/src/imview/include/imview/widget/cv_image_widget.hpp +++ b/src/imview/include/imview/widget/cv_image_widget.hpp @@ -9,8 +9,6 @@ #ifndef QUICKVIZ_CV_IMAGE_WIDGET_HPP #define QUICKVIZ_CV_IMAGE_WIDGET_HPP -#include "glad/glad.h" - #include #include @@ -32,7 +30,7 @@ class CvImageWidget : public Panel { private: std::mutex image_mutex_; cv::Mat image_mat_; - GLuint image_texture_; + uint32_t image_texture_; bool keep_aspect_ratio_ = false; }; } // namespace quickviz diff --git a/src/imview/include/imview/widget/cairo/cairo_context.hpp b/src/imview/include/imview/widget/details/cairo_context.hpp similarity index 100% rename from src/imview/include/imview/widget/cairo/cairo_context.hpp rename to src/imview/include/imview/widget/details/cairo_context.hpp diff --git a/src/imview/include/imview/widget/cairo/cairo_draw.hpp b/src/imview/include/imview/widget/details/cairo_draw.hpp similarity index 100% rename from src/imview/include/imview/widget/cairo/cairo_draw.hpp rename to src/imview/include/imview/widget/details/cairo_draw.hpp diff --git a/src/imview/include/imview/widget/details/gl_frame_buffer.hpp b/src/imview/include/imview/widget/details/gl_frame_buffer.hpp new file mode 100644 index 0000000..7728f1a --- /dev/null +++ b/src/imview/include/imview/widget/details/gl_frame_buffer.hpp @@ -0,0 +1,49 @@ +/* + * @file gl_frame_buffer.hpp + * @date 10/29/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef XMOTION_GL_FRAME_BUFFER_HPP +#define XMOTION_GL_FRAME_BUFFER_HPP + +#include + +namespace quickviz { +class GlFrameBuffer { + public: + GlFrameBuffer(uint32_t width, uint32_t height); + ~GlFrameBuffer(); + + // do not allow copy or move + GlFrameBuffer(const GlFrameBuffer&) = delete; + GlFrameBuffer& operator=(const GlFrameBuffer&) = delete; + GlFrameBuffer(GlFrameBuffer&&) = delete; + GlFrameBuffer& operator=(GlFrameBuffer&&) = delete; + + // public methods + void Bind() const; + void Unbind() const; + void Clear() const; + uint32_t GetTextureId() const { return texture_id_; } + + uint32_t GetWidth() const { return width_; } + uint32_t GetHeight() const { return height_; } + void Resize(uint32_t width, uint32_t height); + + private: + void CreateBuffers(); + void DestroyBuffers(); + + uint32_t width_; + uint32_t height_; + uint32_t texture_id_; + uint32_t frame_buffer_; + uint32_t texture_buffer_; + uint32_t render_buffer_; +}; +} // namespace quickviz + +#endif // XMOTION_GL_FRAME_BUFFER_HPP \ No newline at end of file diff --git a/src/imview/include/imview/widget/gl_scene_widget.hpp b/src/imview/include/imview/widget/gl_scene_widget.hpp new file mode 100644 index 0000000..d3d791b --- /dev/null +++ b/src/imview/include/imview/widget/gl_scene_widget.hpp @@ -0,0 +1,35 @@ +/* + * @file gl_scene_widget.hpp + * @date 10/29/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef XMOTION_GL_SCENE_WIDGET_HPP +#define XMOTION_GL_SCENE_WIDGET_HPP + +#include +#include + +#include "imview/panel.hpp" +#include "imview/widget/details/gl_frame_buffer.hpp" + +namespace quickviz { +class GlSceneWidget : public Panel { + public: + GlSceneWidget(const std::string& widget_name); + ~GlSceneWidget() = default; + + // public methods + using GlRenderFunction = std::function; + void SetGlRenderFunction(GlRenderFunction func); + void Draw() override; + + private: + GlRenderFunction render_function_; + std::unique_ptr frame_buffer_; +}; +} // namespace quickviz + +#endif // XMOTION_GL_SCENE_WIDGET_HPP \ No newline at end of file diff --git a/src/imview/src/widget/cv_image_widget.cpp b/src/imview/src/widget/cv_image_widget.cpp index d3f9052..d080851 100644 --- a/src/imview/src/widget/cv_image_widget.cpp +++ b/src/imview/src/widget/cv_image_widget.cpp @@ -8,6 +8,7 @@ #include "imview/widget/cv_image_widget.hpp" +#include "glad/glad.h" #include "imview/utils/image_utils.hpp" namespace quickviz { diff --git a/src/imview/src/widget/cairo/cairo_context.cpp b/src/imview/src/widget/details/cairo_context.cpp similarity index 98% rename from src/imview/src/widget/cairo/cairo_context.cpp rename to src/imview/src/widget/details/cairo_context.cpp index e9c83b5..78b76bf 100644 --- a/src/imview/src/widget/cairo/cairo_context.cpp +++ b/src/imview/src/widget/details/cairo_context.cpp @@ -7,7 +7,7 @@ * Copyright (c) 2021 Ruixiang Du (rdu) */ -#include "imview/widget/cairo//cairo_context.hpp" +#include "imview/widget/details/cairo_context.hpp" #include diff --git a/src/imview/src/widget/cairo/cairo_draw.cpp b/src/imview/src/widget/details/cairo_draw.cpp similarity index 98% rename from src/imview/src/widget/cairo/cairo_draw.cpp rename to src/imview/src/widget/details/cairo_draw.cpp index 978f9ba..2f5f3b0 100644 --- a/src/imview/src/widget/cairo/cairo_draw.cpp +++ b/src/imview/src/widget/details/cairo_draw.cpp @@ -7,7 +7,7 @@ * Copyright (c) 2021 Ruixiang Du (rdu) */ -#include "imview/widget/cairo/cairo_draw.hpp" +#include "imview/widget/details/cairo_draw.hpp" #include diff --git a/src/imview/src/widget/details/gl_frame_buffer.cpp b/src/imview/src/widget/details/gl_frame_buffer.cpp new file mode 100644 index 0000000..102a515 --- /dev/null +++ b/src/imview/src/widget/details/gl_frame_buffer.cpp @@ -0,0 +1,91 @@ +/* + * @file gl_frame_buffer.cpp + * @date 10/29/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/widget/details/gl_frame_buffer.hpp" + +#include + +#include "glad/glad.h" + +namespace quickviz { +GlFrameBuffer::GlFrameBuffer(uint32_t width, uint32_t height) + : width_(width), + height_(height), + frame_buffer_(0), + texture_buffer_(0), + render_buffer_(0) { + CreateBuffers(); +} + +GlFrameBuffer::~GlFrameBuffer() { DestroyBuffers(); } + +void GlFrameBuffer::CreateBuffers() { + // generate and bind the framebuffer + glGenFramebuffers(1, &frame_buffer_); + glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_); + + // create a color attachment texture + glGenTextures(1, &texture_buffer_); + glBindTexture(GL_TEXTURE_2D, texture_buffer_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width_, height_, 0, GL_RGB, + GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + texture_buffer_, 0); + + // create a renderbuffer object for depth and stencil attachment + glGenRenderbuffers(1, &render_buffer_); + glBindRenderbuffer(GL_RENDERBUFFER, render_buffer_); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width_, height_); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_RENDERBUFFER, render_buffer_); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + throw std::runtime_error("Framebuffer initialization unsuccessful!"); + } + + // Unbind framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void GlFrameBuffer::DestroyBuffers() { + glDeleteFramebuffers(1, &frame_buffer_); + glDeleteTextures(1, &texture_buffer_); + glDeleteRenderbuffers(1, &render_buffer_); + frame_buffer_ = 0; + texture_buffer_ = 0; + render_buffer_ = 0; +} + +void GlFrameBuffer::Bind() const { + glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_); + glViewport(0, 0, width_, height_); + glEnable(GL_DEPTH_TEST); +} + +void GlFrameBuffer::Unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } + +void GlFrameBuffer::Clear() const { + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +void GlFrameBuffer::Resize(uint32_t width, uint32_t height) { + if (width == width_ && height == height_) return; + + width_ = width; + height_ = height; + + // delete old buffers and create new ones + DestroyBuffers(); + CreateBuffers(); + + std::cout << "new frame buffer id: " << frame_buffer_ << std::endl; +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/widget/gl_scene_widget.cpp b/src/imview/src/widget/gl_scene_widget.cpp new file mode 100644 index 0000000..a56d3af --- /dev/null +++ b/src/imview/src/widget/gl_scene_widget.cpp @@ -0,0 +1,46 @@ +/* + * @file gl_scene_widget.cpp + * @date 10/29/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/widget/gl_scene_widget.hpp" + +namespace quickviz { +GlSceneWidget::GlSceneWidget(const std::string& widget_name) + : Panel(widget_name) { + this->SetAutoLayout(false); + this->SetWindowNoMenuButton(); + this->SetNoBackground(true); + + frame_buffer_ = std::make_unique(100, 50); +} + +void GlSceneWidget::SetGlRenderFunction(GlSceneWidget::GlRenderFunction func) { + render_function_ = func; +} + +void GlSceneWidget::Draw() { + Begin(); + { + ImVec2 contentSize = ImGui::GetContentRegionAvail(); + float width = contentSize.x; + float height = contentSize.y; + + frame_buffer_->Resize(width, height); + frame_buffer_->Bind(); + if (render_function_ != nullptr) render_function_(*frame_buffer_.get()); + frame_buffer_->Unbind(); + + ImVec2 uv0 = ImVec2(0, 1); + ImVec2 uv1 = ImVec2(1, 0); + ImVec4 tint_col = ImVec4(1, 1, 1, 1); + ImVec4 border_col = ImVec4(0, 0, 0, 0); + ImGui::Image((void*)(intptr_t)frame_buffer_->GetTextureId(), + ImVec2(width, height), uv0, uv1, tint_col, border_col); + } + End(); +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/test/CMakeLists.txt b/src/imview/test/CMakeLists.txt index 8d79f6d..a8d721b 100644 --- a/src/imview/test/CMakeLists.txt +++ b/src/imview/test/CMakeLists.txt @@ -17,6 +17,9 @@ target_link_libraries(test_event PRIVATE imview) add_executable(test_async_event test_async_event.cpp) target_link_libraries(test_async_event PRIVATE imview) +add_executable(test_framebuffer test_framebuffer.cpp) +target_link_libraries(test_framebuffer PRIVATE imview) + find_package(OpenCV QUIET) if (OpenCV_FOUND) add_executable(test_double_buffer test_double_buffer.cpp) diff --git a/src/imview/test/feature/CMakeLists.txt b/src/imview/test/feature/CMakeLists.txt index eeb5854..d71e979 100644 --- a/src/imview/test/feature/CMakeLists.txt +++ b/src/imview/test/feature/CMakeLists.txt @@ -15,3 +15,6 @@ target_link_libraries(test_cairo_widget PRIVATE imview) add_executable(test_implot_widget test_implot_widget.cpp) target_link_libraries(test_implot_widget PRIVATE imview) + +add_executable(test_gl_scene_widget test_gl_scene_widget.cpp) +target_link_libraries(test_gl_scene_widget PRIVATE imview) diff --git a/src/imview/test/feature/test_gl_scene_widget.cpp b/src/imview/test/feature/test_gl_scene_widget.cpp new file mode 100644 index 0000000..21c8e52 --- /dev/null +++ b/src/imview/test/feature/test_gl_scene_widget.cpp @@ -0,0 +1,50 @@ +/* + * test_cv_image_widget.cpp + * + * Created on: Jul 27, 2021 09:07 + * Description: + * + * Copyright (c) 2021 Ruixiang Du (rdu) + */ + +#include +#include + +#include + +#include "imview/viewer.hpp" +#include "imview/widget/gl_scene_widget.hpp" + +using namespace quickviz; + +void RenderGL(const GlFrameBuffer& frame_buffer) { + // glBegin(GL_TRIANGLES); // Each set of 3 vertices form a triangle + // glColor3f(0.0f, 0.0f, 1.0f); // Blue + // glVertex2f(0.1f, -0.6f); + // glVertex2f(0.7f, -0.6f); + // glVertex2f(0.4f, -0.1f); + // + // glColor3f(1.0f, 0.0f, 0.0f); // Red + // glVertex2f(0.3f, -0.4f); + // glColor3f(0.0f, 1.0f, 0.0f); // Green + // glVertex2f(0.9f, -0.4f); + // glColor3f(0.0f, 0.0f, 1.0f); // Blue + // glVertex2f(0.6f, -0.9f); + // glEnd(); + glClearColor(1.0f, 0.0f, 0.0f, 1.0f); // Red background to make it noticeable + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +int main(int argc, char* argv[]) { + Viewer viewer; + + auto gl_widget = std::make_shared("OpenGL Scene"); + gl_widget->OnResize(300, 200); + gl_widget->SetPosition(0, 0); + gl_widget->SetGlRenderFunction(RenderGL); + viewer.AddSceneObject(gl_widget); + + viewer.Show(); + + return 0; +} \ No newline at end of file diff --git a/src/imview/test/test_framebuffer.cpp b/src/imview/test/test_framebuffer.cpp new file mode 100644 index 0000000..9728186 --- /dev/null +++ b/src/imview/test/test_framebuffer.cpp @@ -0,0 +1,60 @@ +/* + * test_wgui.cpp + * + * Created on: Jul 22, 2021 14:50 + * Description: + * + * Copyright (c) 2021 Ruixiang Du (rdu) + */ + +#include +#include + +#include "glad/glad.h" +#include "imview/window.hpp" +#include "imview/widget/details/gl_frame_buffer.hpp" + +using namespace quickviz; + +int main(int argc, char* argv[]) { + int width = 1920; + int height = 1080; + Window win("Test Window", width, height); + + // Create a framebuffer + auto frame_buffer = std::make_unique(width, height); + + while (!win.ShouldClose()) { + win.PollEvents(); + + // Bind and clear the framebuffer + frame_buffer->Bind(); + glViewport(0, 0, width, height); // Set the viewport to framebuffer size + glClearColor(1.0f, 0.0f, 0.0f, 1.0f); // Clear with red + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + frame_buffer->Unbind(); + + // Render the framebuffer's texture directly to the screen + glBindFramebuffer(GL_FRAMEBUFFER, 0); // Bind default framebuffer + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Clear the screen + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, frame_buffer->GetTextureId()); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); + glVertex2f(-1.0f, -1.0f); + glTexCoord2f(1.0f, 0.0f); + glVertex2f(1.0f, -1.0f); + glTexCoord2f(1.0f, 1.0f); + glVertex2f(1.0f, 1.0f); + glTexCoord2f(0.0f, 1.0f); + glVertex2f(-1.0f, 1.0f); + glEnd(); + glBindTexture(GL_TEXTURE_2D, 0); + + win.SwapBuffers(); + } + + return 0; +} From c2e4c8981490ed6237e7e5b38eb699c0536afa09 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Wed, 30 Oct 2024 23:19:00 +0800 Subject: [PATCH 04/20] gl_widget: fixed framebuffer issue --- src/app/panels/config_panel.cpp | 2 +- src/app/panels/console_panel.cpp | 2 +- src/app/panels/main_docking_panel.cpp | 14 ++-- src/app/panels/main_docking_panel.hpp | 4 ++ src/imview/include/imview/panel.hpp | 10 +-- .../imview/widget/details/gl_frame_buffer.hpp | 6 +- .../src/widget/details/gl_frame_buffer.cpp | 6 +- .../test/feature/test_gl_scene_widget.cpp | 66 ++++++++++++++----- 8 files changed, 75 insertions(+), 35 deletions(-) diff --git a/src/app/panels/config_panel.cpp b/src/app/panels/config_panel.cpp index 115503b..0f4ac0e 100644 --- a/src/app/panels/config_panel.cpp +++ b/src/app/panels/config_panel.cpp @@ -14,7 +14,7 @@ namespace quickviz { ConfigPanel::ConfigPanel(std::string name) : Panel(name) { this->SetAutoLayout(false); this->SetNoResize(true); -// this->SetNoMove(true); + // this->SetNoMove(true); this->SetWindowNoMenuButton(); } diff --git a/src/app/panels/console_panel.cpp b/src/app/panels/console_panel.cpp index 4eaa04b..21dd8d3 100644 --- a/src/app/panels/console_panel.cpp +++ b/src/app/panels/console_panel.cpp @@ -14,7 +14,7 @@ namespace quickviz { ConsolePanel::ConsolePanel(std::string name) : Panel(name) { this->SetAutoLayout(false); this->SetNoResize(true); -// this->SetNoMove(true); + // this->SetNoMove(true); this->SetWindowNoMenuButton(); } diff --git a/src/app/panels/main_docking_panel.cpp b/src/app/panels/main_docking_panel.cpp index 1a8e4aa..36f8d44 100644 --- a/src/app/panels/main_docking_panel.cpp +++ b/src/app/panels/main_docking_panel.cpp @@ -17,6 +17,8 @@ MainDockingPanel::MainDockingPanel(std::string name) : Panel(name) { this->SetNoResize(true); this->SetNoTitleBar(true); this->SetNoBackground(true); + + gl_scene_widget_.SetNoMove(true); } void MainDockingPanel::Draw() { @@ -30,14 +32,14 @@ void MainDockingPanel::Draw() { dockspace_id_ = ImGui::DockBuilderAddNode(); ImGui::DockBuilderSplitNode(dockspace_id_, ImGuiDir_Left, 0.2f, - &config_panel_node_, &scene_panel_node_); - ImGui::DockBuilderSplitNode(scene_panel_node_, ImGuiDir_Up, 0.8f, - &scene_panel_node_, &console_panel_node_); + &config_panel_node_, &gl_scene_widget_node_); + ImGui::DockBuilderSplitNode(gl_scene_widget_node_, ImGuiDir_Up, 0.8f, + &gl_scene_widget_node_, &console_panel_node_); ImGui::DockBuilderDockWindow(config_panel_.GetName().c_str(), config_panel_node_); - ImGui::DockBuilderDockWindow(scene_panel_.GetName().c_str(), - scene_panel_node_); + ImGui::DockBuilderDockWindow(gl_scene_widget_.GetName().c_str(), + gl_scene_widget_node_); ImGui::DockBuilderDockWindow(console_panel_.GetName().c_str(), console_panel_node_); @@ -52,6 +54,6 @@ void MainDockingPanel::Draw() { // draw child panels if (config_panel_.IsVisible()) config_panel_.Draw(); if (console_panel_.IsVisible()) console_panel_.Draw(); - if (scene_panel_.IsVisible()) scene_panel_.Draw(); + if (gl_scene_widget_.IsVisible()) gl_scene_widget_.Draw(); } } // namespace quickviz \ No newline at end of file diff --git a/src/app/panels/main_docking_panel.hpp b/src/app/panels/main_docking_panel.hpp index 509c665..f12026d 100644 --- a/src/app/panels/main_docking_panel.hpp +++ b/src/app/panels/main_docking_panel.hpp @@ -11,6 +11,8 @@ #include "imview/panel.hpp" +#include "imview/widget/gl_scene_widget.hpp" + #include "panels/config_panel.hpp" #include "panels/scene_panel.hpp" #include "panels/console_panel.hpp" @@ -29,10 +31,12 @@ class MainDockingPanel : public Panel { ImGuiID config_panel_node_; ImGuiID scene_panel_node_; ImGuiID console_panel_node_; + ImGuiID gl_scene_widget_node_; ConfigPanel config_panel_; ScenePanel scene_panel_; ConsolePanel console_panel_; + GlSceneWidget gl_scene_widget_ {"Scene"}; }; } // namespace quickviz diff --git a/src/imview/include/imview/panel.hpp b/src/imview/include/imview/panel.hpp index 29f1fd6..32e6cc5 100644 --- a/src/imview/include/imview/panel.hpp +++ b/src/imview/include/imview/panel.hpp @@ -25,11 +25,6 @@ class Panel : public SceneObject { void SetAutoLayout(bool value); void OnRender() override; - protected: - // for derived classes - void Begin(bool *p_open = NULL); - void End(); - void SetNoTitleBar(bool value); void SetNoResize(bool value); void SetNoMove(bool value); @@ -61,6 +56,11 @@ class Panel : public SceneObject { virtual void Draw() = 0; + protected: + // for derived classes + void Begin(bool *p_open = NULL); + void End(); + private: bool auto_layout_ = false; ImGuiWindowFlags flags_ = ImGuiWindowFlags_None; diff --git a/src/imview/include/imview/widget/details/gl_frame_buffer.hpp b/src/imview/include/imview/widget/details/gl_frame_buffer.hpp index 7728f1a..d410b58 100644 --- a/src/imview/include/imview/widget/details/gl_frame_buffer.hpp +++ b/src/imview/include/imview/widget/details/gl_frame_buffer.hpp @@ -26,8 +26,9 @@ class GlFrameBuffer { // public methods void Bind() const; void Unbind() const; - void Clear() const; - uint32_t GetTextureId() const { return texture_id_; } + void Clear(float r = 0.0, float g = 0.0, float b = 0.0, + float a = 1.0) const; + uint32_t GetTextureId() const { return texture_buffer_; } uint32_t GetWidth() const { return width_; } uint32_t GetHeight() const { return height_; } @@ -39,7 +40,6 @@ class GlFrameBuffer { uint32_t width_; uint32_t height_; - uint32_t texture_id_; uint32_t frame_buffer_; uint32_t texture_buffer_; uint32_t render_buffer_; diff --git a/src/imview/src/widget/details/gl_frame_buffer.cpp b/src/imview/src/widget/details/gl_frame_buffer.cpp index 102a515..d781cf1 100644 --- a/src/imview/src/widget/details/gl_frame_buffer.cpp +++ b/src/imview/src/widget/details/gl_frame_buffer.cpp @@ -71,8 +71,8 @@ void GlFrameBuffer::Bind() const { void GlFrameBuffer::Unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } -void GlFrameBuffer::Clear() const { - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); +void GlFrameBuffer::Clear(float r, float g, float b, float a) const { + glClearColor(r, g, b, a); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } @@ -85,7 +85,5 @@ void GlFrameBuffer::Resize(uint32_t width, uint32_t height) { // delete old buffers and create new ones DestroyBuffers(); CreateBuffers(); - - std::cout << "new frame buffer id: " << frame_buffer_ << std::endl; } } // namespace quickviz \ No newline at end of file diff --git a/src/imview/test/feature/test_gl_scene_widget.cpp b/src/imview/test/feature/test_gl_scene_widget.cpp index 21c8e52..0665377 100644 --- a/src/imview/test/feature/test_gl_scene_widget.cpp +++ b/src/imview/test/feature/test_gl_scene_widget.cpp @@ -18,21 +18,57 @@ using namespace quickviz; void RenderGL(const GlFrameBuffer& frame_buffer) { - // glBegin(GL_TRIANGLES); // Each set of 3 vertices form a triangle - // glColor3f(0.0f, 0.0f, 1.0f); // Blue - // glVertex2f(0.1f, -0.6f); - // glVertex2f(0.7f, -0.6f); - // glVertex2f(0.4f, -0.1f); - // - // glColor3f(1.0f, 0.0f, 0.0f); // Red - // glVertex2f(0.3f, -0.4f); - // glColor3f(0.0f, 1.0f, 0.0f); // Green - // glVertex2f(0.9f, -0.4f); - // glColor3f(0.0f, 0.0f, 1.0f); // Blue - // glVertex2f(0.6f, -0.9f); - // glEnd(); - glClearColor(1.0f, 0.0f, 0.0f, 1.0f); // Red background to make it noticeable - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + frame_buffer.Clear(); + + // Define shapes enclosed within a pair of glBegin and glEnd + glBegin(GL_QUADS); // Each set of 4 vertices form a quad + glColor3f(1.0f, 0.0f, 0.0f); // Red + glVertex2f(-0.8f, 0.1f); // Define vertices in counter-clockwise (CCW) order + glVertex2f(-0.2f, 0.1f); // so that the normal (front-face) is facing you + glVertex2f(-0.2f, 0.7f); + glVertex2f(-0.8f, 0.7f); + + glColor3f(0.0f, 1.0f, 0.0f); // Green + glVertex2f(-0.7f, -0.6f); + glVertex2f(-0.1f, -0.6f); + glVertex2f(-0.1f, 0.0f); + glVertex2f(-0.7f, 0.0f); + + glColor3f(0.2f, 0.2f, 0.2f); // Dark Gray + glVertex2f(-0.9f, -0.7f); + glColor3f(1.0f, 1.0f, 1.0f); // White + glVertex2f(-0.5f, -0.7f); + glColor3f(0.2f, 0.2f, 0.2f); // Dark Gray + glVertex2f(-0.5f, -0.3f); + glColor3f(1.0f, 1.0f, 1.0f); // White + glVertex2f(-0.9f, -0.3f); + glEnd(); + + glBegin(GL_TRIANGLES); // Each set of 3 vertices form a triangle + glColor3f(0.0f, 0.0f, 1.0f); // Blue + glVertex2f(0.1f, -0.6f); + glVertex2f(0.7f, -0.6f); + glVertex2f(0.4f, -0.1f); + + glColor3f(1.0f, 0.0f, 0.0f); // Red + glVertex2f(0.3f, -0.4f); + glColor3f(0.0f, 1.0f, 0.0f); // Green + glVertex2f(0.9f, -0.4f); + glColor3f(0.0f, 0.0f, 1.0f); // Blue + glVertex2f(0.6f, -0.9f); + glEnd(); + + glBegin(GL_POLYGON); // These vertices form a closed polygon + glColor3f(1.0f, 1.0f, 0.0f); // Yellow + glVertex2f(0.4f, 0.2f); + glVertex2f(0.6f, 0.2f); + glVertex2f(0.7f, 0.4f); + glVertex2f(0.6f, 0.6f); + glVertex2f(0.4f, 0.6f); + glVertex2f(0.3f, 0.4f); + glEnd(); + + glFlush(); // Render now } int main(int argc, char* argv[]) { From 88933f14e35b5df2aba730d080a8a7dfe94de57b Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Fri, 1 Nov 2024 00:15:54 +0800 Subject: [PATCH 05/20] updated gl_frame_buffer, updated quickviz app --- src/app/CMakeLists.txt | 3 +- src/app/panels/config_panel.cpp | 3 +- src/app/panels/console_panel.cpp | 4 +- src/app/panels/main_docking_panel.cpp | 17 ++++-- src/app/panels/main_docking_panel.hpp | 15 +++--- src/app/panels/menu_bar.cpp | 53 +++++++++++++++++++ src/app/panels/menu_bar.hpp | 40 ++++++++++++++ src/app/panels/scene_panel.cpp | 43 --------------- src/app/panels/scene_panel.hpp | 23 -------- src/app/quickviz_application.cpp | 33 +++++++++++- src/app/quickviz_application.hpp | 6 +++ src/imview/include/imview/fonts.hpp | 2 + src/imview/include/imview/panel.hpp | 1 + src/imview/include/imview/viewer.hpp | 2 + .../imview/widget/details/gl_frame_buffer.hpp | 5 +- src/imview/src/fonts.cpp | 7 +++ src/imview/src/panel.cpp | 2 + src/imview/src/viewer.cpp | 7 ++- .../src/widget/details/gl_frame_buffer.cpp | 11 +++- 19 files changed, 187 insertions(+), 90 deletions(-) create mode 100644 src/app/panels/menu_bar.cpp create mode 100644 src/app/panels/menu_bar.hpp delete mode 100644 src/app/panels/scene_panel.cpp delete mode 100644 src/app/panels/scene_panel.hpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index cbe2ab5..6f84d51 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -4,10 +4,9 @@ if (BUILD_QUICKVIZ_APP) # components quickviz_application.cpp # panels + panels/menu_bar.cpp panels/main_docking_panel.cpp panels/config_panel.cpp - panels/scene_panel.cpp - panels/config_panel.cpp panels/console_panel.cpp) target_link_libraries(quickviz PRIVATE imview) target_include_directories(quickviz PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/app/panels/config_panel.cpp b/src/app/panels/config_panel.cpp index 0f4ac0e..93f31c6 100644 --- a/src/app/panels/config_panel.cpp +++ b/src/app/panels/config_panel.cpp @@ -14,8 +14,9 @@ namespace quickviz { ConfigPanel::ConfigPanel(std::string name) : Panel(name) { this->SetAutoLayout(false); this->SetNoResize(true); - // this->SetNoMove(true); + this->SetNoMove(true); this->SetWindowNoMenuButton(); + this->SetWindowNoTabBar(); } void ConfigPanel::Draw() { diff --git a/src/app/panels/console_panel.cpp b/src/app/panels/console_panel.cpp index 21dd8d3..1dd25bf 100644 --- a/src/app/panels/console_panel.cpp +++ b/src/app/panels/console_panel.cpp @@ -13,8 +13,8 @@ namespace quickviz { ConsolePanel::ConsolePanel(std::string name) : Panel(name) { this->SetAutoLayout(false); - this->SetNoResize(true); - // this->SetNoMove(true); + // this->SetNoResize(true); + this->SetNoMove(true); this->SetWindowNoMenuButton(); } diff --git a/src/app/panels/main_docking_panel.cpp b/src/app/panels/main_docking_panel.cpp index 36f8d44..bd167d7 100644 --- a/src/app/panels/main_docking_panel.cpp +++ b/src/app/panels/main_docking_panel.cpp @@ -8,22 +8,26 @@ #include "panels/main_docking_panel.hpp" +#include "imgui.h" #include "imgui_internal.h" namespace quickviz { MainDockingPanel::MainDockingPanel(std::string name) : Panel(name) { - this->SetAutoLayout(false); + this->SetAutoLayout(true); this->SetNoMove(true); this->SetNoResize(true); this->SetNoTitleBar(true); this->SetNoBackground(true); gl_scene_widget_.SetNoMove(true); + gl_scene_widget_.SetWindowNoTabBar(); } void MainDockingPanel::Draw() { - ImGui::SetNextWindowSize(ImVec2(width_, height_)); - ImGui::SetNextWindowPos(ImVec2(0, 0)); + if (!IsAutoLayout()) { + ImGui::SetNextWindowSize(ImVec2(width_, height_)); + ImGui::SetNextWindowPos(ImVec2(0, 0)); + } // set up layout Begin(); @@ -49,11 +53,16 @@ void MainDockingPanel::Draw() { ImGui::DockBuilderSetNodePos(dockspace_id_, ImGui::GetWindowPos()); ImGui::DockBuilderSetNodeSize(dockspace_id_, ImGui::GetWindowSize()); } - End(); // draw child panels if (config_panel_.IsVisible()) config_panel_.Draw(); if (console_panel_.IsVisible()) console_panel_.Draw(); if (gl_scene_widget_.IsVisible()) gl_scene_widget_.Draw(); + + End(); +} + +void MainDockingPanel::ChangeDebugPanelVisibility(bool visible) { + console_panel_.SetVisibility(visible); } } // namespace quickviz \ No newline at end of file diff --git a/src/app/panels/main_docking_panel.hpp b/src/app/panels/main_docking_panel.hpp index f12026d..615627d 100644 --- a/src/app/panels/main_docking_panel.hpp +++ b/src/app/panels/main_docking_panel.hpp @@ -13,8 +13,8 @@ #include "imview/widget/gl_scene_widget.hpp" +#include "panels/menu_bar.hpp" #include "panels/config_panel.hpp" -#include "panels/scene_panel.hpp" #include "panels/console_panel.hpp" namespace quickviz { @@ -24,19 +24,20 @@ class MainDockingPanel : public Panel { void Draw() override; + void ChangeDebugPanelVisibility(bool visible); + private: - bool layout_initialized_ = false; + bool layout_initialized_ = false; ImGuiID dockspace_id_; + ImGuiID config_panel_node_; - ImGuiID scene_panel_node_; ImGuiID console_panel_node_; ImGuiID gl_scene_widget_node_; - ConfigPanel config_panel_; - ScenePanel scene_panel_; - ConsolePanel console_panel_; - GlSceneWidget gl_scene_widget_ {"Scene"}; + ConfigPanel config_panel_{"Config"}; + ConsolePanel console_panel_{"Console"}; + GlSceneWidget gl_scene_widget_{"Scene"}; }; } // namespace quickviz diff --git a/src/app/panels/menu_bar.cpp b/src/app/panels/menu_bar.cpp new file mode 100644 index 0000000..2823be6 --- /dev/null +++ b/src/app/panels/menu_bar.cpp @@ -0,0 +1,53 @@ +/* + * @file menu_bar.cpp + * @date 10/30/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "panels/menu_bar.hpp" + +namespace quickviz { +MenuBar::MenuBar(std::string name) : Panel(name) { + this->SetAutoLayout(true); + this->SetNoResize(false); +} + +void MenuBar::Draw() { + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + // ShowExampleMenuFile(); + if (ImGui::MenuItem("Exit", "ESC")) { + if (window_should_close_callback_) window_should_close_callback_(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) { + if (ImGui::MenuItem("Undo", "CTRL+Z")) { + } + if (ImGui::MenuItem("Redo", "CTRL+Y", false, false)) { + } // Disabled item + ImGui::Separator(); + if (ImGui::MenuItem("Cut", "CTRL+X")) { + } + if (ImGui::MenuItem("Copy", "CTRL+C")) { + } + if (ImGui::MenuItem("Paste", "CTRL+V")) { + } + ImGui::EndMenu(); + } + static bool show_debug_panel = true; + if (ImGui::BeginMenu("View")) { + if (ImGui::MenuItem(" Show Console ", NULL, show_debug_panel)) { + show_debug_panel = !show_debug_panel; + if (change_debug_panel_visibility_callback_) { + change_debug_panel_visibility_callback_(show_debug_panel); + } + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } +} +} // namespace quickviz \ No newline at end of file diff --git a/src/app/panels/menu_bar.hpp b/src/app/panels/menu_bar.hpp new file mode 100644 index 0000000..0eae9ae --- /dev/null +++ b/src/app/panels/menu_bar.hpp @@ -0,0 +1,40 @@ +/* + * @file menu_bar.hpp + * @date 10/30/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef XMOTION_MENU_BAR_HPP +#define XMOTION_MENU_BAR_HPP + +#include + +#include "imview/panel.hpp" + +namespace quickviz { +class MenuBar : public Panel { + public: + MenuBar(std::string name = "Menu"); + + void Draw() override; + + using ChangeDebugPanelVisibilityCallback = std::function; + void SetChangeDebugPanelVisibilityCallback( + std::function callback) { + change_debug_panel_visibility_callback_ = callback; + } + + using WindowShouldCloseCallback = std::function; + void SetWindowShouldCloseCallback(std::function callback) { + window_should_close_callback_ = callback; + } + + private: + ChangeDebugPanelVisibilityCallback change_debug_panel_visibility_callback_; + WindowShouldCloseCallback window_should_close_callback_; +}; +} // namespace quickviz + +#endif // XMOTION_MENU_BAR_HPP \ No newline at end of file diff --git a/src/app/panels/scene_panel.cpp b/src/app/panels/scene_panel.cpp deleted file mode 100644 index 9f358ca..0000000 --- a/src/app/panels/scene_panel.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * @file scene_panel.cpp - * @date 10/8/24 - * @brief - * - * @copyright Copyright (c) 2024 Ruixiang Du (rdu) - */ - -#include "panels/scene_panel.hpp" - -#include "glad/glad.h" -#include "imview/fonts.hpp" - -namespace quickviz { -ScenePanel::ScenePanel(std::string name) : Panel(name) { - this->SetAutoLayout(false); - this->SetNoResize(true); -// this->SetNoMove(true); - this->SetWindowNoMenuButton(); -} - -void ScenePanel::Draw() { - Begin(); - { - ImGui::PushFont(Fonts::GetFont(FontSize::kFont18)); - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 153, 153, 200)); - ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", - 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); - ImGui::PopStyleColor(); - ImGui::PopFont(); - - glEnable(GL_SCISSOR_TEST); - glViewport(x_, y_, width_, height_); - glScissor(x_, y_, width_, height_); - - glClearColor(0.5, 0, 0, 1.0); - glClear(GL_COLOR_BUFFER_BIT); - - glDisable(GL_SCISSOR_TEST); - } - End(); -} -} // namespace quickviz \ No newline at end of file diff --git a/src/app/panels/scene_panel.hpp b/src/app/panels/scene_panel.hpp deleted file mode 100644 index fde5246..0000000 --- a/src/app/panels/scene_panel.hpp +++ /dev/null @@ -1,23 +0,0 @@ -/* - * @file scene_panel.hpp - * @date 10/8/24 - * @brief - * - * @copyright Copyright (c) 2024 Ruixiang Du (rdu) - */ - -#ifndef QUICKVIZ_SCENE_PANEL_HPP -#define QUICKVIZ_SCENE_PANEL_HPP - -#include "imview/panel.hpp" - -namespace quickviz { -class ScenePanel : public Panel { - public: - ScenePanel(std::string name = "Scene"); - - void Draw() override; -}; -} // namespace quickviz - -#endif // QUICKVIZ_SCENE_PANEL_HPP \ No newline at end of file diff --git a/src/app/quickviz_application.cpp b/src/app/quickviz_application.cpp index d118074..60e3ea8 100644 --- a/src/app/quickviz_application.cpp +++ b/src/app/quickviz_application.cpp @@ -17,16 +17,45 @@ QuickvizApplication::QuickvizApplication() { bool QuickvizApplication::Initialize() { // setup ui layer ui_layer_ = std::make_shared("ui_layer"); - main_docking_panel_ = std::make_shared(); - ui_layer_->AddChild(main_docking_panel_); + + main_box_ = std::make_shared("main_box"); + main_box_->SetFlexDirection(Styling::FlexDirection::kColumn); + { + menu_bar_ = std::make_shared("menu_bar"); + menu_bar_->SetHeight(24); + menu_bar_->SetFlexGrow(0); + menu_bar_->SetFlexShrink(0); + main_box_->AddChild(menu_bar_); + + main_docking_panel_ = std::make_shared(); + main_docking_panel_->SetFlexGrow(1); + main_docking_panel_->SetFlexShrink(1); + main_box_->AddChild(main_docking_panel_); + } + ui_layer_->AddChild(main_box_); // add ui layer to viewer viewer_->AddSceneObject(ui_layer_); + // set up callbacks + menu_bar_->SetChangeDebugPanelVisibilityCallback( + std::bind(&QuickvizApplication::OnChangeDebugPanelVisibility, this, + std::placeholders::_1)); + menu_bar_->SetWindowShouldCloseCallback( + std::bind(&QuickvizApplication::OnWindowShouldClose, this)); + // initialize data reader return true; } void QuickvizApplication::Run() { viewer_->Show(); } + +void QuickvizApplication::OnChangeDebugPanelVisibility(bool visible) { + main_docking_panel_->ChangeDebugPanelVisibility(visible); +} + +void QuickvizApplication::OnWindowShouldClose() { + viewer_->SetWindowShouldClose(); +} } // namespace quickviz \ No newline at end of file diff --git a/src/app/quickviz_application.hpp b/src/app/quickviz_application.hpp index b6af638..90ecde4 100644 --- a/src/app/quickviz_application.hpp +++ b/src/app/quickviz_application.hpp @@ -11,6 +11,7 @@ #include "imview/viewer.hpp" #include "imview/layer.hpp" +#include "imview/box.hpp" #include "panels/main_docking_panel.hpp" #include "data_reader.hpp" @@ -28,11 +29,16 @@ class QuickvizApplication { bool Initialize(); void Run(); + void OnChangeDebugPanelVisibility(bool visible); + void OnWindowShouldClose(); + private: std::unique_ptr viewer_; // ui elements std::shared_ptr ui_layer_; + std::shared_ptr main_box_; + std::shared_ptr menu_bar_; std::shared_ptr main_docking_panel_; std::shared_ptr plot_layer_; diff --git a/src/imview/include/imview/fonts.hpp b/src/imview/include/imview/fonts.hpp index f94e6dd..1002181 100644 --- a/src/imview/include/imview/fonts.hpp +++ b/src/imview/include/imview/fonts.hpp @@ -14,9 +14,11 @@ namespace quickviz { // enum class FontSize { Tiny, Small, Normal, Big, Large, ExtraLarge }; enum class FontSize { + kDefault = 0, kFont16 = 16, kFont18 = 18, kFont20 = 20, + kFont24 = 24, kFont28 = 28, kFont32 = 32, kFont40 = 40 diff --git a/src/imview/include/imview/panel.hpp b/src/imview/include/imview/panel.hpp index 32e6cc5..4c8de7c 100644 --- a/src/imview/include/imview/panel.hpp +++ b/src/imview/include/imview/panel.hpp @@ -23,6 +23,7 @@ class Panel : public SceneObject { // public API void SetAutoLayout(bool value); + bool IsAutoLayout() const; void OnRender() override; void SetNoTitleBar(bool value); diff --git a/src/imview/include/imview/viewer.hpp b/src/imview/include/imview/viewer.hpp index 3ea5147..16e8188 100644 --- a/src/imview/include/imview/viewer.hpp +++ b/src/imview/include/imview/viewer.hpp @@ -38,6 +38,8 @@ class Viewer : public Window { void EnableKeyboardNav(bool enable); void EnableGamepadNav(bool enable); + void SetWindowShouldClose(); + // add renderable layers and start viewer loop bool AddSceneObject(std::shared_ptr obj); void Show(); diff --git a/src/imview/include/imview/widget/details/gl_frame_buffer.hpp b/src/imview/include/imview/widget/details/gl_frame_buffer.hpp index d410b58..1ac8758 100644 --- a/src/imview/include/imview/widget/details/gl_frame_buffer.hpp +++ b/src/imview/include/imview/widget/details/gl_frame_buffer.hpp @@ -24,10 +24,9 @@ class GlFrameBuffer { GlFrameBuffer& operator=(GlFrameBuffer&&) = delete; // public methods - void Bind() const; + void Bind(bool keep_aspect_ratio = true) const; void Unbind() const; - void Clear(float r = 0.0, float g = 0.0, float b = 0.0, - float a = 1.0) const; + void Clear(float r = 0.0, float g = 0.0, float b = 0.0, float a = 1.0) const; uint32_t GetTextureId() const { return texture_buffer_; } uint32_t GetWidth() const { return width_; } diff --git a/src/imview/src/fonts.cpp b/src/imview/src/fonts.cpp index 0b5e857..72634c3 100644 --- a/src/imview/src/fonts.cpp +++ b/src/imview/src/fonts.cpp @@ -24,12 +24,19 @@ std::unordered_map custom_fonts_; void Fonts::LoadFonts() { ImGuiIO &io = ImGui::GetIO(); + io.Fonts->Clear(); + + custom_fonts_[FontSize::kDefault] = io.Fonts->AddFontFromMemoryCompressedTTF( + OpenSansRegular_compressed_data, OpenSansRegular_compressed_size, 18.f); + custom_fonts_[FontSize::kFont16] = io.Fonts->AddFontFromMemoryCompressedTTF( OpenSansRegular_compressed_data, OpenSansRegular_compressed_size, 16.f); custom_fonts_[FontSize::kFont18] = io.Fonts->AddFontFromMemoryCompressedTTF( OpenSansRegular_compressed_data, OpenSansRegular_compressed_size, 18.f); custom_fonts_[FontSize::kFont20] = io.Fonts->AddFontFromMemoryCompressedTTF( OpenSansRegular_compressed_data, OpenSansRegular_compressed_size, 20.f); + custom_fonts_[FontSize::kFont24] = io.Fonts->AddFontFromMemoryCompressedTTF( + OpenSansRegular_compressed_data, OpenSansRegular_compressed_size, 24.f); custom_fonts_[FontSize::kFont28] = io.Fonts->AddFontFromMemoryCompressedTTF( OpenSansRegular_compressed_data, OpenSansRegular_compressed_size, 28.f); custom_fonts_[FontSize::kFont32] = io.Fonts->AddFontFromMemoryCompressedTTF( diff --git a/src/imview/src/panel.cpp b/src/imview/src/panel.cpp index 3283c5a..a6ba387 100644 --- a/src/imview/src/panel.cpp +++ b/src/imview/src/panel.cpp @@ -31,6 +31,8 @@ void Panel::End() { ImGui::End(); } void Panel::SetAutoLayout(bool value) { auto_layout_ = value; } +bool Panel::IsAutoLayout() const { return auto_layout_; } + void Panel::SetNoTitleBar(bool value) { if (value) { flags_ |= ImGuiWindowFlags_NoTitleBar; diff --git a/src/imview/src/viewer.cpp b/src/imview/src/viewer.cpp index 6c4825e..f97d42a 100644 --- a/src/imview/src/viewer.cpp +++ b/src/imview/src/viewer.cpp @@ -191,6 +191,10 @@ void Viewer::EnableGamepadNav(bool enable) { } } +void Viewer::SetWindowShouldClose() { + glfwSetWindowShouldClose(win_, GLFW_TRUE); +} + void Viewer::ClearBackground() { int display_w, display_h; glfwGetFramebufferSize(win_, &display_w, &display_h); @@ -227,7 +231,8 @@ bool Viewer::AddSceneObject(std::shared_ptr obj) { } void Viewer::OnResize(GLFWwindow *window, int width, int height) { - std::cout << "-- Viewer::OnResize: " << width << "x" << height << std::endl; + // std::cout << "-- Viewer::OnResize: " << width << "x" << height << + // std::endl; for (auto &obj : scene_objects_) { obj->OnResize(width, height); } diff --git a/src/imview/src/widget/details/gl_frame_buffer.cpp b/src/imview/src/widget/details/gl_frame_buffer.cpp index d781cf1..09578c7 100644 --- a/src/imview/src/widget/details/gl_frame_buffer.cpp +++ b/src/imview/src/widget/details/gl_frame_buffer.cpp @@ -63,9 +63,16 @@ void GlFrameBuffer::DestroyBuffers() { render_buffer_ = 0; } -void GlFrameBuffer::Bind() const { +void GlFrameBuffer::Bind(bool keep_aspect_ratio) const { glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_); - glViewport(0, 0, width_, height_); + if (keep_aspect_ratio) { + float square_size = std::min(width_, height_); + int x_offset = (width_ - square_size) / 2; + int y_offset = (height_ - square_size) / 2; + glViewport(x_offset, y_offset, square_size, square_size); + } else { + glViewport(0, 0, width_, height_); + } glEnable(GL_DEPTH_TEST); } From 6507aff4088b37e83ace82d224955da8baa92733 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sat, 2 Nov 2024 16:58:52 +0800 Subject: [PATCH 06/20] cmake: make yoga and auto layout optional --- .github/workflows/default.yml | 12 ++++-- CMakeLists.txt | 3 +- README.md | 4 +- src/CMakeLists.txt | 4 +- src/app/panels/config_panel.cpp | 43 ++++++++++++++++--- src/imview/CMakeLists.txt | 19 +++++--- .../include/imview/interface/resizable.hpp | 2 + src/imview/include/imview/scene_object.hpp | 6 +++ src/imview/src/layer.cpp | 6 +++ src/imview/src/scene_object.cpp | 17 ++++++-- src/imview/test/CMakeLists.txt | 6 ++- src/imview/test/feature/CMakeLists.txt | 6 ++- src/third_party/CMakeLists.txt | 4 +- 13 files changed, 106 insertions(+), 26 deletions(-) diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 1bcc51d..5ffaf5a 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -14,7 +14,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-22.04, ubuntu-24.04 ] + os: [ ubuntu-20.04, ubuntu-22.04, ubuntu-24.04 ] steps: - uses: actions/checkout@v4 - name: Checkout submodules @@ -23,10 +23,16 @@ jobs: run: sudo apt-get update && sudo apt-get install -y libgl1-mesa-dev libglfw3-dev libcairo2-dev libopencv-dev - name: Create Build Environment run: cmake -E make_directory ${{runner.workspace}}/build - - name: Configure CMake + - name: Configure CMake (Ubuntu 20.04) + if: ${{ matrix.os == 'ubuntu-20.04' }} shell: bash working-directory: ${{runner.workspace}}/build - run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DENABLE_AUTO_LAYOUT=OFF -DBUILD_TESTING=ON + - name: Configure CMake (Ubuntu 22.04 or 24.04) + if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'ubuntu-24.04' }} + shell: bash + working-directory: ${{runner.workspace}}/build + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBUILD_TESTING=ON - name: Build working-directory: ${{runner.workspace}}/build shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef0b93..4812732 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,11 +19,12 @@ endif () message(STATUS "------------------------------------------------") ## Project Options +option(ENABLE_AUTO_LAYOUT "Enable autolayout" ON) +option(IMVIEW_WITH_GLAD "Integrate glad into imview" ON) option(BUILD_QUICKVIZ_APP "Build quickviz app" OFF) option(BUILD_TESTING "Build tests" ON) option(QUICKVIZ_DEV_MODE "Development mode forces building tests" OFF) option(STATIC_CHECK "Perform static check" OFF) -option(IMVIEW_WITH_GLAD "Integrate glad into imview" ON) ## Check if code compiles on x86_64 platform if (CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") diff --git a/README.md b/README.md index 7ec7515..1feb265 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ imview library in your own project, it's recommended to read this design documen ## Build -The code in this repository should build on any recent linux distributions with a compiler supporting C++11/14. +The code in this repository should build on any recent linux distributions with a compiler supporting C++11/14/17. Note +that the yoga library for layout management requires certain C++20 features, and you will need to disable automatic +layout feature if you are using an older compiler (such as the gcc that comes with Ubuntu 20.04 or older). **Setup toolchain** diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6046305..09d8d03 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,4 +9,6 @@ endif () add_subdirectory(third_party) # applications -add_subdirectory(app) +if (ENABLE_AUTO_LAYOUT) + add_subdirectory(app) +endif () diff --git a/src/app/panels/config_panel.cpp b/src/app/panels/config_panel.cpp index 93f31c6..70e1fd3 100644 --- a/src/app/panels/config_panel.cpp +++ b/src/app/panels/config_panel.cpp @@ -21,14 +21,45 @@ ConfigPanel::ConfigPanel(std::string name) : Panel(name) { void ConfigPanel::Draw() { Begin(); + ImGui::PushFont(Fonts::GetFont(FontSize::kFont16)); { - ImGui::PushFont(Fonts::GetFont(FontSize::kFont18)); - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 153, 153, 200)); - ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", - 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); - ImGui::PopStyleColor(); - ImGui::PopFont(); + if (ImGui::CollapsingHeader("Help")) { + ImGui::SeparatorText("ABOUT THIS DEMO:"); + ImGui::BulletText( + "Sections below are demonstrating many aspects of the library."); + ImGui::BulletText( + "The \"Examples\" menu above leads to more demo contents."); + ImGui::BulletText( + "The \"Tools\" menu above gives access to: About Box, Style Editor,\n" + "and Metrics/Debugger (general purpose Dear ImGui debugging tool)."); + + ImGui::SeparatorText("PROGRAMMER GUIDE:"); + ImGui::BulletText( + "See the ShowDemoWindow() code in imgui_demo.cpp. <- you are here!"); + ImGui::BulletText("See comments in imgui.cpp."); + ImGui::BulletText("See example applications in the examples/ folder."); + ImGui::BulletText("Read the FAQ at "); + ImGui::SameLine(0, 0); + ImGui::TextLinkOpenURL("https://www.dearimgui.com/faq/"); + ImGui::BulletText( + "Set 'io.ConfigFlags |= NavEnableKeyboard' for keyboard controls."); + ImGui::BulletText( + "Set 'io.ConfigFlags |= NavEnableGamepad' for gamepad controls."); + + ImGui::SeparatorText("USER GUIDE:"); + ImGui::ShowUserGuide(); + } + + // ImGui::PushFont(Fonts::GetFont(FontSize::kFont18)); + // ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 153, 153, 200)); + // ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", + // 1000.0f / ImGui::GetIO().Framerate, + // ImGui::GetIO().Framerate); + // ImGui::PopStyleColor(); + // ImGui::PopFont(); + // ImGui::Spacing(); } + ImGui::PopFont(); End(); } } // namespace quickviz \ No newline at end of file diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index ae9cb2b..edbc7d2 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -5,18 +5,22 @@ pkg_check_modules(Fontconfig REQUIRED IMPORTED_TARGET fontconfig) find_package(OpenCV REQUIRED) +if (ENABLE_AUTO_LAYOUT) + set(AUTO_LAYOUT_SRC + src/box.cpp + src/yoga_utils.cpp) + set(AUTO_LAYOUT_LIBS yogacore) +endif () + # add library add_library(imview - ## internal implementations - src/yoga_utils.cpp - ## public class # ui components src/window.cpp src/viewer.cpp src/fonts.cpp src/scene_object.cpp src/panel.cpp - src/box.cpp + ${AUTO_LAYOUT_SRC} src/layer.cpp # src/popup.cpp src/buffer/scrolling_plot_buffer.cpp @@ -37,7 +41,12 @@ add_library(imview src/event/event_dispatcher.cpp src/event/async_event_dispatcher.cpp ) -target_link_libraries(imview PUBLIC imcore yogacore PkgConfig::Cairo PkgConfig::Fontconfig ${OpenCV_LIBS}) +target_link_libraries(imview PUBLIC imcore + PkgConfig::Cairo PkgConfig::Fontconfig + ${AUTO_LAYOUT_LIBS} ${OpenCV_LIBS}) +if (ENABLE_AUTO_LAYOUT) + target_compile_definitions(imview PUBLIC ENABLE_AUTO_LAYOUT) +endif () if (IMVIEW_WITH_GLAD) target_link_libraries(imview PUBLIC glad) target_compile_definitions(imview PUBLIC IMVIEW_WITH_GLAD) diff --git a/src/imview/include/imview/interface/resizable.hpp b/src/imview/include/imview/interface/resizable.hpp index a647456..7c5a340 100644 --- a/src/imview/include/imview/interface/resizable.hpp +++ b/src/imview/include/imview/interface/resizable.hpp @@ -21,6 +21,7 @@ class Resizable { virtual void OnResize(float width, float height) = 0; // used for automatic layout only +#ifdef ENABLE_AUTO_LAYOUT virtual void SetAlignContent(Styling::AlignContent content) = 0; virtual void SetAlignItems(Styling::AlignItems items) = 0; virtual void SetAspectRatio(float aspect_ratio) = 0; @@ -44,6 +45,7 @@ class Resizable { virtual void SetMinHeight(float height) = 0; virtual void SetMaxWidth(float width) = 0; virtual void SetMaxHeight(float height) = 0; +#endif }; } // namespace quickviz diff --git a/src/imview/include/imview/scene_object.hpp b/src/imview/include/imview/scene_object.hpp index 6a93b6c..1032626 100644 --- a/src/imview/include/imview/scene_object.hpp +++ b/src/imview/include/imview/scene_object.hpp @@ -15,8 +15,10 @@ #include "imview/interface/resizable.hpp" #include "imview/interface/renderable.hpp" +#ifdef ENABLE_AUTO_LAYOUT struct YGNode; typedef struct YGNode* YGNodeRef; +#endif namespace quickviz { class SceneObject : public Resizable, public Renderable { @@ -36,6 +38,7 @@ class SceneObject : public Resizable, public Renderable { bool IsVisible() const override { return visible_; } // used for automatic layout only +#ifdef ENABLE_AUTO_LAYOUT YGNodeRef GetYogaNode() { return yg_node_; } void SetAlignContent(Styling::AlignContent content) override; void SetAlignItems(Styling::AlignItems items) override; @@ -60,6 +63,7 @@ class SceneObject : public Resizable, public Renderable { void SetMinHeight(float height) override; void SetMaxWidth(float width) override; void SetMaxHeight(float height) override; +#endif protected: std::string name_; @@ -70,8 +74,10 @@ class SceneObject : public Resizable, public Renderable { float width_ = 0; float height_ = 0; +#ifdef ENABLE_AUTO_LAYOUT YGNodeRef yg_node_; size_t child_count_ = 0; +#endif }; } // namespace quickviz diff --git a/src/imview/src/layer.cpp b/src/imview/src/layer.cpp index 308cc8f..140302b 100644 --- a/src/imview/src/layer.cpp +++ b/src/imview/src/layer.cpp @@ -10,7 +10,9 @@ #include +#ifdef ENABLE_AUTO_LAYOUT #include +#endif namespace quickviz { Layer::Layer(std::string name) : SceneObject(name) {} @@ -18,9 +20,11 @@ Layer::Layer(std::string name) : SceneObject(name) {} void Layer::AddChild(std::shared_ptr obj) { if (obj == nullptr) return; children_[obj->GetName()] = obj; +#ifdef ENABLE_AUTO_LAYOUT auto idx = YGNodeGetChildCount(yg_node_); child_name_by_index_[idx] = obj->GetName(); YGNodeInsertChild(yg_node_, obj->GetYogaNode(), idx); +#endif } void Layer::RemoveChild(const std::string& name) { @@ -32,7 +36,9 @@ void Layer::RemoveChild(const std::string& name) { } auto it = children_.find(name); if (it != children_.end()) { +#ifdef ENABLE_AUTO_LAYOUT YGNodeRemoveChild(yg_node_, it->second->GetYogaNode()); +#endif children_.erase(it); } } diff --git a/src/imview/src/scene_object.cpp b/src/imview/src/scene_object.cpp index cbeba40..0b5d5d8 100644 --- a/src/imview/src/scene_object.cpp +++ b/src/imview/src/scene_object.cpp @@ -8,18 +8,25 @@ #include "imview/scene_object.hpp" +#ifdef ENABLE_AUTO_LAYOUT #include - #include "yoga_utils.hpp" -namespace quickviz { -using namespace YogaUtils; +using namespace quickviz::YogaUtils; +#endif +namespace quickviz { SceneObject::SceneObject(std::string name) : name_(std::move(name)) { +#ifdef ENABLE_AUTO_LAYOUT yg_node_ = YGNodeNew(); +#endif } -SceneObject::~SceneObject() { YGNodeFreeRecursive(yg_node_); } +SceneObject::~SceneObject() { +#ifdef ENABLE_AUTO_LAYOUT + YGNodeFreeRecursive(yg_node_); +#endif +} void SceneObject::SetPosition(float x, float y) { x_ = x; @@ -31,6 +38,7 @@ void SceneObject::OnResize(float width, float height) { height_ = height; } +#ifdef ENABLE_AUTO_LAYOUT void SceneObject::SetAlignContent(Styling::AlignContent align) { YGNodeStyleSetAlignContent(yg_node_, ToYogaAlign(align)); } @@ -122,4 +130,5 @@ void SceneObject::SetMaxWidth(float width) { void SceneObject::SetMaxHeight(float height) { YGNodeStyleSetMaxHeight(yg_node_, height); } +#endif } // namespace quickviz \ No newline at end of file diff --git a/src/imview/test/CMakeLists.txt b/src/imview/test/CMakeLists.txt index a8d721b..613d684 100644 --- a/src/imview/test/CMakeLists.txt +++ b/src/imview/test/CMakeLists.txt @@ -8,8 +8,10 @@ add_subdirectory(unit_test) add_executable(test_window_gl_triangle test_window_gl_triangle.cpp) target_link_libraries(test_window_gl_triangle PRIVATE imview) -add_executable(test_box test_box.cpp) -target_link_libraries(test_box PRIVATE imview) +if (ENABLE_AUTO_LAYOUT) + add_executable(test_box test_box.cpp) + target_link_libraries(test_box PRIVATE imview) +endif () add_executable(test_event test_event.cpp) target_link_libraries(test_event PRIVATE imview) diff --git a/src/imview/test/feature/CMakeLists.txt b/src/imview/test/feature/CMakeLists.txt index d71e979..32b20f5 100644 --- a/src/imview/test/feature/CMakeLists.txt +++ b/src/imview/test/feature/CMakeLists.txt @@ -1,8 +1,10 @@ add_executable(test_window test_window.cpp) target_link_libraries(test_window PRIVATE imview) -add_executable(test_viewer test_viewer.cpp) -target_link_libraries(test_viewer PRIVATE imview) +if (ENABLE_AUTO_LAYOUT) + add_executable(test_viewer test_viewer.cpp) + target_link_libraries(test_viewer PRIVATE imview) +endif () add_executable(test_cv_image_widget test_cv_image_widget.cpp) target_link_libraries(test_cv_image_widget PRIVATE imview) diff --git a/src/third_party/CMakeLists.txt b/src/third_party/CMakeLists.txt index fa08a3e..dbcdb76 100644 --- a/src/third_party/CMakeLists.txt +++ b/src/third_party/CMakeLists.txt @@ -1,5 +1,7 @@ # external libraries -add_subdirectory(yoga) +if (ENABLE_AUTO_LAYOUT) + add_subdirectory(yoga) +endif () add_subdirectory(glad) add_subdirectory(imcore) From 59ba6fceaf26029e4db04a5313a479d153899baf Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sat, 2 Nov 2024 17:05:54 +0800 Subject: [PATCH 07/20] cmake: fixed dl lib dependency issue --- CMakeLists.txt | 4 ++++ src/imview/CMakeLists.txt | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4812732..46b2180 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,10 @@ if(QUICKVIZ_DEV_MODE) message(STATUS "quickviz development mode enabled") endif() +if(ENABLE_AUTO_LAYOUT) + message(STATUS "Auto layout enabled") +endif() + # Build tests if (((NOT BUILD_AS_MODULE) AND BUILD_TESTING) OR QUICKVIZ_DEV_MODE) set(BUILD_TESTING ON) diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index edbc7d2..4584055 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -2,7 +2,7 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(Cairo REQUIRED IMPORTED_TARGET cairo) pkg_check_modules(Fontconfig REQUIRED IMPORTED_TARGET fontconfig) - +find_package(OpenGL REQUIRED) find_package(OpenCV REQUIRED) if (ENABLE_AUTO_LAYOUT) @@ -43,6 +43,7 @@ add_library(imview ) target_link_libraries(imview PUBLIC imcore PkgConfig::Cairo PkgConfig::Fontconfig + OpenGL::OpenGL ${CMAKE_DL_LIBS} ${AUTO_LAYOUT_LIBS} ${OpenCV_LIBS}) if (ENABLE_AUTO_LAYOUT) target_compile_definitions(imview PUBLIC ENABLE_AUTO_LAYOUT) From 51e9008b3145352a6008b9193a3476b222bf55f5 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sat, 2 Nov 2024 17:14:58 +0800 Subject: [PATCH 08/20] cmake: fix dl linking issue for glad --- src/imview/CMakeLists.txt | 2 +- src/third_party/glad/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index 4584055..df7629c 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -43,7 +43,7 @@ add_library(imview ) target_link_libraries(imview PUBLIC imcore PkgConfig::Cairo PkgConfig::Fontconfig - OpenGL::OpenGL ${CMAKE_DL_LIBS} + OpenGL::OpenGL ${AUTO_LAYOUT_LIBS} ${OpenCV_LIBS}) if (ENABLE_AUTO_LAYOUT) target_compile_definitions(imview PUBLIC ENABLE_AUTO_LAYOUT) diff --git a/src/third_party/glad/CMakeLists.txt b/src/third_party/glad/CMakeLists.txt index 96c27e2..5d6558b 100644 --- a/src/third_party/glad/CMakeLists.txt +++ b/src/third_party/glad/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(glad STATIC src/glad.c) +target_link_libraries(glad PUBLIC ${CMAKE_DL_LIBS}) target_include_directories(glad PUBLIC $ $ From bdec33fe19a1bae806d7aea663e5f051d235ee05 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sat, 2 Nov 2024 17:19:22 +0800 Subject: [PATCH 09/20] cmake: added threads dep --- src/imview/CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index df7629c..7f7a671 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -2,6 +2,8 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(Cairo REQUIRED IMPORTED_TARGET cairo) pkg_check_modules(Fontconfig REQUIRED IMPORTED_TARGET fontconfig) + +find_package(Threads REQUIRED) find_package(OpenGL REQUIRED) find_package(OpenCV REQUIRED) @@ -43,8 +45,10 @@ add_library(imview ) target_link_libraries(imview PUBLIC imcore PkgConfig::Cairo PkgConfig::Fontconfig + Threads::Threads OpenGL::OpenGL - ${AUTO_LAYOUT_LIBS} ${OpenCV_LIBS}) + ${AUTO_LAYOUT_LIBS} + ${OpenCV_LIBS}) if (ENABLE_AUTO_LAYOUT) target_compile_definitions(imview PUBLIC ENABLE_AUTO_LAYOUT) endif () From 9ffce1697f9f9317ac712dbd0520622bd04c82e6 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sat, 2 Nov 2024 20:16:02 +0800 Subject: [PATCH 10/20] [wip] shaders --- src/imview/CMakeLists.txt | 5 ++- .../include/imview/widget/details/shader.hpp | 44 +++++++++++++++++++ src/imview/src/widget/details/shader.cpp | 11 +++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/imview/include/imview/widget/details/shader.hpp create mode 100644 src/imview/src/widget/details/shader.cpp diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index 7f7a671..fa93889 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -22,10 +22,9 @@ add_library(imview src/fonts.cpp src/scene_object.cpp src/panel.cpp - ${AUTO_LAYOUT_SRC} src/layer.cpp + ${AUTO_LAYOUT_SRC} # src/popup.cpp - src/buffer/scrolling_plot_buffer.cpp # utils src/utils/image_utils.cpp # widgets @@ -37,8 +36,10 @@ add_library(imview src/widget/rt_line_plot_widget.cpp src/widget/gl_scene_widget.cpp src/widget/details/gl_frame_buffer.cpp + src/widget/details/shader.cpp # data buffer src/buffer/buffer_registry.cpp + src/buffer/scrolling_plot_buffer.cpp # event handling src/event/event_dispatcher.cpp src/event/async_event_dispatcher.cpp diff --git a/src/imview/include/imview/widget/details/shader.hpp b/src/imview/include/imview/widget/details/shader.hpp new file mode 100644 index 0000000..1b3f8eb --- /dev/null +++ b/src/imview/include/imview/widget/details/shader.hpp @@ -0,0 +1,44 @@ +/* + * @file shader.hpp + * @date 11/2/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef XMOTION_SHADER_HPP +#define XMOTION_SHADER_HPP + +#include +#include + +#include + +#include "glad/glad.h" + +namespace quickviz { +class Shader { + public: + enum class Type { + kUnknown = 0, + kVertex, + kFragment, + }; + + public: + Shader(const std::string& source, Type type); + ~Shader(); + + // public methods + void Compile(); + GLuint GetShaderID() const { return shader_id_; } + + private: + std::string LoadSourceFile(const std::string& file_path); + + GLuint shader_id_; + Type type_; +}; +} // namespace quickviz + +#endif // XMOTION_SHADER_HPP \ No newline at end of file diff --git a/src/imview/src/widget/details/shader.cpp b/src/imview/src/widget/details/shader.cpp new file mode 100644 index 0000000..cedcbd0 --- /dev/null +++ b/src/imview/src/widget/details/shader.cpp @@ -0,0 +1,11 @@ +/* + * @file shader.cpp + * @date 11/2/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/widget/details/shader.hpp" + +namespace quickviz {} // namespace quickviz \ No newline at end of file From 318a070aca307cae4a15d60dba481eba97a1d885 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sat, 2 Nov 2024 22:45:02 +0800 Subject: [PATCH 11/20] component: added shader and shader_program class --- .github/workflows/default.yml | 4 +- data/glsl/fragment_shader.glsl | 6 ++ data/glsl/vertex_shader.glsl | 6 ++ src/app/panels/main_docking_panel.hpp | 2 +- src/imview/CMakeLists.txt | 16 ++-- .../details => component}/cairo_context.hpp | 0 .../details => component}/cairo_draw.hpp | 0 .../frame_buffer.hpp} | 22 ++--- .../{utils => component}/image_utils.hpp | 0 .../{widget/details => component}/shader.hpp | 22 +++-- .../imview/component/shader_program.hpp | 45 +++++++++ .../include/imview/widget/cairo_widget.hpp | 4 +- .../include/imview/widget/gl_scene_widget.hpp | 35 ------- .../include/imview/widget/gl_widget.hpp | 35 +++++++ .../details => component}/cairo_context.cpp | 2 +- .../details => component}/cairo_draw.cpp | 2 +- .../frame_buffer.cpp} | 20 ++-- .../src/{utils => component}/image_utils.cpp | 2 +- src/imview/src/component/shader.cpp | 75 +++++++++++++++ src/imview/src/component/shader_program.cpp | 75 +++++++++++++++ .../src/widget/buffered_cv_image_widget.cpp | 2 +- src/imview/src/widget/cv_image_widget.cpp | 2 +- src/imview/src/widget/details/shader.cpp | 11 --- .../{gl_scene_widget.cpp => gl_widget.cpp} | 12 +-- src/imview/src/window.cpp | 4 +- src/imview/test/CMakeLists.txt | 3 + .../test/feature/test_gl_scene_widget.cpp | 6 +- src/imview/test/test_framebuffer.cpp | 5 +- src/imview/test/test_shader.cpp | 91 +++++++++++++++++++ 29 files changed, 402 insertions(+), 107 deletions(-) create mode 100644 data/glsl/fragment_shader.glsl create mode 100644 data/glsl/vertex_shader.glsl rename src/imview/include/imview/{widget/details => component}/cairo_context.hpp (100%) rename src/imview/include/imview/{widget/details => component}/cairo_draw.hpp (100%) rename src/imview/include/imview/{widget/details/gl_frame_buffer.hpp => component/frame_buffer.hpp} (63%) rename src/imview/include/imview/{utils => component}/image_utils.hpp (100%) rename src/imview/include/imview/{widget/details => component}/shader.hpp (56%) create mode 100644 src/imview/include/imview/component/shader_program.hpp delete mode 100644 src/imview/include/imview/widget/gl_scene_widget.hpp create mode 100644 src/imview/include/imview/widget/gl_widget.hpp rename src/imview/src/{widget/details => component}/cairo_context.cpp (98%) rename src/imview/src/{widget/details => component}/cairo_draw.cpp (98%) rename src/imview/src/{widget/details/gl_frame_buffer.cpp => component/frame_buffer.cpp} (81%) rename src/imview/src/{utils => component}/image_utils.cpp (96%) create mode 100644 src/imview/src/component/shader.cpp create mode 100644 src/imview/src/component/shader_program.cpp delete mode 100644 src/imview/src/widget/details/shader.cpp rename src/imview/src/widget/{gl_scene_widget.cpp => gl_widget.cpp} (74%) create mode 100644 src/imview/test/test_shader.cpp diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 5ffaf5a..cf89464 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -20,7 +20,9 @@ jobs: - name: Checkout submodules run: git submodule update --init --recursive - name: Install Dependencies - run: sudo apt-get update && sudo apt-get install -y libgl1-mesa-dev libglfw3-dev libcairo2-dev libopencv-dev + run: | + sudo apt-get update && sudo apt-get install -y libgl1-mesa-dev \ + libglfw3-dev libcairo2-dev libopencv-dev libglm-dev - name: Create Build Environment run: cmake -E make_directory ${{runner.workspace}}/build - name: Configure CMake (Ubuntu 20.04) diff --git a/data/glsl/fragment_shader.glsl b/data/glsl/fragment_shader.glsl new file mode 100644 index 0000000..07d1335 --- /dev/null +++ b/data/glsl/fragment_shader.glsl @@ -0,0 +1,6 @@ +#version 330 core +out vec4 FragColor; +void main() +{ + FragColor = vec4(0.2, 0.5, 0.8, 1.0); +} diff --git a/data/glsl/vertex_shader.glsl b/data/glsl/vertex_shader.glsl new file mode 100644 index 0000000..75215ea --- /dev/null +++ b/data/glsl/vertex_shader.glsl @@ -0,0 +1,6 @@ +#version 330 core +layout (location = 0) in vec3 aPos; +void main() +{ + gl_Position = vec4(aPos, 1.0); +} diff --git a/src/app/panels/main_docking_panel.hpp b/src/app/panels/main_docking_panel.hpp index 615627d..1d51b0a 100644 --- a/src/app/panels/main_docking_panel.hpp +++ b/src/app/panels/main_docking_panel.hpp @@ -11,7 +11,7 @@ #include "imview/panel.hpp" -#include "imview/widget/gl_scene_widget.hpp" +#include "imview/widget/gl_widget.hpp" #include "panels/menu_bar.hpp" #include "panels/config_panel.hpp" diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index fa93889..55dd257 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -24,19 +24,19 @@ add_library(imview src/panel.cpp src/layer.cpp ${AUTO_LAYOUT_SRC} - # src/popup.cpp - # utils - src/utils/image_utils.cpp # widgets src/widget/cv_image_widget.cpp src/widget/buffered_cv_image_widget.cpp src/widget/cairo_widget.cpp - src/widget/details/cairo_context.cpp - src/widget/details/cairo_draw.cpp src/widget/rt_line_plot_widget.cpp - src/widget/gl_scene_widget.cpp - src/widget/details/gl_frame_buffer.cpp - src/widget/details/shader.cpp + src/widget/gl_widget.cpp + # components + src/component/cairo_context.cpp + src/component/cairo_draw.cpp + src/component/image_utils.cpp + src/component/shader.cpp + src/component/shader_program.cpp + src/component/frame_buffer.cpp # data buffer src/buffer/buffer_registry.cpp src/buffer/scrolling_plot_buffer.cpp diff --git a/src/imview/include/imview/widget/details/cairo_context.hpp b/src/imview/include/imview/component/cairo_context.hpp similarity index 100% rename from src/imview/include/imview/widget/details/cairo_context.hpp rename to src/imview/include/imview/component/cairo_context.hpp diff --git a/src/imview/include/imview/widget/details/cairo_draw.hpp b/src/imview/include/imview/component/cairo_draw.hpp similarity index 100% rename from src/imview/include/imview/widget/details/cairo_draw.hpp rename to src/imview/include/imview/component/cairo_draw.hpp diff --git a/src/imview/include/imview/widget/details/gl_frame_buffer.hpp b/src/imview/include/imview/component/frame_buffer.hpp similarity index 63% rename from src/imview/include/imview/widget/details/gl_frame_buffer.hpp rename to src/imview/include/imview/component/frame_buffer.hpp index 1ac8758..a97205a 100644 --- a/src/imview/include/imview/widget/details/gl_frame_buffer.hpp +++ b/src/imview/include/imview/component/frame_buffer.hpp @@ -1,27 +1,27 @@ /* - * @file gl_frame_buffer.hpp + * @file frame_buffer.hpp * @date 10/29/24 * @brief * * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#ifndef XMOTION_GL_FRAME_BUFFER_HPP -#define XMOTION_GL_FRAME_BUFFER_HPP +#ifndef XMOTION_FRAME_BUFFER_HPP +#define XMOTION_FRAME_BUFFER_HPP #include namespace quickviz { -class GlFrameBuffer { +class FrameBuffer { public: - GlFrameBuffer(uint32_t width, uint32_t height); - ~GlFrameBuffer(); + FrameBuffer(uint32_t width, uint32_t height); + ~FrameBuffer(); // do not allow copy or move - GlFrameBuffer(const GlFrameBuffer&) = delete; - GlFrameBuffer& operator=(const GlFrameBuffer&) = delete; - GlFrameBuffer(GlFrameBuffer&&) = delete; - GlFrameBuffer& operator=(GlFrameBuffer&&) = delete; + FrameBuffer(const FrameBuffer&) = delete; + FrameBuffer& operator=(const FrameBuffer&) = delete; + FrameBuffer(FrameBuffer&&) = delete; + FrameBuffer& operator=(FrameBuffer&&) = delete; // public methods void Bind(bool keep_aspect_ratio = true) const; @@ -45,4 +45,4 @@ class GlFrameBuffer { }; } // namespace quickviz -#endif // XMOTION_GL_FRAME_BUFFER_HPP \ No newline at end of file +#endif // XMOTION_FRAME_BUFFER_HPP \ No newline at end of file diff --git a/src/imview/include/imview/utils/image_utils.hpp b/src/imview/include/imview/component/image_utils.hpp similarity index 100% rename from src/imview/include/imview/utils/image_utils.hpp rename to src/imview/include/imview/component/image_utils.hpp diff --git a/src/imview/include/imview/widget/details/shader.hpp b/src/imview/include/imview/component/shader.hpp similarity index 56% rename from src/imview/include/imview/widget/details/shader.hpp rename to src/imview/include/imview/component/shader.hpp index 1b3f8eb..a329006 100644 --- a/src/imview/include/imview/widget/details/shader.hpp +++ b/src/imview/include/imview/component/shader.hpp @@ -9,35 +9,39 @@ #ifndef XMOTION_SHADER_HPP #define XMOTION_SHADER_HPP +#include #include #include -#include - -#include "glad/glad.h" - namespace quickviz { class Shader { public: - enum class Type { + enum class Type : uint32_t { kUnknown = 0, kVertex, kFragment, }; public: - Shader(const std::string& source, Type type); + Shader(const std::string& source_file, Type type); ~Shader(); + // do not allow copy + Shader(const Shader&) = delete; + Shader& operator=(const Shader&) = delete; + // public methods - void Compile(); - GLuint GetShaderID() const { return shader_id_; } + void Print() const; + bool Compile(); + uint32_t GetShaderID() const { return shader_id_; } private: std::string LoadSourceFile(const std::string& file_path); - GLuint shader_id_; + std::string source_file_; Type type_; + std::string source_code_; + uint32_t shader_id_; }; } // namespace quickviz diff --git a/src/imview/include/imview/component/shader_program.hpp b/src/imview/include/imview/component/shader_program.hpp new file mode 100644 index 0000000..ef3afd2 --- /dev/null +++ b/src/imview/include/imview/component/shader_program.hpp @@ -0,0 +1,45 @@ +/* + * @file shader_program.hpp + * @date 11/2/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef QUICKVIZ_SHADER_PROGRAM_HPP +#define QUICKVIZ_SHADER_PROGRAM_HPP + +#include + +#include +#include + +#include "imview/component/shader.hpp" + +namespace quickviz { +class ShaderProgram { + public: + ShaderProgram(); + ~ShaderProgram(); + + // public methods + void AttachShader(const Shader& shader); + bool LinkProgram(); + void Use() const; + + // Uniform setting functions + void SetUniform(const std::string& name, bool value); + void SetUniform(const std::string& name, int value); + void SetUniform(const std::string& name, float value); + void SetUniform(const std::string& name, const glm::vec3& vector); + void SetUniform(const std::string& name, const glm::mat4& matrix); + + private: + uint32_t GetUniformLocation(const std::string& name); + + uint32_t program_id_; + std::unordered_map uniform_location_cache_; +}; +} // namespace quickviz + +#endif // QUICKVIZ_SHADER_PROGRAM_HPP \ No newline at end of file diff --git a/src/imview/include/imview/widget/cairo_widget.hpp b/src/imview/include/imview/widget/cairo_widget.hpp index 7422f34..c556426 100644 --- a/src/imview/include/imview/widget/cairo_widget.hpp +++ b/src/imview/include/imview/widget/cairo_widget.hpp @@ -21,8 +21,8 @@ #include "imgui.h" #include "imview/panel.hpp" -#include "imview/widget/details/cairo_context.hpp" -#include "imview/widget/details/cairo_draw.hpp" +#include "imview/component/cairo_context.hpp" +#include "imview/component/cairo_draw.hpp" namespace quickviz { class CairoWidget : public Panel { diff --git a/src/imview/include/imview/widget/gl_scene_widget.hpp b/src/imview/include/imview/widget/gl_scene_widget.hpp deleted file mode 100644 index d3d791b..0000000 --- a/src/imview/include/imview/widget/gl_scene_widget.hpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * @file gl_scene_widget.hpp - * @date 10/29/24 - * @brief - * - * @copyright Copyright (c) 2024 Ruixiang Du (rdu) - */ - -#ifndef XMOTION_GL_SCENE_WIDGET_HPP -#define XMOTION_GL_SCENE_WIDGET_HPP - -#include -#include - -#include "imview/panel.hpp" -#include "imview/widget/details/gl_frame_buffer.hpp" - -namespace quickviz { -class GlSceneWidget : public Panel { - public: - GlSceneWidget(const std::string& widget_name); - ~GlSceneWidget() = default; - - // public methods - using GlRenderFunction = std::function; - void SetGlRenderFunction(GlRenderFunction func); - void Draw() override; - - private: - GlRenderFunction render_function_; - std::unique_ptr frame_buffer_; -}; -} // namespace quickviz - -#endif // XMOTION_GL_SCENE_WIDGET_HPP \ No newline at end of file diff --git a/src/imview/include/imview/widget/gl_widget.hpp b/src/imview/include/imview/widget/gl_widget.hpp new file mode 100644 index 0000000..5680434 --- /dev/null +++ b/src/imview/include/imview/widget/gl_widget.hpp @@ -0,0 +1,35 @@ +/* + * @file gl_widget.hpp + * @date 10/29/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef XMOTION_GL_WIDGET_HPP +#define XMOTION_GL_WIDGET_HPP + +#include +#include + +#include "imview/panel.hpp" +#include "imview/component/frame_buffer.hpp" + +namespace quickviz { +class GlWidget : public Panel { + public: + GlWidget(const std::string& widget_name); + ~GlWidget() = default; + + // public methods + using GlRenderFunction = std::function; + void SetGlRenderFunction(GlRenderFunction func); + void Draw() override; + + private: + GlRenderFunction render_function_; + std::unique_ptr frame_buffer_; +}; +} // namespace quickviz + +#endif // XMOTION_GL_WIDGET_HPP \ No newline at end of file diff --git a/src/imview/src/widget/details/cairo_context.cpp b/src/imview/src/component/cairo_context.cpp similarity index 98% rename from src/imview/src/widget/details/cairo_context.cpp rename to src/imview/src/component/cairo_context.cpp index 78b76bf..d4dccfb 100644 --- a/src/imview/src/widget/details/cairo_context.cpp +++ b/src/imview/src/component/cairo_context.cpp @@ -7,7 +7,7 @@ * Copyright (c) 2021 Ruixiang Du (rdu) */ -#include "imview/widget/details/cairo_context.hpp" +#include "imview/component/cairo_context.hpp" #include diff --git a/src/imview/src/widget/details/cairo_draw.cpp b/src/imview/src/component/cairo_draw.cpp similarity index 98% rename from src/imview/src/widget/details/cairo_draw.cpp rename to src/imview/src/component/cairo_draw.cpp index 2f5f3b0..1b18325 100644 --- a/src/imview/src/widget/details/cairo_draw.cpp +++ b/src/imview/src/component/cairo_draw.cpp @@ -7,7 +7,7 @@ * Copyright (c) 2021 Ruixiang Du (rdu) */ -#include "imview/widget/details/cairo_draw.hpp" +#include "imview/component/cairo_draw.hpp" #include diff --git a/src/imview/src/widget/details/gl_frame_buffer.cpp b/src/imview/src/component/frame_buffer.cpp similarity index 81% rename from src/imview/src/widget/details/gl_frame_buffer.cpp rename to src/imview/src/component/frame_buffer.cpp index 09578c7..d154192 100644 --- a/src/imview/src/widget/details/gl_frame_buffer.cpp +++ b/src/imview/src/component/frame_buffer.cpp @@ -1,19 +1,19 @@ /* - * @file gl_frame_buffer.cpp + * @file frame_buffer.cpp * @date 10/29/24 * @brief * * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/widget/details/gl_frame_buffer.hpp" +#include "imview/component/frame_buffer.hpp" #include #include "glad/glad.h" namespace quickviz { -GlFrameBuffer::GlFrameBuffer(uint32_t width, uint32_t height) +FrameBuffer::FrameBuffer(uint32_t width, uint32_t height) : width_(width), height_(height), frame_buffer_(0), @@ -22,9 +22,9 @@ GlFrameBuffer::GlFrameBuffer(uint32_t width, uint32_t height) CreateBuffers(); } -GlFrameBuffer::~GlFrameBuffer() { DestroyBuffers(); } +FrameBuffer::~FrameBuffer() { DestroyBuffers(); } -void GlFrameBuffer::CreateBuffers() { +void FrameBuffer::CreateBuffers() { // generate and bind the framebuffer glGenFramebuffers(1, &frame_buffer_); glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_); @@ -54,7 +54,7 @@ void GlFrameBuffer::CreateBuffers() { glBindFramebuffer(GL_FRAMEBUFFER, 0); } -void GlFrameBuffer::DestroyBuffers() { +void FrameBuffer::DestroyBuffers() { glDeleteFramebuffers(1, &frame_buffer_); glDeleteTextures(1, &texture_buffer_); glDeleteRenderbuffers(1, &render_buffer_); @@ -63,7 +63,7 @@ void GlFrameBuffer::DestroyBuffers() { render_buffer_ = 0; } -void GlFrameBuffer::Bind(bool keep_aspect_ratio) const { +void FrameBuffer::Bind(bool keep_aspect_ratio) const { glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_); if (keep_aspect_ratio) { float square_size = std::min(width_, height_); @@ -76,14 +76,14 @@ void GlFrameBuffer::Bind(bool keep_aspect_ratio) const { glEnable(GL_DEPTH_TEST); } -void GlFrameBuffer::Unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } +void FrameBuffer::Unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } -void GlFrameBuffer::Clear(float r, float g, float b, float a) const { +void FrameBuffer::Clear(float r, float g, float b, float a) const { glClearColor(r, g, b, a); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } -void GlFrameBuffer::Resize(uint32_t width, uint32_t height) { +void FrameBuffer::Resize(uint32_t width, uint32_t height) { if (width == width_ && height == height_) return; width_ = width; diff --git a/src/imview/src/utils/image_utils.cpp b/src/imview/src/component/image_utils.cpp similarity index 96% rename from src/imview/src/utils/image_utils.cpp rename to src/imview/src/component/image_utils.cpp index 275d5bc..23e6ae6 100644 --- a/src/imview/src/utils/image_utils.cpp +++ b/src/imview/src/component/image_utils.cpp @@ -6,7 +6,7 @@ * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/utils/image_utils.hpp" +#include "imview/component/image_utils.hpp" namespace quickviz { cv::Mat GenerateRandomMat(int width, int height) { diff --git a/src/imview/src/component/shader.cpp b/src/imview/src/component/shader.cpp new file mode 100644 index 0000000..faf2880 --- /dev/null +++ b/src/imview/src/component/shader.cpp @@ -0,0 +1,75 @@ +/* + * @file shader.cpp + * @date 11/2/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/component/shader.hpp" + +#include +#include +#include + +#include "glad/glad.h" + +namespace quickviz { +namespace { +std::string LoadShaderSource(const std::string& file_path) { + std::ifstream file; + file.exceptions(std::ifstream::failbit | std::ifstream::badbit); + try { + file.open(file_path); + std::stringstream buffer; + buffer << file.rdbuf(); + file.close(); + return buffer.str(); + } catch (std::ifstream::failure e) { + std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ" << std::endl; + } + return ""; +} +} // namespace + +Shader::Shader(const std::string& source_file, Shader::Type type) + : source_file_(source_file), type_(type) { + source_code_ = LoadShaderSource(source_file_); + if (type_ == Shader::Type::kVertex) + shader_id_ = glCreateShader(GL_VERTEX_SHADER); + else if (type_ == Shader::Type::kFragment) + shader_id_ = glCreateShader(GL_FRAGMENT_SHADER); + const char* code = source_code_.c_str(); + glShaderSource(shader_id_, 1, &code, NULL); +} + +Shader::~Shader() { glDeleteShader(shader_id_); } + +void Shader::Print() const { + std::cout << "Shader source file: " << source_file_ << std::endl; + std::cout << "Shader type: " << static_cast(type_) << std::endl; + std::cout << "Shader source code: " << std::endl; + std::cout << "------ start ------" << std::endl; + std::cout << source_code_ << std::endl; + std::cout << "------ end ------" << std::endl; +} + +bool Shader::Compile() { + glCompileShader(shader_id_); + + GLint success; + GLchar infoLog[512]; + glGetShaderiv(shader_id_, GL_COMPILE_STATUS, &success); + if (!success) { + glGetShaderInfoLog(shader_id_, 512, NULL, infoLog); + std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" + << infoLog << std::endl; + return false; + } + return success; +} + +std::string Shader::LoadSourceFile(const std::string& file_path) { + return std::string(); +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/component/shader_program.cpp b/src/imview/src/component/shader_program.cpp new file mode 100644 index 0000000..ba2d617 --- /dev/null +++ b/src/imview/src/component/shader_program.cpp @@ -0,0 +1,75 @@ +/* + * @file shader_program.cpp + * @date 11/2/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/component/shader_program.hpp" + +#include + +#include "glad/glad.h" + +namespace quickviz { +ShaderProgram::ShaderProgram() { program_id_ = glCreateProgram(); } + +ShaderProgram::~ShaderProgram() { glDeleteProgram(program_id_); } + +void ShaderProgram::AttachShader(const Shader& shader) { + glAttachShader(program_id_, shader.GetShaderID()); +} + +bool ShaderProgram::LinkProgram() { + glLinkProgram(program_id_); + GLint success; + glGetProgramiv(program_id_, GL_LINK_STATUS, &success); + if (!success) { + GLchar infoLog[512]; + glGetProgramInfoLog(program_id_, 512, NULL, infoLog); + std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" + << infoLog << std::endl; + return false; + } + return true; +} + +void ShaderProgram::Use() const { glUseProgram(program_id_); } + +void ShaderProgram::SetUniform(const std::string& name, bool value) { + glUniform1i(GetUniformLocation(name), static_cast(value)); +} + +void ShaderProgram::SetUniform(const std::string& name, int value) { + glUniform1i(GetUniformLocation(name), value); +} + +void ShaderProgram::SetUniform(const std::string& name, float value) { + glUniform1f(GetUniformLocation(name), value); +} + +void ShaderProgram::SetUniform(const std::string& name, + const glm::vec3& vector) { + glUniform3fv(GetUniformLocation(name), 1, &vector[0]); +} + +void ShaderProgram::SetUniform(const std::string& name, + const glm::mat4& matrix) { + glUniformMatrix4fv(GetUniformLocation(name), 1, GL_FALSE, &matrix[0][0]); +} + +uint32_t ShaderProgram::GetUniformLocation(const std::string& name) { + // Use cache if location already retrieved + if (uniform_location_cache_.find(name) != uniform_location_cache_.end()) + return uniform_location_cache_[name]; + + // Query location and cache it + GLint location = glGetUniformLocation(program_id_, name.c_str()); + if (location == -1) { + throw std::runtime_error("Trying to access non-existent uniform: " + name); + } + uniform_location_cache_[name] = location; + return location; +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/widget/buffered_cv_image_widget.cpp b/src/imview/src/widget/buffered_cv_image_widget.cpp index eeeb833..78f9b54 100644 --- a/src/imview/src/widget/buffered_cv_image_widget.cpp +++ b/src/imview/src/widget/buffered_cv_image_widget.cpp @@ -8,7 +8,7 @@ #include "imview/widget/buffered_cv_image_widget.hpp" -#include "imview/utils/image_utils.hpp" +#include "imview/component/image_utils.hpp" namespace quickviz { BufferedCvImageWidget::BufferedCvImageWidget(const std::string& widget_name, diff --git a/src/imview/src/widget/cv_image_widget.cpp b/src/imview/src/widget/cv_image_widget.cpp index d080851..d019423 100644 --- a/src/imview/src/widget/cv_image_widget.cpp +++ b/src/imview/src/widget/cv_image_widget.cpp @@ -9,7 +9,7 @@ #include "imview/widget/cv_image_widget.hpp" #include "glad/glad.h" -#include "imview/utils/image_utils.hpp" +#include "imview/component/image_utils.hpp" namespace quickviz { CvImageWidget::CvImageWidget(const std::string& widget_name) diff --git a/src/imview/src/widget/details/shader.cpp b/src/imview/src/widget/details/shader.cpp deleted file mode 100644 index cedcbd0..0000000 --- a/src/imview/src/widget/details/shader.cpp +++ /dev/null @@ -1,11 +0,0 @@ -/* - * @file shader.cpp - * @date 11/2/24 - * @brief - * - * @copyright Copyright (c) 2024 Ruixiang Du (rdu) - */ - -#include "imview/widget/details/shader.hpp" - -namespace quickviz {} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/widget/gl_scene_widget.cpp b/src/imview/src/widget/gl_widget.cpp similarity index 74% rename from src/imview/src/widget/gl_scene_widget.cpp rename to src/imview/src/widget/gl_widget.cpp index a56d3af..b760c5c 100644 --- a/src/imview/src/widget/gl_scene_widget.cpp +++ b/src/imview/src/widget/gl_widget.cpp @@ -1,28 +1,28 @@ /* - * @file gl_scene_widget.cpp + * @file gl_widget.cpp * @date 10/29/24 * @brief * * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/widget/gl_scene_widget.hpp" +#include "imview/widget/gl_widget.hpp" namespace quickviz { -GlSceneWidget::GlSceneWidget(const std::string& widget_name) +GlWidget::GlWidget(const std::string& widget_name) : Panel(widget_name) { this->SetAutoLayout(false); this->SetWindowNoMenuButton(); this->SetNoBackground(true); - frame_buffer_ = std::make_unique(100, 50); + frame_buffer_ = std::make_unique(100, 50); } -void GlSceneWidget::SetGlRenderFunction(GlSceneWidget::GlRenderFunction func) { +void GlWidget::SetGlRenderFunction(GlWidget::GlRenderFunction func) { render_function_ = func; } -void GlSceneWidget::Draw() { +void GlWidget::Draw() { Begin(); { ImVec2 contentSize = ImGui::GetContentRegionAvail(); diff --git a/src/imview/src/window.cpp b/src/imview/src/window.cpp index 704e1ad..dba6ae3 100644 --- a/src/imview/src/window.cpp +++ b/src/imview/src/window.cpp @@ -66,9 +66,9 @@ void Window::ApplyWindowHints(uint32_t window_hints) { glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac #else - // GL 3.0 + GLSL 130 + // GL 3.3 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only // glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only #endif diff --git a/src/imview/test/CMakeLists.txt b/src/imview/test/CMakeLists.txt index 613d684..c83ede2 100644 --- a/src/imview/test/CMakeLists.txt +++ b/src/imview/test/CMakeLists.txt @@ -22,6 +22,9 @@ target_link_libraries(test_async_event PRIVATE imview) add_executable(test_framebuffer test_framebuffer.cpp) target_link_libraries(test_framebuffer PRIVATE imview) +add_executable(test_shader test_shader.cpp) +target_link_libraries(test_shader PRIVATE imview) + find_package(OpenCV QUIET) if (OpenCV_FOUND) add_executable(test_double_buffer test_double_buffer.cpp) diff --git a/src/imview/test/feature/test_gl_scene_widget.cpp b/src/imview/test/feature/test_gl_scene_widget.cpp index 0665377..9fb8ded 100644 --- a/src/imview/test/feature/test_gl_scene_widget.cpp +++ b/src/imview/test/feature/test_gl_scene_widget.cpp @@ -13,11 +13,11 @@ #include #include "imview/viewer.hpp" -#include "imview/widget/gl_scene_widget.hpp" +#include "imview/widget/gl_widget.hpp" using namespace quickviz; -void RenderGL(const GlFrameBuffer& frame_buffer) { +void RenderGL(const FrameBuffer& frame_buffer) { frame_buffer.Clear(); // Define shapes enclosed within a pair of glBegin and glEnd @@ -74,7 +74,7 @@ void RenderGL(const GlFrameBuffer& frame_buffer) { int main(int argc, char* argv[]) { Viewer viewer; - auto gl_widget = std::make_shared("OpenGL Scene"); + auto gl_widget = std::make_shared("OpenGL Scene"); gl_widget->OnResize(300, 200); gl_widget->SetPosition(0, 0); gl_widget->SetGlRenderFunction(RenderGL); diff --git a/src/imview/test/test_framebuffer.cpp b/src/imview/test/test_framebuffer.cpp index 9728186..6717262 100644 --- a/src/imview/test/test_framebuffer.cpp +++ b/src/imview/test/test_framebuffer.cpp @@ -7,12 +7,11 @@ * Copyright (c) 2021 Ruixiang Du (rdu) */ -#include #include #include "glad/glad.h" #include "imview/window.hpp" -#include "imview/widget/details/gl_frame_buffer.hpp" +#include "imview/component/frame_buffer.hpp" using namespace quickviz; @@ -22,7 +21,7 @@ int main(int argc, char* argv[]) { Window win("Test Window", width, height); // Create a framebuffer - auto frame_buffer = std::make_unique(width, height); + auto frame_buffer = std::make_unique(width, height); while (!win.ShouldClose()) { win.PollEvents(); diff --git a/src/imview/test/test_shader.cpp b/src/imview/test/test_shader.cpp new file mode 100644 index 0000000..e1b39d2 --- /dev/null +++ b/src/imview/test/test_shader.cpp @@ -0,0 +1,91 @@ +/* + * @file test_shader.cpp + * @date 11/2/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include + +#include "imview/window.hpp" +#include "imview/component/shader.hpp" +#include "imview/component/shader_program.hpp" + +using namespace quickviz; + +int main(int argc, char* argv[]) { + int width = 1920; + int height = 1080; + Window win("Test Window", width, height); + + Shader vertex_shader("../data/glsl/vertex_shader.glsl", + Shader::Type::kVertex); + // vertex_shader.Print(); + if (vertex_shader.Compile()) + std::cout << "Vertex shader compiled successfully" << std::endl; + else + std::cout << "Vertex shader compilation failed" << std::endl; + + Shader fragment_shader("../data/glsl/fragment_shader.glsl", + Shader::Type::kFragment); + if (fragment_shader.Compile()) + std::cout << "Fragment shader compiled successfully" << std::endl; + else + std::cout << "Fragment shader compilation failed" << std::endl; + + ShaderProgram shader_program; + shader_program.AttachShader(vertex_shader); + shader_program.AttachShader(fragment_shader); + if (shader_program.LinkProgram()) + std::cout << "Shader program linked successfully" << std::endl; + else + std::cout << "Shader program linking failed" << std::endl; + + ///////////////////////////////////////////////////////////////////////////// + + // data in main memory + float vertices[] = { + 0.0f, 0.5f, 0.0f, // Top vertex + -0.5f, -0.5f, 0.0f, // Bottom-left vertex + 0.5f, -0.5f, 0.0f // Bottom-right vertex + }; + + // transfer data to GPU + GLuint VBO, VAO; + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + + glBindVertexArray(VAO); + { + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), + (void*)0); + glEnableVertexAttribArray(0); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + glBindVertexArray(0); + + while (!win.ShouldClose()) { + win.PollEvents(); + + glViewport(0, 0, width, height); + + glClearColor(0.3, 0.3, 0.3, 0.5); + glClear(GL_COLOR_BUFFER_BIT); + + shader_program.Use(); + glBindVertexArray(VAO); + glDrawArrays(GL_TRIANGLES, 0, 3); + + win.SwapBuffers(); + } + + glDeleteVertexArrays(1, &VAO); + glDeleteBuffers(1, &VBO); + + return 0; +} \ No newline at end of file From c39c283d95997df0b98c1b60f4e2079bf9c984fa Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sun, 3 Nov 2024 00:12:43 +0800 Subject: [PATCH 12/20] updated code organization, started working on opengl 3d visualization --- docs/opengl/opengl_coordinate_frames.png | Bin 0 -> 153353 bytes docs/opengl/orthographic_projection.png | Bin 0 -> 43427 bytes docs/opengl/perspective_projection.png | Bin 0 -> 33294 bytes src/app/panels/main_docking_panel.cpp | 8 +- src/app/panels/main_docking_panel.hpp | 3 +- src/imview/CMakeLists.txt | 10 +- .../buffer/buffer_interface.hpp | 0 .../buffer/buffer_registry.hpp | 2 +- .../{ => component}/buffer/double_buffer.hpp | 2 +- .../{ => component}/buffer/ring_buffer.hpp | 2 +- .../buffer/scrolling_plot_buffer.hpp | 0 .../event/async_event_dispatcher.hpp | 4 +- .../event/async_event_emitter.hpp | 2 +- .../imview/{ => component}/event/event.hpp | 0 .../event/event_dispatcher.hpp | 2 +- .../{ => component}/event/event_emitter.hpp | 2 +- .../event/thread_safe_queue.hpp | 0 .../include/imview/component/shader.hpp | 3 +- src/imview/include/imview/primitive/grid.hpp | 41 +++++++ src/imview/include/imview/viewer.hpp | 1 + .../widget/buffered_cv_image_widget.hpp | 2 +- .../imview/widget/rt_line_plot_widget.hpp | 4 +- .../buffer/buffer_registry.cpp | 2 +- .../buffer/scrolling_plot_buffer.cpp | 2 +- .../event/async_event_dispatcher.cpp | 2 +- .../event/event_dispatcher.cpp | 2 +- src/imview/src/component/shader.cpp | 26 ++++- src/imview/src/primitive/grid.cpp | 104 ++++++++++++++++++ src/imview/src/viewer.cpp | 10 +- src/imview/test/CMakeLists.txt | 3 + .../feature/test_buffered_cv_image_widget.cpp | 6 +- .../test/feature/test_implot_widget.cpp | 4 +- src/imview/test/test_async_event.cpp | 6 +- src/imview/test/test_double_buffer.cpp | 2 +- src/imview/test/test_event.cpp | 6 +- src/imview/test/test_grid.cpp | 57 ++++++++++ src/imview/test/test_ring_buffer.cpp | 2 +- .../test/unit_test/utest_ring_buffer.cpp | 2 +- 38 files changed, 277 insertions(+), 47 deletions(-) create mode 100644 docs/opengl/opengl_coordinate_frames.png create mode 100644 docs/opengl/orthographic_projection.png create mode 100644 docs/opengl/perspective_projection.png rename src/imview/include/imview/{ => component}/buffer/buffer_interface.hpp (100%) rename src/imview/include/imview/{ => component}/buffer/buffer_registry.hpp (96%) rename src/imview/include/imview/{ => component}/buffer/double_buffer.hpp (96%) rename src/imview/include/imview/{ => component}/buffer/ring_buffer.hpp (99%) rename src/imview/include/imview/{ => component}/buffer/scrolling_plot_buffer.hpp (100%) rename src/imview/include/imview/{ => component}/event/async_event_dispatcher.hpp (91%) rename src/imview/include/imview/{ => component}/event/async_event_emitter.hpp (90%) rename src/imview/include/imview/{ => component}/event/event.hpp (100%) rename src/imview/include/imview/{ => component}/event/event_dispatcher.hpp (96%) rename src/imview/include/imview/{ => component}/event/event_emitter.hpp (90%) rename src/imview/include/imview/{ => component}/event/thread_safe_queue.hpp (100%) create mode 100644 src/imview/include/imview/primitive/grid.hpp rename src/imview/src/{ => component}/buffer/buffer_registry.cpp (91%) rename src/imview/src/{ => component}/buffer/scrolling_plot_buffer.cpp (94%) rename src/imview/src/{ => component}/event/async_event_dispatcher.cpp (97%) rename src/imview/src/{ => component}/event/event_dispatcher.cpp (92%) create mode 100644 src/imview/src/primitive/grid.cpp create mode 100644 src/imview/test/test_grid.cpp diff --git a/docs/opengl/opengl_coordinate_frames.png b/docs/opengl/opengl_coordinate_frames.png new file mode 100644 index 0000000000000000000000000000000000000000..9124cfe3d1df9c8c294b4325f78bb9421654844d GIT binary patch literal 153353 zcmeFY_d8r+*EUR|BnXM<8HDK3OLQWLDADUgjb5USHhPE-E!a0^ZWtduiri#+RVP@TIE{nTx*^Clcu`TJt8_H92}f`%C8i(ad7a*ad2)e z-ys10b1Tgj4gA1^yinG?16=-htRjHFX+0DTJ#<`bJiIO3t#NFfUEW#qKrG#@t(_ru zE*?9#+hl+@@!Y&g-rd^5!`{XDv9A3)YaAU5kH`FikJVkg9t-jd3O^PQkPs1;5E6K- z`TFrwezED9)Bzlv$2iIga=Jblo3rZ>V*~gqcBjE_JI(O!En~&VsX|?$ZTMe@I%!ME zPN6{2>+kdGVjzJBUg4HgIwsO4W4SpT@vP2|ao#H3YQUJFnqT4MD5xlEvd9Ial;R$z z_A0>B)o?LVr%i2^#Whq9{_lda4%?GGSNPv+nOm!zm-&A$_gh5civRnfg8K80#)$vD zdhc==Qvcsl;<)%5|9c}dGy7L_lmEMO8IPhk>VNNy$OsPc{J%Tn9%}yo9}4K||BHH? zM;L-{^qaxpxJ69;APz%o3A^^cx-rN;lg>Z&?lV;;bEDS0{5H3?eyw|~c}PSfEql-c zP`D07w|%U(f_mIWqYqq(LQ$8Ck}igzL_eyafU|!^5Qcd874U-PF3<0wo{l_<@OWEJ zMYv@Y$T6xb3ZK&WlfO?iE|L&=ZYq+57DG}`c7g)UYC~h=<-cP^gTuq*c<)#B zZUm4MJdTF%IKGqn^3BQRU1dXqTvLmZZBW*p&kr9dw%0oQQkL!>w)^)LmuqOEA1(OE zwC+6o$fQM~RW@N?10F=7gk)tkC=?4T$>Yf4c?7*`>*~F)Q5WkPbw+-*!{+?$Q@7>? zbXAy=+=F)-#bBL+z{Z#cgno19iP8ws*;<9M-jU}@BC2zqU8+LKpc6D?vh|BHkIiq6 z#$1DtL4|V(>+7D)I;_uyRoGaK$oRy>x)@<=>tT zHxtlAw;02UdlQMT>?M1W3D0v)D`(GZ>U!9Ci6ql7*93=jE`RQee zT9tk_m{W@_0Td7|GSKUc3>nm{1XLL)C@&_xBf z3-E|uLd>9Eah`pD0(|uI8BAK6AL#ZffvYA#AR8N z$5W5T+lY5*spZL9iA8=&h<;4MnKE$k*Pw05pzVwjPy=&hjt)arM6aYdv3?5Uo>>rf z{^V>}bkpe_us>syFX*WB6r+r+w_b%XKf?lN^WUbb3NpsyZ+R$c&BfFw-+4o;EBi8C z3?`ku*U2oM1NQHl`S0NeVZDQt5WmE>9^8bAW+B_}Z)d>+cEU@4s%$R~WUt}jWDNXk zp7vueoVj2BDmJhmf10Suq_u`_;Du>*f9MVG2dgGyekl8bN*O%}H|Y=Uy$Pj!PlA8h zeb{I;Zg3A%$<5(<`ary{(nAfq5Zr{_J{S_k(%d^Y&Tw=`_v4rH4ngcKKI*r;DAE3H zB!y8+UioHp?8W#)eJ)f0)&Y?=HfYm8A>1j*@c|Fh;ti8Bon2lWx##6wdvdvh;1626r3e5lQc?AmdygZGEdV8bcUuZN(!ky zFz?exm2gccF37AODg#iw7;(&FSDzm$@p<~}cG`mJ}L%{j7P}7vtbpo@Q=0RK06uWUQpKzx9 zz~fB51eK%2vQ}vC&eaO(xVnl4jE<#sxa~N0ZbH43JB2y$z`$&G?`Yrz&NYEbM+Z3J zpc}XcYfg&HX4*YLXQ2Yq>5pbw1JWBB#CgqHql;?NZXzONC!pMv0)Jy;3nNatrzca2{T9!YSENk;EDdK$;>kVN zqVV7EV?SvPn(Cr_0DZDAl{d2Pj@b^_o^L{WRQ2cP8nvFBkTaUKM4-ipMp0>qt%M%8 z!N5G^j>|me`a&ZpmS^*gcmd>h)O-gO{*Z5yL(SlfN7Ty}m1Vqy=`|6rQW~K@p%%4z zwv9%X{Gh7VF&o>qp{?oLfFOf|gCt`Pde_H5EOakzndtD>Zn~V zFC9r%*XS(yd$M~Wl{pJL*Vpq^P1CEg7iDdWt2L@b`X~WQ64G-1>7@aUvc0hucB$!g zAYS*auCh&(nsgn^ZLEC1iAkt^Q{W|Q|$WkbdAg-2$QD#ZWn7x_qu1WZd- z^MQC8f~)8>jqM1Ww&fLmsh%~|VzNEgGH|ppQ84D9%at_3PzbE_j3E}fzkK)n{Jb94 zwR9mYTFR?gmh37VE@1%U8Vg4VNcs`Zv|=N!_ciJSV5rXSZe^T6k2l<_BK;F)<3v56 zVPusQyYgEG?y`}&9anodI5>CEqe$-l39>Hf0KGOJ=vCN(5xrORsHxbg`p?vqm7`+ z1KUvj{X{^#*DdpWAWr>T*cwm(3xiHka;md_nzQcRvHics{ZS$8Pd6^|t}n=noogHX zk0ex6N?o_kl#uU&V7E^C%IbFlQS>IVr&9w#*DagRlolPnd_O!7Xl;O`m&Dj!m|de> zptTi4K^L-D^s2-Vmn-v?79d&;O5FQ$L}an>d&|SFZkj^TZA$(jaNMCqa~tLX{50i_ zjs33tUqSfBMj6&_fnPXwDSbZmqTWV#Ke``N2x%&z_#%+#L>FwUPX`&}NE z)F9uV{F)D13vat!Qp1P0fePQS|MGjwOI&Qy*2N{ZX`a{8spI3>jlyI=Q%5Mn(G~0HeLEGW|t*z`XyG#xmbvU{GBI zh%HV#WRtVgi20qYcXo3j#iN;Z)w*kNqZB+x5ApZhKVewWe$@hICNe&2;q6+~jFJQFADLq= zOsB;+QIPn;pOQV}O^<8+^-q>%_4p5_WlB;EjDeS9Shll@Y3X^D)zfKud<+cjZ$MiH zx(t1Y$B@h4P9+`BBEGc<9H@w9&mGTl_;*gk;%EI91%!SNwwi__P88)yQls)oh0`jp zr2A_`2BV@DKSQj*=$+e)NKbXYSNq8{mb(FD6P~?Q(U*Z)$XG@J5$Pc%QQ00hknx1{ zuSdxFBg^Z7mdTQu`chRb!%FUS-M;Cr>3r~I=t2W;_6{z-QF6g6IRFO%*6{UA!ic+p z&`qRTGz(&15~F(4Z9(!hCwOqxWNRxUuPJ%*2T)5u==gVZ(ftu95lfP)5uU5#vY-y+ zL^932GZMnGAgXb)^L*K$S+b%j_OddB=}y8iUviiE)e)KU@MrWy*yWzS{6AIq`7r{h zCS*fxNxticeW$q%3MvH zD1U;he)}8yg}IoGFzizT$%Ln!?!dC5T%G8*a;gc>R0rKO=kYu8S?v?88aK*dY!td* zJl*B-pNcCmvL>N?3hZi3h@m&gqk%i#bwBxjq`|o#a^?Et?3jTpS#<4?FnDpllcdzr zr@VQ1HoVBt@l$8;M#?IX-@9}X{eg9;Em^)1#%NcNJSPdK5|ceV&RX0pUYj#zS1QGw zqVmEi{ix*Gn_e0vf8{DjM zCj^8y^!`&K?q3o9r&QekJNPSHhg-b@xn&+GG06yfxGiIBZP^sF(=hm29>6gd*pm{6}Qr`EH zsT56)q|KUZRww}nvZE0H4p!S}wed|MyPP2rV>ioieVW1;0AiQ&Z|p-Py#|B^vcr-E zE3|3amV#XjhI`vBW7p0))P-TyN!!2*k=Z)o)luhtBBVsdv}$rTe<(rf?q~<wYTU+zx_&x1muu98EL+4DhPEP}BQiGQ?Fn zCvd>V^`)UT$?lxr^m&BF@_`yll-F#b)0h}I(a!p(%Lql@LE!>@W}Cdfc&!6KHWT4= zL-*e@O4i{~ieCEQkozMnjSVOvnp2yZG>LBe&s$qK6CvGtD>KBD?R68-$dW|FNZ`~z zXO;eM`vur%WQbcvd2?gO^(D3qG5bhJL|V1#m%nXxV~siN>fAK0aRpg89@>ui-N_9s zu1YM#V*gR#<4^3ToZu^Sg&;9D38!gtcFxwF3jXOF4kCx72ym{#!ud>n<#SFV!_>^T zg+{DI{!1*ZM7K#kJ~H4FWMEIYrxnwIH7-mdU*TBp+@u%>1IPo8*)Nm5j&Kfo<-*@7 z@Ub1cYfOzw$2+=lBIf`0nTBr@Bisgi#(BGb2#P>(C;YG@m&*B94kfn6$ZTVxy?_)B zlHN*CF#jPClR_6$JM1c<1(=3qC=4x@n1kcUemN>V(d`NPa-^JU$`yw9UZcnY=2s=z zla0?}-|q@SHv&m(v5^;M$&vD+ox$1bFaKDv7N?T1k^s!|&aEJKg`WFo;1{26PX2#d zRB=Km^Ia)IK+Oly+*Qlak2S}cE~aM^QWLpgm>5^7ES-A;1-T+_4-LuuSSuraf&nM} zybMXet@$pogz1m(uRHhr=2#Lj^@V}^c}K0+_3({_m$nKK!dNBWN?ouw(A;Ui1etIkW9ETQXAY~ZDJ z(Dn9N*45lUHOe099O&MhiwEMjqj6CXBXBBa{+h3C6@cSHn^>SFvEuRu^{>K?nc$U;Xq?C1Nv!*mYP-rX#$5yZhr4Vm)+Y>L?Qx+ z92;o#_hS4apWNu`-xYUo^M0cSo?fR8IjX8G7*tSVVeceZ3(hcalbTl?-BjO`y?Hj; z68y8?5TTQJ)4Xby3_FDdEZ!hq0JSUM+}|aI$c*uk*xt38$xdPYp{RZ}^TOHK$|?6Y zzovNuxkx?5c#UHju)ho)s&bkx3(;h6`jTno^wyn~zg)KWZx%XG0u~ZijuRTx@%qiQ z;g&U4Kk#xU?=Qh1{>l!|vfvwjE^S|<&Jj04YWY8V`Hi^N6S6I#H@8CH%!Px_18743 znc>YhbCcnWKzO;-cDclMB^9>UNv?0NoYQ9o=l$n(_$Euj+(ufB zF`r07@qchIE#ze0d(K2PSvjd$e2)95$;~ja`lf}_3k8t!a7HJ1iJ84ZJdMZBhY1oG zR+LK+Ws!uTcD6<$A)s66cnWa>d1EW9TCtYf|8}OzoxD^|IiS^kIm|E}f))cBPL>&c ziJLy$J5DtJ+-&DVh^IKj7_I3gMQ5sDLtLtGPlorkmb>&V^ly~T5;I}D5UD{I0V@*G zzGRW_$CT}+amsPyEOSyo|1i$7LJSs)7TaMl&pOoOb2XtxVFh5J9p-uk|9a1u#GBP$ zAg!Nq=(q>iTnY*We)>qgL?`b+Jif&6NSQI%zU;@4dlYS(f~mt>{trF5fARDDDQo=HfF!EBxl3R8KO)E9Y)S>DM4?+@$h{yn(IHfW zAN%s$UwV>Y0X%9YP$-Bo$WMZGdW1sgZr}99p#xH=TrlTi6E9*hxTNGYgS7wi`*w1_HS-vs&ZJvPGdCT6MciaavGV_LrCEm2$jxF2J75KxJtt;kG-&y({mp@ zf-CTQ)ShwI82Y1fwRzn({))&nHuiQC3yl^fp}(5mc+PV3e2*awSOj)jU+o0V;|E5AF1 z%>n}x+OdnS2wl`cs>67}1Xm*B4pQsjCP+OHZ~O10B11@S;US((jBk=-3iW&CrextE zJ4O#$q`>rffQ^k!+LteE(JzcA0JQE ztBoxtfBdh{+_&M?)5n)<2aKk$XQDijA>fi>B~j;i^nRWTLwQwQ(3f!b8-48$7rgy1 zM5KfDkbJackT$_=cTsk#L={CHjBeZmD&4vKYK};1uF+UlG#8WHUL?s?yGW# ziQw2=khh}Y`}zM2gdCp3HJkUl%Xo4#z;_8r<}~nUL_CNYMgGOi~NuGL~dR41`P7xeEfLb)p?UD4zf|_LNx-M@Uo(zOmxe(2k%%$j%PtvUykUwB zmU_LL(f21%mgGkQv+d_R93L77=jZ0l7-w67XHRI!B9vH{I9Q1MzteekKK98&{__`i zFh%h|L;O_8${y}?pB1|JJGFbss8p%>O;A7mJUCQ+GKOKSs;xnd&pjIl+c+?YUgZTT zHc%}20l2b!UPK48ZJy%SWrD-y+|I{U#m%s7W=SghL%GK_gPfh#?F%rhE-jbzsjC#T+3HF>vR z$)&szEp^}OSIl|b4iVWS2ZV_`(73S>esUoEr@9i(=uGgY>l2-u7sy^ygsC<0ww)Xj zOq6j0JwU9K!S>yZgT-jHD9G*@jj0-^#;{6e6`QKIW!TyZ^|3c2Tnp? z_519{A*(jph4A6;H}O`S3@~8SI296iwc%vjrJBi-Huef(_QCa)18x8coQSlnuebL7i-4bcyRe5 za!pdxx%0y{@wG$ak59te>pr~C)$QLq8hHT-w9HOF)a83STz$gYn#cT|khu+47tT=ma*UwbZ}E?+5HurHq7Wq9}ImcTxM0bb+Cnw zs5uI5sSUq9+AnTobF~z36AqT0fT{eBu9l%MI(jy+v+hj*e3R7ZUF;fCoJQVMEo95U zKFXPdX3&b7hZz6!viKV}b=&x&geLJ0{_o4JG(_oLF7DOM6^50!HjAXL_AE8F14fb3 zeB7r`k3B?#1?G85_m_a;oK+9QH#6byJ&(|o9U^+E>;)>;FbG)Pcz_nhEFK5{grt^* z�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�}zSJ{DIfAS8<1lbm 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 literal 0 HcmV?d00001 diff --git a/docs/opengl/orthographic_projection.png b/docs/opengl/orthographic_projection.png new file mode 100644 index 0000000000000000000000000000000000000000..93a9ddc6ec87e79435cb2651d2993a40d2c6005c GIT binary patch literal 43427 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 G8u34^=_j!O literal 0 HcmV?d00001 diff --git a/docs/opengl/perspective_projection.png b/docs/opengl/perspective_projection.png new file mode 100644 index 0000000000000000000000000000000000000000..461a4e8074aeb8979ca309be013344d1f3a07687 GIT binary patch literal 33294 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%0D>V*xe0K_@@hPI=O3a{CB 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 this->SetNoBackground(true); - gl_scene_widget_.SetNoMove(true); - gl_scene_widget_.SetWindowNoTabBar(); + gl_widget_.SetNoMove(true); + gl_widget_.SetWindowNoTabBar(); } void MainDockingPanel::Draw() { @@ -42,7 +42,7 @@ void MainDockingPanel::Draw() { ImGui::DockBuilderDockWindow(config_panel_.GetName().c_str(), config_panel_node_); - ImGui::DockBuilderDockWindow(gl_scene_widget_.GetName().c_str(), + ImGui::DockBuilderDockWindow(gl_widget_.GetName().c_str(), gl_scene_widget_node_); ImGui::DockBuilderDockWindow(console_panel_.GetName().c_str(), console_panel_node_); @@ -57,7 +57,7 @@ void MainDockingPanel::Draw() { // draw child panels if (config_panel_.IsVisible()) config_panel_.Draw(); if (console_panel_.IsVisible()) console_panel_.Draw(); - if (gl_scene_widget_.IsVisible()) gl_scene_widget_.Draw(); + if (gl_widget_.IsVisible()) gl_widget_.Draw(); End(); } diff --git a/src/app/panels/main_docking_panel.hpp b/src/app/panels/main_docking_panel.hpp index 1d51b0a..8a7e905 100644 --- a/src/app/panels/main_docking_panel.hpp +++ b/src/app/panels/main_docking_panel.hpp @@ -27,7 +27,6 @@ class MainDockingPanel : public Panel { void ChangeDebugPanelVisibility(bool visible); private: - bool layout_initialized_ = false; ImGuiID dockspace_id_; @@ -37,7 +36,7 @@ class MainDockingPanel : public Panel { ConfigPanel config_panel_{"Config"}; ConsolePanel console_panel_{"Console"}; - GlSceneWidget gl_scene_widget_{"Scene"}; + GlWidget gl_widget_{"Scene"}; }; } // namespace quickviz diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index 55dd257..afb3de8 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -37,12 +37,14 @@ add_library(imview src/component/shader.cpp src/component/shader_program.cpp src/component/frame_buffer.cpp + # opengl primitives + src/primitive/grid.cpp # data buffer - src/buffer/buffer_registry.cpp - src/buffer/scrolling_plot_buffer.cpp + src/component/buffer/buffer_registry.cpp + src/component/buffer/scrolling_plot_buffer.cpp # event handling - src/event/event_dispatcher.cpp - src/event/async_event_dispatcher.cpp + src/component/event/event_dispatcher.cpp + src/component/event/async_event_dispatcher.cpp ) target_link_libraries(imview PUBLIC imcore PkgConfig::Cairo PkgConfig::Fontconfig diff --git a/src/imview/include/imview/buffer/buffer_interface.hpp b/src/imview/include/imview/component/buffer/buffer_interface.hpp similarity index 100% rename from src/imview/include/imview/buffer/buffer_interface.hpp rename to src/imview/include/imview/component/buffer/buffer_interface.hpp diff --git a/src/imview/include/imview/buffer/buffer_registry.hpp b/src/imview/include/imview/component/buffer/buffer_registry.hpp similarity index 96% rename from src/imview/include/imview/buffer/buffer_registry.hpp rename to src/imview/include/imview/component/buffer/buffer_registry.hpp index 525a4f5..a25d2d3 100644 --- a/src/imview/include/imview/buffer/buffer_registry.hpp +++ b/src/imview/include/imview/component/buffer/buffer_registry.hpp @@ -14,7 +14,7 @@ #include #include -#include "imview/buffer/buffer_interface.hpp" +#include "imview/component/buffer/buffer_interface.hpp" namespace quickviz { class BufferRegistry { diff --git a/src/imview/include/imview/buffer/double_buffer.hpp b/src/imview/include/imview/component/buffer/double_buffer.hpp similarity index 96% rename from src/imview/include/imview/buffer/double_buffer.hpp rename to src/imview/include/imview/component/buffer/double_buffer.hpp index aa59719..7645993 100644 --- a/src/imview/include/imview/buffer/double_buffer.hpp +++ b/src/imview/include/imview/component/buffer/double_buffer.hpp @@ -13,7 +13,7 @@ #include #include -#include "imview/buffer/buffer_interface.hpp" +#include "imview/component/buffer/buffer_interface.hpp" namespace quickviz { template diff --git a/src/imview/include/imview/buffer/ring_buffer.hpp b/src/imview/include/imview/component/buffer/ring_buffer.hpp similarity index 99% rename from src/imview/include/imview/buffer/ring_buffer.hpp rename to src/imview/include/imview/component/buffer/ring_buffer.hpp index 9c5f48e..ca90497 100644 --- a/src/imview/include/imview/buffer/ring_buffer.hpp +++ b/src/imview/include/imview/component/buffer/ring_buffer.hpp @@ -64,7 +64,7 @@ #include #include -#include "imview/buffer/buffer_interface.hpp" +#include "imview/component/buffer/buffer_interface.hpp" namespace quickviz { template diff --git a/src/imview/include/imview/buffer/scrolling_plot_buffer.hpp b/src/imview/include/imview/component/buffer/scrolling_plot_buffer.hpp similarity index 100% rename from src/imview/include/imview/buffer/scrolling_plot_buffer.hpp rename to src/imview/include/imview/component/buffer/scrolling_plot_buffer.hpp diff --git a/src/imview/include/imview/event/async_event_dispatcher.hpp b/src/imview/include/imview/component/event/async_event_dispatcher.hpp similarity index 91% rename from src/imview/include/imview/event/async_event_dispatcher.hpp rename to src/imview/include/imview/component/event/async_event_dispatcher.hpp index 7d108d8..bd9a1ea 100644 --- a/src/imview/include/imview/event/async_event_dispatcher.hpp +++ b/src/imview/include/imview/component/event/async_event_dispatcher.hpp @@ -18,8 +18,8 @@ #include #include -#include "imview/event/event.hpp" -#include "imview/event/thread_safe_queue.hpp" +#include "imview/component/event/event.hpp" +#include "imview/component/event/thread_safe_queue.hpp" namespace quickviz { class AsyncEventDispatcher { diff --git a/src/imview/include/imview/event/async_event_emitter.hpp b/src/imview/include/imview/component/event/async_event_emitter.hpp similarity index 90% rename from src/imview/include/imview/event/async_event_emitter.hpp rename to src/imview/include/imview/component/event/async_event_emitter.hpp index 2d7362a..8b08fdf 100644 --- a/src/imview/include/imview/event/async_event_emitter.hpp +++ b/src/imview/include/imview/component/event/async_event_emitter.hpp @@ -8,7 +8,7 @@ #ifndef QUICKVIZ_EVENT_ASYNC_EMITTER_HPP #define QUICKVIZ_EVENT_ASYNC_EMITTER_HPP -#include "imview/event/async_event_dispatcher.hpp" +#include "imview/component/event/async_event_dispatcher.hpp" namespace quickviz { class AsyncEventEmitter { diff --git a/src/imview/include/imview/event/event.hpp b/src/imview/include/imview/component/event/event.hpp similarity index 100% rename from src/imview/include/imview/event/event.hpp rename to src/imview/include/imview/component/event/event.hpp diff --git a/src/imview/include/imview/event/event_dispatcher.hpp b/src/imview/include/imview/component/event/event_dispatcher.hpp similarity index 96% rename from src/imview/include/imview/event/event_dispatcher.hpp rename to src/imview/include/imview/component/event/event_dispatcher.hpp index c4c02d9..c377e7e 100644 --- a/src/imview/include/imview/event/event_dispatcher.hpp +++ b/src/imview/include/imview/component/event/event_dispatcher.hpp @@ -15,7 +15,7 @@ #include #include -#include "imview/event/event.hpp" +#include "imview/component/event/event.hpp" namespace quickviz { class EventDispatcher { diff --git a/src/imview/include/imview/event/event_emitter.hpp b/src/imview/include/imview/component/event/event_emitter.hpp similarity index 90% rename from src/imview/include/imview/event/event_emitter.hpp rename to src/imview/include/imview/component/event/event_emitter.hpp index dfff7d3..45e57f5 100644 --- a/src/imview/include/imview/event/event_emitter.hpp +++ b/src/imview/include/imview/component/event/event_emitter.hpp @@ -8,7 +8,7 @@ #ifndef QUICKVIZ_EVENT_EMITTER_HPP #define QUICKVIZ_EVENT_EMITTER_HPP -#include "imview/event/event_dispatcher.hpp" +#include "imview/component/event/event_dispatcher.hpp" namespace quickviz { class EventEmitter { diff --git a/src/imview/include/imview/event/thread_safe_queue.hpp b/src/imview/include/imview/component/event/thread_safe_queue.hpp similarity index 100% rename from src/imview/include/imview/event/thread_safe_queue.hpp rename to src/imview/include/imview/component/event/thread_safe_queue.hpp diff --git a/src/imview/include/imview/component/shader.hpp b/src/imview/include/imview/component/shader.hpp index a329006..e0945ed 100644 --- a/src/imview/include/imview/component/shader.hpp +++ b/src/imview/include/imview/component/shader.hpp @@ -23,6 +23,7 @@ class Shader { }; public: + Shader(const char* source_code, Type type); Shader(const std::string& source_file, Type type); ~Shader(); @@ -36,7 +37,7 @@ class Shader { uint32_t GetShaderID() const { return shader_id_; } private: - std::string LoadSourceFile(const std::string& file_path); + void CreateShader(); std::string source_file_; Type type_; diff --git a/src/imview/include/imview/primitive/grid.hpp b/src/imview/include/imview/primitive/grid.hpp new file mode 100644 index 0000000..000c36f --- /dev/null +++ b/src/imview/include/imview/primitive/grid.hpp @@ -0,0 +1,41 @@ +/* + * @file grid.hpp + * @date 11/2/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef QUICKVIZ_GRID_HPP +#define QUICKVIZ_GRID_HPP + +#include + +#include + +#include "imview/component/shader_program.hpp" + +namespace quickviz { +class Grid { + public: + Grid(float grid_size = 10.0f, float spacing = 1.0f, + glm::vec3 color = glm::vec3(0.5f, 0.5f, 0.5f)); + ~Grid(); + + void Initialize(); + void Draw(const glm::mat4& projection, const glm::mat4& view); + + private: + void GenerateGrid(); + + float grid_size_; + float spacing_; + glm::vec3 color_; + uint32_t vao_; + uint32_t vbo_; + std::vector vertices_; + ShaderProgram shader_; +}; +} // namespace quickviz + +#endif // QUICKVIZ_GRID_HPP \ No newline at end of file diff --git a/src/imview/include/imview/viewer.hpp b/src/imview/include/imview/viewer.hpp index 16e8188..68524df 100644 --- a/src/imview/include/imview/viewer.hpp +++ b/src/imview/include/imview/viewer.hpp @@ -45,6 +45,7 @@ class Viewer : public Window { void Show(); protected: + void SetupOpenGL(); void ClearBackground(); void CreateNewImGuiFrame(); void RenderImGuiFrame(); diff --git a/src/imview/include/imview/widget/buffered_cv_image_widget.hpp b/src/imview/include/imview/widget/buffered_cv_image_widget.hpp index 7e448c8..f622cf7 100644 --- a/src/imview/include/imview/widget/buffered_cv_image_widget.hpp +++ b/src/imview/include/imview/widget/buffered_cv_image_widget.hpp @@ -12,7 +12,7 @@ #include #include "imview/panel.hpp" -#include "imview/buffer/buffer_registry.hpp" +#include "imview/component/buffer/buffer_registry.hpp" #include "glad/glad.h" diff --git a/src/imview/include/imview/widget/rt_line_plot_widget.hpp b/src/imview/include/imview/widget/rt_line_plot_widget.hpp index 026d80e..162a0dd 100644 --- a/src/imview/include/imview/widget/rt_line_plot_widget.hpp +++ b/src/imview/include/imview/widget/rt_line_plot_widget.hpp @@ -13,8 +13,8 @@ #include #include "imview/panel.hpp" -#include "imview/buffer/buffer_registry.hpp" -#include "imview/buffer/scrolling_plot_buffer.hpp" +#include "imview/component/buffer/buffer_registry.hpp" +#include "imview/component/buffer/scrolling_plot_buffer.hpp" namespace quickviz { class RtLinePlotWidget : public Panel { diff --git a/src/imview/src/buffer/buffer_registry.cpp b/src/imview/src/component/buffer/buffer_registry.cpp similarity index 91% rename from src/imview/src/buffer/buffer_registry.cpp rename to src/imview/src/component/buffer/buffer_registry.cpp index f889632..7c83933 100644 --- a/src/imview/src/buffer/buffer_registry.cpp +++ b/src/imview/src/component/buffer/buffer_registry.cpp @@ -8,7 +8,7 @@ #include -#include "imview/buffer/buffer_registry.hpp" +#include "imview/component/buffer/buffer_registry.hpp" namespace quickviz { BufferRegistry& BufferRegistry::GetInstance() { diff --git a/src/imview/src/buffer/scrolling_plot_buffer.cpp b/src/imview/src/component/buffer/scrolling_plot_buffer.cpp similarity index 94% rename from src/imview/src/buffer/scrolling_plot_buffer.cpp rename to src/imview/src/component/buffer/scrolling_plot_buffer.cpp index bc2c5ba..42faa48 100644 --- a/src/imview/src/buffer/scrolling_plot_buffer.cpp +++ b/src/imview/src/component/buffer/scrolling_plot_buffer.cpp @@ -7,7 +7,7 @@ * Copyright (c) 2021 Ruixiang Du (rdu) */ -#include "imview/buffer/scrolling_plot_buffer.hpp" +#include "imview/component/buffer/scrolling_plot_buffer.hpp" #include diff --git a/src/imview/src/event/async_event_dispatcher.cpp b/src/imview/src/component/event/async_event_dispatcher.cpp similarity index 97% rename from src/imview/src/event/async_event_dispatcher.cpp rename to src/imview/src/component/event/async_event_dispatcher.cpp index 13aa74e..451198f 100644 --- a/src/imview/src/event/async_event_dispatcher.cpp +++ b/src/imview/src/component/event/async_event_dispatcher.cpp @@ -6,7 +6,7 @@ * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/event/async_event_dispatcher.hpp" +#include "imview/component/event/async_event_dispatcher.hpp" namespace quickviz { AsyncEventDispatcher& AsyncEventDispatcher::GetInstance() { diff --git a/src/imview/src/event/event_dispatcher.cpp b/src/imview/src/component/event/event_dispatcher.cpp similarity index 92% rename from src/imview/src/event/event_dispatcher.cpp rename to src/imview/src/component/event/event_dispatcher.cpp index 21770e5..53a0561 100644 --- a/src/imview/src/event/event_dispatcher.cpp +++ b/src/imview/src/component/event/event_dispatcher.cpp @@ -6,7 +6,7 @@ * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/event/event_dispatcher.hpp" +#include "imview/component/event/event_dispatcher.hpp" namespace quickviz { EventDispatcher& EventDispatcher::GetInstance() { diff --git a/src/imview/src/component/shader.cpp b/src/imview/src/component/shader.cpp index faf2880..b1f1d8b 100644 --- a/src/imview/src/component/shader.cpp +++ b/src/imview/src/component/shader.cpp @@ -32,9 +32,29 @@ std::string LoadShaderSource(const std::string& file_path) { } } // namespace +Shader::Shader(const char* source_code, Type type) + : source_code_(source_code), type_(type) { + if (source_code == nullptr) { + std::cout << "ERROR::SHADER::INVALID_SOURCE_CODE" << std::endl; + } + CreateShader(); +} + Shader::Shader(const std::string& source_file, Shader::Type type) : source_file_(source_file), type_(type) { source_code_ = LoadShaderSource(source_file_); + + CreateShader(); +} + +Shader::~Shader() { glDeleteShader(shader_id_); } + +void Shader::CreateShader() { + if (source_code_.empty()) { + std::cout << "ERROR::SHADER::INVALID_SOURCE_FILE" << std::endl; + throw std::invalid_argument("Invalid shader source file"); + } + if (type_ == Shader::Type::kVertex) shader_id_ = glCreateShader(GL_VERTEX_SHADER); else if (type_ == Shader::Type::kFragment) @@ -43,8 +63,6 @@ Shader::Shader(const std::string& source_file, Shader::Type type) glShaderSource(shader_id_, 1, &code, NULL); } -Shader::~Shader() { glDeleteShader(shader_id_); } - void Shader::Print() const { std::cout << "Shader source file: " << source_file_ << std::endl; std::cout << "Shader type: " << static_cast(type_) << std::endl; @@ -68,8 +86,4 @@ bool Shader::Compile() { } return success; } - -std::string Shader::LoadSourceFile(const std::string& file_path) { - return std::string(); -} } // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/primitive/grid.cpp b/src/imview/src/primitive/grid.cpp new file mode 100644 index 0000000..26a8adf --- /dev/null +++ b/src/imview/src/primitive/grid.cpp @@ -0,0 +1,104 @@ +/* + * @file grid.cpp + * @date 11/2/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/primitive/grid.hpp" + +#include + +#include "glad/glad.h" + +namespace quickviz { +namespace { +std::string vertex_shader_source = R"( +#version 330 core + +layout(location = 0) in vec3 aPos; +uniform mat4 projection; +uniform mat4 view; +uniform mat4 model; + +void main() { + gl_Position = projection * view * model * vec4(aPos, 1.0); +} +)"; + +std::string fragment_shader_source = R"( +#version 330 core + +out vec4 FragColor; +uniform vec3 lineColor; + +void main() { + FragColor = vec4(lineColor, 1.0); +} +)"; +} // namespace + +Grid::Grid(float grid_size, float spacing, glm::vec3 color) + : grid_size_(grid_size), spacing_(spacing), color_(color) { + Shader vertex_shader(vertex_shader_source.c_str(), Shader::Type::kVertex); + Shader fragment_shader(fragment_shader_source.c_str(), + Shader::Type::kFragment); + shader_.AttachShader(vertex_shader); + shader_.AttachShader(fragment_shader); + if (!shader_.LinkProgram()) { + std::cout << "ERROR::GRID::SHADER_PROGRAM_LINKING_FAILED" << std::endl; + throw std::runtime_error("Shader program linking failed"); + } + GenerateGrid(); +} + +Grid::~Grid() { + glDeleteVertexArrays(1, &vao_); + glDeleteBuffers(1, &vbo_); +} + +void Grid::Initialize() { + shader_.Use(); + shader_.SetUniform("lineColor", color_); +} + +void Grid::Draw(const glm::mat4& projection, const glm::mat4& view) { + shader_.Use(); + shader_.SetUniform("projection", projection); + shader_.SetUniform("view", view); + shader_.SetUniform("model", glm::mat4(1.0f)); // Identity matrix for the grid + + glBindVertexArray(vao_); + glDrawArrays(GL_LINES, 0, vertices_.size()); + glBindVertexArray(0); +} + +void Grid::GenerateGrid() { + // generate grid vertices along X and Z axes + for (float x = -grid_size_; x <= grid_size_; x += spacing_) { + vertices_.emplace_back(x, 0.0f, -grid_size_); + vertices_.emplace_back(x, 0.0f, grid_size_); + } + + for (float z = -grid_size_; z <= grid_size_; z += spacing_) { + vertices_.emplace_back(-grid_size_, 0.0f, z); + vertices_.emplace_back(grid_size_, 0.0f, z); + } + + // set up VAO and VBO + glGenVertexArrays(1, &vao_); + glGenBuffers(1, &vbo_); + + glBindVertexArray(vao_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, vertices_.size() * sizeof(glm::vec3), + vertices_.data(), GL_STATIC_DRAW); + + // set vertex attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); + glEnableVertexAttribArray(0); + + glBindVertexArray(0); // Unbind VAO +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/viewer.cpp b/src/imview/src/viewer.cpp index f97d42a..66e460c 100644 --- a/src/imview/src/viewer.cpp +++ b/src/imview/src/viewer.cpp @@ -195,12 +195,18 @@ void Viewer::SetWindowShouldClose() { glfwSetWindowShouldClose(win_, GLFW_TRUE); } +void Viewer::SetupOpenGL() { + glEnable(GL_DEPTH_TEST); + // glEnable(GL_BLEND); + // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +} + void Viewer::ClearBackground() { int display_w, display_h; glfwGetFramebufferSize(win_, &display_w, &display_h); glViewport(0, 0, display_w, display_h); glClearColor(bg_color_[0], bg_color_[1], bg_color_[2], bg_color_[3]); - glClear(GL_COLOR_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } void Viewer::CreateNewImGuiFrame() { @@ -244,6 +250,8 @@ void Viewer::Show() { glfwGetFramebufferSize(win_, &display_w, &display_h); OnResize(win_, display_w, display_h); + SetupOpenGL(); + // main loop while (!ShouldClose()) { // handle events diff --git a/src/imview/test/CMakeLists.txt b/src/imview/test/CMakeLists.txt index c83ede2..a7fb7f5 100644 --- a/src/imview/test/CMakeLists.txt +++ b/src/imview/test/CMakeLists.txt @@ -25,6 +25,9 @@ target_link_libraries(test_framebuffer PRIVATE imview) add_executable(test_shader test_shader.cpp) target_link_libraries(test_shader PRIVATE imview) +add_executable(test_grid test_grid.cpp) +target_link_libraries(test_grid PRIVATE imview) + find_package(OpenCV QUIET) if (OpenCV_FOUND) add_executable(test_double_buffer test_double_buffer.cpp) diff --git a/src/imview/test/feature/test_buffered_cv_image_widget.cpp b/src/imview/test/feature/test_buffered_cv_image_widget.cpp index ec8a209..d556366 100644 --- a/src/imview/test/feature/test_buffered_cv_image_widget.cpp +++ b/src/imview/test/feature/test_buffered_cv_image_widget.cpp @@ -14,9 +14,9 @@ #include "imview/viewer.hpp" #include "imview/box.hpp" -#include "imview/buffer/buffer_registry.hpp" -#include "imview/buffer/ring_buffer.hpp" -#include "imview/buffer/double_buffer.hpp" +#include "imview/component/buffer/buffer_registry.hpp" +#include "imview/component/buffer/ring_buffer.hpp" +#include "imview/component/buffer/double_buffer.hpp" #include "scene_objects/gl_triangle_scene_object.hpp" #include "imview/widget/buffered_cv_image_widget.hpp" diff --git a/src/imview/test/feature/test_implot_widget.cpp b/src/imview/test/feature/test_implot_widget.cpp index 424f7ad..a943d0d 100644 --- a/src/imview/test/feature/test_implot_widget.cpp +++ b/src/imview/test/feature/test_implot_widget.cpp @@ -12,8 +12,8 @@ #include -#include "imview/buffer/buffer_registry.hpp" -#include "imview/buffer/ring_buffer.hpp" +#include "imview/component/buffer/buffer_registry.hpp" +#include "imview/component/buffer/ring_buffer.hpp" #include "imview/viewer.hpp" #include "imview/widget/rt_line_plot_widget.hpp" diff --git a/src/imview/test/test_async_event.cpp b/src/imview/test/test_async_event.cpp index 2bafce0..a1da4f2 100644 --- a/src/imview/test/test_async_event.cpp +++ b/src/imview/test/test_async_event.cpp @@ -8,9 +8,9 @@ #include -#include "imview/event/event.hpp" -#include "imview/event/async_event_dispatcher.hpp" -#include "imview/event/async_event_emitter.hpp" +#include "imview/component/event/event.hpp" +#include "imview/component/event/async_event_dispatcher.hpp" +#include "imview/component/event/async_event_emitter.hpp" using namespace quickviz; diff --git a/src/imview/test/test_double_buffer.cpp b/src/imview/test/test_double_buffer.cpp index 492d218..a6c20cd 100644 --- a/src/imview/test/test_double_buffer.cpp +++ b/src/imview/test/test_double_buffer.cpp @@ -9,7 +9,7 @@ #include #include -#include "imview/buffer/double_buffer.hpp" +#include "imview/component/buffer/double_buffer.hpp" #include diff --git a/src/imview/test/test_event.cpp b/src/imview/test/test_event.cpp index 456a228..0ba71df 100644 --- a/src/imview/test/test_event.cpp +++ b/src/imview/test/test_event.cpp @@ -8,9 +8,9 @@ #include -#include "imview/event/event.hpp" -#include "imview/event/event_dispatcher.hpp" -#include "imview/event/event_emitter.hpp" +#include "imview/component/event/event.hpp" +#include "imview/component/event/event_dispatcher.hpp" +#include "imview/component/event/event_emitter.hpp" using namespace quickviz; diff --git a/src/imview/test/test_grid.cpp b/src/imview/test/test_grid.cpp new file mode 100644 index 0000000..7645025 --- /dev/null +++ b/src/imview/test/test_grid.cpp @@ -0,0 +1,57 @@ +/* + * test_wgui.cpp + * + * Created on: Jul 22, 2021 14:50 + * Description: + * + * Copyright (c) 2021 Ruixiang Du (rdu) + */ + +#include + +#include +#include + +#include "glad/glad.h" +#include "imview/window.hpp" +#include "imview/primitive/grid.hpp" + +using namespace quickviz; + +int main(int argc, char* argv[]) { + int width = 1920; + int height = 1080; + Window win("Test Window", width, height); + + glEnable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Orthographic projection for a top-down view + float aspect_ratio = static_cast(width) / static_cast(height); + glm::mat4 projection = + glm::perspective(glm::radians(45.0f), aspect_ratio, 0.1f, 100.0f); + + // Simple view matrix looking at an angle + glm::mat4 view = glm::lookAt( + glm::vec3(10.0f, 10.0f, 10.0f), // Camera positioned at an angle + glm::vec3(0.0f, 0.0f, 0.0f), // Looking at the origin + glm::vec3(0.0f, 1.0f, 0.0f) // Up vector pointing along the Y-axis + ); + + // set up grid + Grid grid(10.0f, 1.0f, glm::vec3(0.7f, 0.7f, 0.7f)); + grid.Initialize(); + + while (!win.ShouldClose()) { + win.PollEvents(); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + grid.Draw(projection, view); + + win.SwapBuffers(); + } + + return 0; +} diff --git a/src/imview/test/test_ring_buffer.cpp b/src/imview/test/test_ring_buffer.cpp index c38c316..2f45ec0 100644 --- a/src/imview/test/test_ring_buffer.cpp +++ b/src/imview/test/test_ring_buffer.cpp @@ -9,7 +9,7 @@ #include #include -#include "imview/buffer/ring_buffer.hpp" +#include "imview/component/buffer/ring_buffer.hpp" #include diff --git a/src/imview/test/unit_test/utest_ring_buffer.cpp b/src/imview/test/unit_test/utest_ring_buffer.cpp index 19d113f..658b03d 100644 --- a/src/imview/test/unit_test/utest_ring_buffer.cpp +++ b/src/imview/test/unit_test/utest_ring_buffer.cpp @@ -7,7 +7,7 @@ */ #include -#include "imview/buffer/ring_buffer.hpp" +#include "imview/component/buffer/ring_buffer.hpp" using namespace quickviz; From 0afb7afa0ae98e4f1b49d6087f3b4fce2dcab7aa Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sun, 3 Nov 2024 23:02:36 +0800 Subject: [PATCH 13/20] add camera class, added console to quickviz app --- src/app/CMakeLists.txt | 1 + src/app/component/log_processor.cpp | 108 +++++++++++++++++ src/app/component/log_processor.hpp | 43 +++++++ src/app/panels/config_panel.cpp | 30 ++++- src/app/panels/console_panel.cpp | 23 ++-- src/app/panels/console_panel.hpp | 4 + src/imview/CMakeLists.txt | 14 +-- .../imview/component/opengl/camera.hpp | 78 ++++++++++++ .../component/{ => opengl}/frame_buffer.hpp | 0 .../{primitive => component/opengl}/grid.hpp | 8 +- .../imview/component/{ => opengl}/shader.hpp | 0 .../component/{ => opengl}/shader_program.hpp | 2 +- .../include/imview/widget/gl_widget.hpp | 2 +- src/imview/include/imview/window.hpp | 3 + src/imview/src/component/opengl/camera.cpp | 95 +++++++++++++++ .../component/{ => opengl}/frame_buffer.cpp | 2 +- .../{primitive => component/opengl}/grid.cpp | 24 ++-- .../src/component/{ => opengl}/shader.cpp | 2 +- .../component/{ => opengl}/shader_program.cpp | 2 +- src/imview/src/viewer.cpp | 5 +- src/imview/src/widget/gl_widget.cpp | 9 +- src/imview/src/window.cpp | 4 + src/imview/test/CMakeLists.txt | 3 + src/imview/test/test_camera.cpp | 111 ++++++++++++++++++ src/imview/test/test_framebuffer.cpp | 2 +- src/imview/test/test_grid.cpp | 2 +- src/imview/test/test_shader.cpp | 4 +- 27 files changed, 537 insertions(+), 44 deletions(-) create mode 100644 src/app/component/log_processor.cpp create mode 100644 src/app/component/log_processor.hpp create mode 100644 src/imview/include/imview/component/opengl/camera.hpp rename src/imview/include/imview/component/{ => opengl}/frame_buffer.hpp (100%) rename src/imview/include/imview/{primitive => component/opengl}/grid.hpp (79%) rename src/imview/include/imview/component/{ => opengl}/shader.hpp (100%) rename src/imview/include/imview/component/{ => opengl}/shader_program.hpp (95%) create mode 100644 src/imview/src/component/opengl/camera.cpp rename src/imview/src/component/{ => opengl}/frame_buffer.cpp (98%) rename src/imview/src/{primitive => component/opengl}/grid.cpp (77%) rename src/imview/src/component/{ => opengl}/shader.cpp (98%) rename src/imview/src/component/{ => opengl}/shader_program.cpp (97%) create mode 100644 src/imview/test/test_camera.cpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 6f84d51..a1b15ea 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -3,6 +3,7 @@ if (BUILD_QUICKVIZ_APP) add_executable(quickviz main.cpp # components quickviz_application.cpp + component/log_processor.cpp # panels panels/menu_bar.cpp panels/main_docking_panel.cpp diff --git a/src/app/component/log_processor.cpp b/src/app/component/log_processor.cpp new file mode 100644 index 0000000..a18a38d --- /dev/null +++ b/src/app/component/log_processor.cpp @@ -0,0 +1,108 @@ +/* + * @file log_processor.cpp + * @date 11/3/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "component/log_processor.hpp" + +namespace quickviz { +LogProcessor::LogProcessor() { + auto_scroll_ = true; + Clear(); +} + +void LogProcessor::Clear() { + text_buffer_.clear(); + line_offsets_.clear(); + line_offsets_.push_back(0); +} + +void LogProcessor::Draw(const char* title, bool* p_open) { + ImGui::Spacing(); + + // Options menu + if (ImGui::BeginPopup("Options")) { + ImGui::Checkbox("Auto-scroll", &auto_scroll_); + ImGui::EndPopup(); + } + + // Main window + if (ImGui::Button("Options")) ImGui::OpenPopup("Options"); + ImGui::SameLine(); + bool clear = ImGui::Button("Clear"); + ImGui::SameLine(); + bool copy = ImGui::Button("Copy"); + ImGui::SameLine(); + text_filter_.Draw("Filter", -100.0f); + + ImGui::Separator(); + + if (ImGui::BeginChild("scrolling", ImVec2(0, 0), ImGuiChildFlags_None, + ImGuiWindowFlags_HorizontalScrollbar)) { + if (clear) Clear(); + if (copy) ImGui::LogToClipboard(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + const char* buf = text_buffer_.begin(); + const char* buf_end = text_buffer_.end(); + if (text_filter_.IsActive()) { + // In this example we don't use the clipper when text_filter_ is enabled. + // This is because we don't have random access to the result of our + // filter. A real application processing logs with ten of thousands of + // entries may want to store the result of search/filter.. especially + // if the filtering function is not trivial (e.g. reg-exp). + for (int line_no = 0; line_no < line_offsets_.Size; line_no++) { + const char* line_start = buf + line_offsets_[line_no]; + const char* line_end = (line_no + 1 < line_offsets_.Size) + ? (buf + line_offsets_[line_no + 1] - 1) + : buf_end; + if (text_filter_.PassFilter(line_start, line_end)) + ImGui::TextUnformatted(line_start, line_end); + } + } else { + // The simplest and easy way to display the entire buffer: + // ImGui::TextUnformatted(buf_begin, buf_end); + // And it'll just work. TextUnformatted() has specialization for large + // blob of text and will fast-forward to skip non-visible lines. Here + // we instead demonstrate using the clipper to only process lines that + // are within the visible area. If you have tens of thousands of items + // and their processing cost is non-negligible, coarse clipping them + // on your side is recommended. Using ImGuiListClipper requires + // - A) random access into your data + // - B) items all being the same height, + // both of which we can handle since we have an array pointing to the + // beginning of each line of text. When using the filter (in the block + // of code above) we don't have random access into the data to display + // anymore, which is why we don't use the clipper. Storing or skimming + // through the search result would make it possible (and would be + // recommended if you want to search through tens of thousands of + // entries). + ImGuiListClipper clipper; + clipper.Begin(line_offsets_.Size); + while (clipper.Step()) { + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; + line_no++) { + const char* line_start = buf + line_offsets_[line_no]; + const char* line_end = (line_no + 1 < line_offsets_.Size) + ? (buf + line_offsets_[line_no + 1] - 1) + : buf_end; + ImGui::TextUnformatted(line_start, line_end); + } + } + clipper.End(); + } + ImGui::PopStyleVar(); + + // Keep up at the bottom of the scroll region if we were already at the + // bottom at the beginning of the frame. Using a scrollbar or + // mouse-wheel will take away from the bottom edge. + if (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + } + ImGui::EndChild(); + // ImGui::End(); +} +} // namespace quickviz \ No newline at end of file diff --git a/src/app/component/log_processor.hpp b/src/app/component/log_processor.hpp new file mode 100644 index 0000000..f79b777 --- /dev/null +++ b/src/app/component/log_processor.hpp @@ -0,0 +1,43 @@ +/* + * @file log_processor.hpp + * @date 11/3/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef QUICKVIZ_LOG_PROCESSOR_HPP +#define QUICKVIZ_LOG_PROCESSOR_HPP + +#include "imgui.h" + +namespace quickviz { +class LogProcessor { + public: + LogProcessor(); + + // public methods + void Clear(); + + void AddLog(const char* fmt, ...) IM_FMTARGS(2) { + int old_size = text_buffer_.size(); + va_list args; + va_start(args, fmt); + text_buffer_.appendfv(fmt, args); + va_end(args); + for (int new_size = text_buffer_.size(); old_size < new_size; old_size++) + if (text_buffer_[old_size] == '\n') line_offsets_.push_back(old_size + 1); + } + + void Draw(const char* title, bool* p_open = NULL); + + private: + ImGuiTextBuffer text_buffer_; + ImGuiTextFilter text_filter_; + ImVector line_offsets_; // Index to lines offset. We maintain this with + // AddLog() calls. + bool auto_scroll_; // Keep scrolling if already at the bottom. +}; +} // namespace quickviz + +#endif // QUICKVIZ_LOG_PROCESSOR_HPP \ No newline at end of file diff --git a/src/app/panels/config_panel.cpp b/src/app/panels/config_panel.cpp index 70e1fd3..9d9f812 100644 --- a/src/app/panels/config_panel.cpp +++ b/src/app/panels/config_panel.cpp @@ -21,8 +21,28 @@ ConfigPanel::ConfigPanel(std::string name) : Panel(name) { void ConfigPanel::Draw() { Begin(); + ImVec2 content_size = ImGui::GetContentRegionAvail(); + ImGui::PushFont(Fonts::GetFont(FontSize::kFont16)); { + //----------------------- view settings -----------------------// + if (ImGui::CollapsingHeader("Views")) { + ImGui::Indent(16.0f); + ImGui::PushItemWidth(80); + { + ImGui::SetNextItemWidth(content_size.x - 80); + static int item_current_2 = 0; + ImGui::Combo("##view-selection-combo", &item_current_2, + "Obit\0TopDown\0\0"); + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + } + } + ImGui::PopItemWidth(); + ImGui::Unindent(16.0f); + } + + //----------------------- help -----------------------// if (ImGui::CollapsingHeader("Help")) { ImGui::SeparatorText("ABOUT THIS DEMO:"); ImGui::BulletText( @@ -52,9 +72,13 @@ void ConfigPanel::Draw() { // ImGui::PushFont(Fonts::GetFont(FontSize::kFont18)); // ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 153, 153, 200)); - // ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", - // 1000.0f / ImGui::GetIO().Framerate, - // ImGui::GetIO().Framerate); + ImGui::SetCursorPos(ImVec2(0, ImGui::GetWindowHeight() - 35)); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::Indent(16.0f); + ImGui::Text("Frame update rate: %.3f ms/frame (%.1f FPS)", + 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); + ImGui::Unindent(16.0f); // ImGui::PopStyleColor(); // ImGui::PopFont(); // ImGui::Spacing(); diff --git a/src/app/panels/console_panel.cpp b/src/app/panels/console_panel.cpp index 1dd25bf..8252bb9 100644 --- a/src/app/panels/console_panel.cpp +++ b/src/app/panels/console_panel.cpp @@ -16,18 +16,25 @@ ConsolePanel::ConsolePanel(std::string name) : Panel(name) { // this->SetNoResize(true); this->SetNoMove(true); this->SetWindowNoMenuButton(); + + static int counter = 0; + const char* categories[3] = {"info", "warn", "error"}; + const char* words[] = {"Bumfuzzled", "Cattywampus", "Snickersnee", + "Abibliophobia", "Absquatulate", "Nincompoop", + "Pauciloquent"}; + for (int n = 0; n < 5; n++) { + const char* category = categories[counter % IM_ARRAYSIZE(categories)]; + const char* word = words[counter % IM_ARRAYSIZE(words)]; + log_.AddLog( + "[%05d] [%s] Hello, current time is %.1f, here's a word: '%s'\n", + ImGui::GetFrameCount(), category, ImGui::GetTime(), word); + counter++; + } } void ConsolePanel::Draw() { Begin(); - { - ImGui::PushFont(Fonts::GetFont(FontSize::kFont18)); - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 153, 153, 200)); - ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", - 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); - ImGui::PopStyleColor(); - ImGui::PopFont(); - } + log_.Draw("Example: Log"); End(); } } // namespace quickviz \ No newline at end of file diff --git a/src/app/panels/console_panel.hpp b/src/app/panels/console_panel.hpp index 2a5b3d0..03eb165 100644 --- a/src/app/panels/console_panel.hpp +++ b/src/app/panels/console_panel.hpp @@ -10,6 +10,7 @@ #define QUICKVIZ_CONSOLE_PANEL_HPP #include "imview/panel.hpp" +#include "component/log_processor.hpp" namespace quickviz { class ConsolePanel : public Panel { @@ -17,6 +18,9 @@ class ConsolePanel : public Panel { ConsolePanel(std::string name = "Debug"); void Draw() override; + + private: + LogProcessor log_; }; } // namespace quickviz diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index afb3de8..152d2b9 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -31,18 +31,16 @@ add_library(imview src/widget/rt_line_plot_widget.cpp src/widget/gl_widget.cpp # components + src/component/image_utils.cpp src/component/cairo_context.cpp src/component/cairo_draw.cpp - src/component/image_utils.cpp - src/component/shader.cpp - src/component/shader_program.cpp - src/component/frame_buffer.cpp - # opengl primitives - src/primitive/grid.cpp - # data buffer + src/component/opengl/shader.cpp + src/component/opengl/shader_program.cpp + src/component/opengl/frame_buffer.cpp + src/component/opengl/grid.cpp + src/component/opengl/camera.cpp src/component/buffer/buffer_registry.cpp src/component/buffer/scrolling_plot_buffer.cpp - # event handling src/component/event/event_dispatcher.cpp src/component/event/async_event_dispatcher.cpp ) diff --git a/src/imview/include/imview/component/opengl/camera.hpp b/src/imview/include/imview/component/opengl/camera.hpp new file mode 100644 index 0000000..acdacb0 --- /dev/null +++ b/src/imview/include/imview/component/opengl/camera.hpp @@ -0,0 +1,78 @@ +/* + * @file camera.hpp + * @date 11/3/24 + * @brief a perspective camera class + * + * V_clip = P * V * M * V_local + * P: projection matrix + * V: view matrix + * M: model matrix + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef QUICKVIZ_CAMERA_HPP +#define QUICKVIZ_CAMERA_HPP + +#include +#include + +namespace quickviz { +class Camera { + static constexpr float default_movement_speed = 2.5f; + static constexpr float default_mouse_sensitivity = 0.05f; + static constexpr float default_fov = 45.0f; + static constexpr float pitch_min = -89.0f; + static constexpr float pitch_max = 89.0f; + static constexpr float fov_min = 1.0f; + static constexpr float fov_max = 45.0f; + + struct State { + // camera pose + glm::vec3 position; + float yaw; + float pitch; + + // camera reference vectors + glm::vec3 front; + glm::vec3 up; + glm::vec3 right; + }; + + public: + enum class Movement { kForward, kBackward, kLeft, kRight }; + + public: + Camera(glm::vec3 position, float yaw, float pitch, float fov = default_fov); + + void SetWorldUpVector(glm::vec3 up) { world_up_ = up; } + + void Reset(); + glm::mat4 GetViewMatrix() const; + glm::mat4 GetProjectionMatrix(float aspect_ratio, float z_near = 0.1f, + float z_far = 100.0f) const; + + void ProcessKeyboard(Movement direction, float dt); + void ProcessMouseMovement(float x_offset, float y_offset, + bool constrain_pitch = true); + void ProcessMouseScroll(float y_offset); + + glm::vec3 GetPosition() const { return current_state_.position; } + glm::vec3 GetFront() const { return current_state_.front; } + + private: + void UpdateCameraVectors(); + + // default to (0, 1, 0) unless specified otherwise + glm::vec3 world_up_ = glm::vec3(0.0f, 1.0f, 0.0f); + + State initial_state_; + State current_state_; + + float movement_speed_ = default_movement_speed; + float mouse_sensitivity_ = default_mouse_sensitivity; + float fov_ = default_fov; +}; +} // namespace quickviz + +#endif // QUICKVIZ_CAMERA_HPP \ No newline at end of file diff --git a/src/imview/include/imview/component/frame_buffer.hpp b/src/imview/include/imview/component/opengl/frame_buffer.hpp similarity index 100% rename from src/imview/include/imview/component/frame_buffer.hpp rename to src/imview/include/imview/component/opengl/frame_buffer.hpp diff --git a/src/imview/include/imview/primitive/grid.hpp b/src/imview/include/imview/component/opengl/grid.hpp similarity index 79% rename from src/imview/include/imview/primitive/grid.hpp rename to src/imview/include/imview/component/opengl/grid.hpp index 000c36f..38b81c0 100644 --- a/src/imview/include/imview/primitive/grid.hpp +++ b/src/imview/include/imview/component/opengl/grid.hpp @@ -13,7 +13,7 @@ #include -#include "imview/component/shader_program.hpp" +#include "imview/component/opengl/shader_program.hpp" namespace quickviz { class Grid { @@ -22,15 +22,21 @@ class Grid { glm::vec3 color = glm::vec3(0.5f, 0.5f, 0.5f)); ~Grid(); + void SetLineColor(const glm::vec3& color, float alpha = 0.5f); + void Initialize(); void Draw(const glm::mat4& projection, const glm::mat4& view); private: void GenerateGrid(); + // grid parameters float grid_size_; float spacing_; glm::vec3 color_; + float alpha_ = 0.5f; + + // OpenGL related uint32_t vao_; uint32_t vbo_; std::vector vertices_; diff --git a/src/imview/include/imview/component/shader.hpp b/src/imview/include/imview/component/opengl/shader.hpp similarity index 100% rename from src/imview/include/imview/component/shader.hpp rename to src/imview/include/imview/component/opengl/shader.hpp diff --git a/src/imview/include/imview/component/shader_program.hpp b/src/imview/include/imview/component/opengl/shader_program.hpp similarity index 95% rename from src/imview/include/imview/component/shader_program.hpp rename to src/imview/include/imview/component/opengl/shader_program.hpp index ef3afd2..c43f716 100644 --- a/src/imview/include/imview/component/shader_program.hpp +++ b/src/imview/include/imview/component/opengl/shader_program.hpp @@ -14,7 +14,7 @@ #include #include -#include "imview/component/shader.hpp" +#include "imview/component/opengl/shader.hpp" namespace quickviz { class ShaderProgram { diff --git a/src/imview/include/imview/widget/gl_widget.hpp b/src/imview/include/imview/widget/gl_widget.hpp index 5680434..11ee65a 100644 --- a/src/imview/include/imview/widget/gl_widget.hpp +++ b/src/imview/include/imview/widget/gl_widget.hpp @@ -13,7 +13,7 @@ #include #include "imview/panel.hpp" -#include "imview/component/frame_buffer.hpp" +#include "imview/component/opengl/frame_buffer.hpp" namespace quickviz { class GlWidget : public Panel { diff --git a/src/imview/include/imview/window.hpp b/src/imview/include/imview/window.hpp index 30f8966..ca9bbd6 100644 --- a/src/imview/include/imview/window.hpp +++ b/src/imview/include/imview/window.hpp @@ -50,6 +50,9 @@ class Window { void PollEvents(); void SwapBuffers(); + // for testing purposes, not recommended for normal use + GLFWwindow *GetWindowObject(); + protected: void ApplyWindowHints(uint32_t window_hints); void LoadDefaultStyle(); diff --git a/src/imview/src/component/opengl/camera.cpp b/src/imview/src/component/opengl/camera.cpp new file mode 100644 index 0000000..e71a255 --- /dev/null +++ b/src/imview/src/component/opengl/camera.cpp @@ -0,0 +1,95 @@ +/* + * @file camera.cpp + * @date 11/3/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/component/opengl/camera.hpp" + +#include + +namespace quickviz { +Camera::Camera(glm::vec3 position, float yaw, float pitch, float fov) + : fov_(fov) { + current_state_.position = position; + current_state_.yaw = yaw; + current_state_.pitch = pitch; + + UpdateCameraVectors(); + + // save initial state for reset + initial_state_.position = position; + initial_state_.yaw = yaw; + initial_state_.pitch = pitch; + initial_state_.front = current_state_.front; + initial_state_.up = current_state_.up; + initial_state_.right = current_state_.right; +} + +void Camera::Reset() { + current_state_ = initial_state_; + UpdateCameraVectors(); +} + +glm::mat4 Camera::GetViewMatrix() const { + return glm::lookAt(current_state_.position, + current_state_.position + current_state_.front, + current_state_.up); +} + +glm::mat4 Camera::GetProjectionMatrix(float aspect_ratio, float z_near, + float z_far) const { + return glm::perspective(glm::radians(fov_), aspect_ratio, z_near, z_far); +} + +void Camera::UpdateCameraVectors() { + glm::vec3 front; + front.x = std::cos(glm::radians(current_state_.yaw)) * + std::cos(glm::radians(current_state_.pitch)); + front.y = std::sin(glm::radians(current_state_.pitch)); + front.z = std::sin(glm::radians(current_state_.yaw)) * + std::cos(glm::radians(current_state_.pitch)); + current_state_.front = glm::normalize(front); + + current_state_.right = + glm::normalize(glm::cross(current_state_.front, world_up_)); + current_state_.up = + glm::normalize(glm::cross(current_state_.right, current_state_.front)); +} + +void Camera::ProcessKeyboard(Camera::Movement direction, float dt) { + float velocity = movement_speed_ * dt; + if (direction == Movement::kForward) + current_state_.position += current_state_.front * velocity; + if (direction == Movement::kBackward) + current_state_.position -= current_state_.front * velocity; + if (direction == Movement::kLeft) + current_state_.position -= current_state_.right * velocity; + if (direction == Movement::kRight) + current_state_.position += current_state_.right * velocity; +} + +void Camera::ProcessMouseMovement(float x_offset, float y_offset, + bool constrain_pitch) { + x_offset *= mouse_sensitivity_; + y_offset *= mouse_sensitivity_; + + current_state_.yaw += x_offset; + current_state_.pitch += y_offset; + + if (constrain_pitch) { + if (current_state_.pitch > pitch_max) current_state_.pitch = pitch_max; + if (current_state_.pitch < pitch_min) current_state_.pitch = pitch_min; + } + + UpdateCameraVectors(); +} + +void Camera::ProcessMouseScroll(float y_offset) { + fov_ -= y_offset; + if (fov_ < fov_min) fov_ = fov_min; + if (fov_ > fov_max) fov_ = fov_max; +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/component/frame_buffer.cpp b/src/imview/src/component/opengl/frame_buffer.cpp similarity index 98% rename from src/imview/src/component/frame_buffer.cpp rename to src/imview/src/component/opengl/frame_buffer.cpp index d154192..516a634 100644 --- a/src/imview/src/component/frame_buffer.cpp +++ b/src/imview/src/component/opengl/frame_buffer.cpp @@ -6,7 +6,7 @@ * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/component/frame_buffer.hpp" +#include "imview/component/opengl/frame_buffer.hpp" #include diff --git a/src/imview/src/primitive/grid.cpp b/src/imview/src/component/opengl/grid.cpp similarity index 77% rename from src/imview/src/primitive/grid.cpp rename to src/imview/src/component/opengl/grid.cpp index 26a8adf..96c5d3e 100644 --- a/src/imview/src/primitive/grid.cpp +++ b/src/imview/src/component/opengl/grid.cpp @@ -6,7 +6,7 @@ * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/primitive/grid.hpp" +#include "imview/component/opengl/grid.hpp" #include @@ -32,9 +32,10 @@ std::string fragment_shader_source = R"( out vec4 FragColor; uniform vec3 lineColor; +uniform float lineAlpha; void main() { - FragColor = vec4(lineColor, 1.0); + FragColor = vec4(lineColor, lineAlpha); } )"; } // namespace @@ -58,9 +59,15 @@ Grid::~Grid() { glDeleteBuffers(1, &vbo_); } +void Grid::SetLineColor(const glm::vec3& color, float alpha) { + color_ = color; + alpha_ = alpha; +} + void Grid::Initialize() { shader_.Use(); shader_.SetUniform("lineColor", color_); + shader_.SetUniform("lineAlpha", alpha_); } void Grid::Draw(const glm::mat4& projection, const glm::mat4& view) { @@ -76,14 +83,15 @@ void Grid::Draw(const glm::mat4& projection, const glm::mat4& view) { void Grid::GenerateGrid() { // generate grid vertices along X and Z axes - for (float x = -grid_size_; x <= grid_size_; x += spacing_) { - vertices_.emplace_back(x, 0.0f, -grid_size_); - vertices_.emplace_back(x, 0.0f, grid_size_); + float half_grid_size = grid_size_ / 2.0f; + for (float x = -half_grid_size; x <= half_grid_size; x += spacing_) { + vertices_.emplace_back(x, 0.0f, -half_grid_size); + vertices_.emplace_back(x, 0.0f, half_grid_size); } - for (float z = -grid_size_; z <= grid_size_; z += spacing_) { - vertices_.emplace_back(-grid_size_, 0.0f, z); - vertices_.emplace_back(grid_size_, 0.0f, z); + for (float z = -half_grid_size; z <= half_grid_size; z += spacing_) { + vertices_.emplace_back(-half_grid_size, 0.0f, z); + vertices_.emplace_back(half_grid_size, 0.0f, z); } // set up VAO and VBO diff --git a/src/imview/src/component/shader.cpp b/src/imview/src/component/opengl/shader.cpp similarity index 98% rename from src/imview/src/component/shader.cpp rename to src/imview/src/component/opengl/shader.cpp index b1f1d8b..73d0f39 100644 --- a/src/imview/src/component/shader.cpp +++ b/src/imview/src/component/opengl/shader.cpp @@ -6,7 +6,7 @@ * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/component/shader.hpp" +#include "imview/component/opengl/shader.hpp" #include #include diff --git a/src/imview/src/component/shader_program.cpp b/src/imview/src/component/opengl/shader_program.cpp similarity index 97% rename from src/imview/src/component/shader_program.cpp rename to src/imview/src/component/opengl/shader_program.cpp index ba2d617..fc2e49f 100644 --- a/src/imview/src/component/shader_program.cpp +++ b/src/imview/src/component/opengl/shader_program.cpp @@ -6,7 +6,7 @@ * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "imview/component/shader_program.hpp" +#include "imview/component/opengl/shader_program.hpp" #include diff --git a/src/imview/src/viewer.cpp b/src/imview/src/viewer.cpp index 66e460c..2bdf4aa 100644 --- a/src/imview/src/viewer.cpp +++ b/src/imview/src/viewer.cpp @@ -196,9 +196,10 @@ void Viewer::SetWindowShouldClose() { } void Viewer::SetupOpenGL() { + glEnable(GL_MULTISAMPLE); glEnable(GL_DEPTH_TEST); - // glEnable(GL_BLEND); - // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } void Viewer::ClearBackground() { diff --git a/src/imview/src/widget/gl_widget.cpp b/src/imview/src/widget/gl_widget.cpp index b760c5c..cc01140 100644 --- a/src/imview/src/widget/gl_widget.cpp +++ b/src/imview/src/widget/gl_widget.cpp @@ -9,8 +9,7 @@ #include "imview/widget/gl_widget.hpp" namespace quickviz { -GlWidget::GlWidget(const std::string& widget_name) - : Panel(widget_name) { +GlWidget::GlWidget(const std::string& widget_name) : Panel(widget_name) { this->SetAutoLayout(false); this->SetWindowNoMenuButton(); this->SetNoBackground(true); @@ -25,9 +24,9 @@ void GlWidget::SetGlRenderFunction(GlWidget::GlRenderFunction func) { void GlWidget::Draw() { Begin(); { - ImVec2 contentSize = ImGui::GetContentRegionAvail(); - float width = contentSize.x; - float height = contentSize.y; + ImVec2 content_size = ImGui::GetContentRegionAvail(); + float width = content_size.x; + float height = content_size.y; frame_buffer_->Resize(width, height); frame_buffer_->Bind(); diff --git a/src/imview/src/window.cpp b/src/imview/src/window.cpp index dba6ae3..5a1fb48 100644 --- a/src/imview/src/window.cpp +++ b/src/imview/src/window.cpp @@ -73,6 +73,8 @@ void Window::ApplyWindowHints(uint32_t window_hints) { // glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only #endif + glfwWindowHint(GLFW_SAMPLES, 4); + // optional hints if (window_hints & WIN_FOCUSED) { glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE); @@ -133,4 +135,6 @@ void Window::SwapBuffers() { glfwSwapBuffers(win_); } void Window::CloseWindow() { glfwSetWindowShouldClose(win_, 1); } bool Window::ShouldClose() const { return glfwWindowShouldClose(win_); } + +GLFWwindow *Window::GetWindowObject() { return win_; } } // namespace quickviz \ No newline at end of file diff --git a/src/imview/test/CMakeLists.txt b/src/imview/test/CMakeLists.txt index a7fb7f5..38f280f 100644 --- a/src/imview/test/CMakeLists.txt +++ b/src/imview/test/CMakeLists.txt @@ -28,6 +28,9 @@ target_link_libraries(test_shader PRIVATE imview) add_executable(test_grid test_grid.cpp) target_link_libraries(test_grid PRIVATE imview) +add_executable(test_camera test_camera.cpp) +target_link_libraries(test_camera PRIVATE imview) + find_package(OpenCV QUIET) if (OpenCV_FOUND) add_executable(test_double_buffer test_double_buffer.cpp) diff --git a/src/imview/test/test_camera.cpp b/src/imview/test/test_camera.cpp new file mode 100644 index 0000000..b144362 --- /dev/null +++ b/src/imview/test/test_camera.cpp @@ -0,0 +1,111 @@ +/* + * test_wgui.cpp + * + * Created on: Jul 22, 2021 14:50 + * Description: + * + * Copyright (c) 2021 Ruixiang Du (rdu) + */ + +#include + +#include +#include + +#include "glad/glad.h" +#include "imview/window.hpp" +#include "imview/component/opengl/grid.hpp" +#include "imview/component/opengl/camera.hpp" + +using namespace quickviz; + +float deltaTime = 0.0f; // Time between current frame and last frame +float lastFrame = 0.0f; // Time of last frame + +bool firstMouse = true; +float lastX = 1920 / 2.0f; +float lastY = 1080 / 2.0f; + +Camera camera(glm::vec3(0.0f, 3.0f, 8.0f), -90.0f, -25.0f); + +void ProcessInput(GLFWwindow* window) { + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { + std::cout << "W key pressed" << std::endl; + camera.ProcessKeyboard(Camera::Movement::kForward, deltaTime); + } + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { + std::cout << "S key pressed" << std::endl; + camera.ProcessKeyboard(Camera::Movement::kBackward, deltaTime); + } + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { + std::cout << "A key pressed" << std::endl; + camera.ProcessKeyboard(Camera::Movement::kLeft, deltaTime); + } + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { + std::cout << "D key pressed" << std::endl; + camera.ProcessKeyboard(Camera::Movement::kRight, deltaTime); + } + if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) { + camera.Reset(); + } +} + +void MouseCallback(GLFWwindow* window, double xpos, double ypos) { + if (firstMouse) { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + float xoffset = xpos - lastX; + float yoffset = + lastY - ypos; // Reversed since y-coordinates go from bottom to top + lastX = xpos; + lastY = ypos; + + camera.ProcessMouseMovement(xoffset, yoffset); +} + +void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + camera.ProcessMouseScroll(yoffset); +} + +int main(int argc, char* argv[]) { + int width = 1920; + int height = 1080; + Window win("Test Window", width, height); + + glfwSetCursorPosCallback(win.GetWindowObject(), MouseCallback); + glfwSetScrollCallback(win.GetWindowObject(), ScrollCallback); + glfwSetInputMode(win.GetWindowObject(), GLFW_CURSOR, GLFW_CURSOR_DISABLED); + + glEnable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // set up grid + Grid grid(10.0f, 1.0f, glm::vec3(0.7f, 0.7f, 0.7f)); + grid.Initialize(); + + while (!win.ShouldClose()) { + win.PollEvents(); + + // view + ProcessInput(win.GetWindowObject()); + float currentFrame = glfwGetTime(); + deltaTime = currentFrame - lastFrame; + lastFrame = currentFrame; + + glm::mat4 projection = + camera.GetProjectionMatrix(static_cast(width) / height); + glm::mat4 view = camera.GetViewMatrix(); + + // render + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + grid.Draw(projection, view); + + win.SwapBuffers(); + } + + return 0; +} diff --git a/src/imview/test/test_framebuffer.cpp b/src/imview/test/test_framebuffer.cpp index 6717262..8f57af8 100644 --- a/src/imview/test/test_framebuffer.cpp +++ b/src/imview/test/test_framebuffer.cpp @@ -11,7 +11,7 @@ #include "glad/glad.h" #include "imview/window.hpp" -#include "imview/component/frame_buffer.hpp" +#include "imview/component/opengl/frame_buffer.hpp" using namespace quickviz; diff --git a/src/imview/test/test_grid.cpp b/src/imview/test/test_grid.cpp index 7645025..f6b2c19 100644 --- a/src/imview/test/test_grid.cpp +++ b/src/imview/test/test_grid.cpp @@ -14,7 +14,7 @@ #include "glad/glad.h" #include "imview/window.hpp" -#include "imview/primitive/grid.hpp" +#include "imview/component/opengl/grid.hpp" using namespace quickviz; diff --git a/src/imview/test/test_shader.cpp b/src/imview/test/test_shader.cpp index e1b39d2..4a548ad 100644 --- a/src/imview/test/test_shader.cpp +++ b/src/imview/test/test_shader.cpp @@ -9,8 +9,8 @@ #include #include "imview/window.hpp" -#include "imview/component/shader.hpp" -#include "imview/component/shader_program.hpp" +#include "imview/component/opengl/shader.hpp" +#include "imview/component/opengl/shader_program.hpp" using namespace quickviz; From 62dc3feb77562b8a5373a567fcece10b1d73391d Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Mon, 4 Nov 2024 23:24:09 +0800 Subject: [PATCH 14/20] updated opengl widget --- src/app/CMakeLists.txt | 1 + src/app/panels/main_docking_panel.hpp | 4 +- src/app/panels/scene_panel.cpp | 47 ++++++++ src/app/panels/scene_panel.hpp | 26 +++++ src/imview/CMakeLists.txt | 2 + .../component/opengl/camera_controller.hpp | 18 +++ .../imview/component/opengl/frame_buffer.hpp | 2 +- .../include/imview/component/opengl/grid.hpp | 6 +- .../imview/component/opengl/triangle.hpp | 45 ++++++++ .../imview/interface/opengl_drawable.hpp | 24 ++++ .../include/imview/widget/gl_widget.hpp | 17 ++- .../component/opengl/camera_controller.cpp | 11 ++ .../src/component/opengl/frame_buffer.cpp | 14 +-- src/imview/src/component/opengl/grid.cpp | 10 +- src/imview/src/component/opengl/triangle.cpp | 108 ++++++++++++++++++ src/imview/src/widget/gl_widget.cpp | 24 +++- src/imview/test/feature/CMakeLists.txt | 4 +- .../test/feature/test_gl_scene_widget.cpp | 86 -------------- src/imview/test/feature/test_gl_widget.cpp | 38 ++++++ src/imview/test/test_camera.cpp | 3 +- src/imview/test/test_grid.cpp | 3 +- 21 files changed, 372 insertions(+), 121 deletions(-) create mode 100644 src/app/panels/scene_panel.cpp create mode 100644 src/app/panels/scene_panel.hpp create mode 100644 src/imview/include/imview/component/opengl/camera_controller.hpp create mode 100644 src/imview/include/imview/component/opengl/triangle.hpp create mode 100644 src/imview/include/imview/interface/opengl_drawable.hpp create mode 100644 src/imview/src/component/opengl/camera_controller.cpp create mode 100644 src/imview/src/component/opengl/triangle.cpp delete mode 100644 src/imview/test/feature/test_gl_scene_widget.cpp create mode 100644 src/imview/test/feature/test_gl_widget.cpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index a1b15ea..57e4787 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -7,6 +7,7 @@ if (BUILD_QUICKVIZ_APP) # panels panels/menu_bar.cpp panels/main_docking_panel.cpp + panels/scene_panel.cpp panels/config_panel.cpp panels/console_panel.cpp) target_link_libraries(quickviz PRIVATE imview) diff --git a/src/app/panels/main_docking_panel.hpp b/src/app/panels/main_docking_panel.hpp index 8a7e905..a4f7e01 100644 --- a/src/app/panels/main_docking_panel.hpp +++ b/src/app/panels/main_docking_panel.hpp @@ -15,6 +15,7 @@ #include "panels/menu_bar.hpp" #include "panels/config_panel.hpp" +#include "panels/scene_panel.hpp" #include "panels/console_panel.hpp" namespace quickviz { @@ -36,7 +37,8 @@ class MainDockingPanel : public Panel { ConfigPanel config_panel_{"Config"}; ConsolePanel console_panel_{"Console"}; - GlWidget gl_widget_{"Scene"}; + // GlWidget gl_widget_{"Scene"}; + ScenePanel gl_widget_{"Scene"}; }; } // namespace quickviz diff --git a/src/app/panels/scene_panel.cpp b/src/app/panels/scene_panel.cpp new file mode 100644 index 0000000..ec82c66 --- /dev/null +++ b/src/app/panels/scene_panel.cpp @@ -0,0 +1,47 @@ +/* + * @file scene_panel.cpp + * @date 11/4/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "panels/scene_panel.hpp" + +#include + +#include +#include + +namespace quickviz { +ScenePanel::ScenePanel(const std::string& panel_name) : GlWidget(panel_name) { + this->SetNoMove(true); + this->SetNoResize(true); + this->SetNoTitleBar(true); + this->SetNoBackground(true); + + auto grid = std::make_unique(10.0f, 1.0f, glm::vec3(0.7f, 0.7f, 0.7f)); + this->AddOpenGLObject("grid", std::move(grid)); +} + +void ScenePanel::Draw() { + ImVec2 content_size = ImGui::GetContentRegionAvail(); + // Orthographic projection for a top-down view + float aspect_ratio = + static_cast(content_size.x) / static_cast(content_size.y); + // std::cout << "aspect ratio: " << aspect_ratio << std::endl; + glm::mat4 projection = + glm::perspective(glm::radians(45.0f), aspect_ratio, 0.1f, 100.0f); + + // Simple view matrix looking at an angle + glm::mat4 view = glm::lookAt( + glm::vec3(10.0f, 10.0f, 10.0f), // Camera positioned at an angle + glm::vec3(0.0f, 0.0f, 0.0f), // Looking at the origin + glm::vec3(0.0f, 1.0f, 0.0f) // Up vector pointing along the Y-axis + ); + + UpdateView(projection, view); + + GlWidget::Draw(); +} +} // namespace quickviz \ No newline at end of file diff --git a/src/app/panels/scene_panel.hpp b/src/app/panels/scene_panel.hpp new file mode 100644 index 0000000..b0b5ed0 --- /dev/null +++ b/src/app/panels/scene_panel.hpp @@ -0,0 +1,26 @@ +/* + * @file scene_panel.hpp + * @date 11/4/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef QUICKVIZ_SCENE_PANEL_HPP +#define QUICKVIZ_SCENE_PANEL_HPP + +#include + +#include "imview/widget/gl_widget.hpp" +#include "imview/component/opengl/grid.hpp" + +namespace quickviz { +class ScenePanel : public GlWidget { + public: + ScenePanel(const std::string& panel_name); + + void Draw() override; +}; +} // namespace quickviz + +#endif // QUICKVIZ_SCENE_PANEL_HPP \ No newline at end of file diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index 152d2b9..2f44070 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -38,7 +38,9 @@ add_library(imview src/component/opengl/shader_program.cpp src/component/opengl/frame_buffer.cpp src/component/opengl/grid.cpp + src/component/opengl/triangle.cpp src/component/opengl/camera.cpp + src/component/opengl/camera_controller.cpp src/component/buffer/buffer_registry.cpp src/component/buffer/scrolling_plot_buffer.cpp src/component/event/event_dispatcher.cpp diff --git a/src/imview/include/imview/component/opengl/camera_controller.hpp b/src/imview/include/imview/component/opengl/camera_controller.hpp new file mode 100644 index 0000000..6cfeb93 --- /dev/null +++ b/src/imview/include/imview/component/opengl/camera_controller.hpp @@ -0,0 +1,18 @@ +/* + * @file camera_controller.hpp + * @date 11/3/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef QUICKVIZ_CAMERA_CONTROLLER_HPP +#define QUICKVIZ_CAMERA_CONTROLLER_HPP + +#include "imview/component/opengl/camera.hpp" + +namespace quickviz { +class CameraController {}; +} // namespace quickviz + +#endif // QUICKVIZ_CAMERA_CONTROLLER_HPP \ No newline at end of file diff --git a/src/imview/include/imview/component/opengl/frame_buffer.hpp b/src/imview/include/imview/component/opengl/frame_buffer.hpp index a97205a..ccbc349 100644 --- a/src/imview/include/imview/component/opengl/frame_buffer.hpp +++ b/src/imview/include/imview/component/opengl/frame_buffer.hpp @@ -24,7 +24,7 @@ class FrameBuffer { FrameBuffer& operator=(FrameBuffer&&) = delete; // public methods - void Bind(bool keep_aspect_ratio = true) const; + void Bind() const; void Unbind() const; void Clear(float r = 0.0, float g = 0.0, float b = 0.0, float a = 1.0) const; uint32_t GetTextureId() const { return texture_buffer_; } diff --git a/src/imview/include/imview/component/opengl/grid.hpp b/src/imview/include/imview/component/opengl/grid.hpp index 38b81c0..f3a3940 100644 --- a/src/imview/include/imview/component/opengl/grid.hpp +++ b/src/imview/include/imview/component/opengl/grid.hpp @@ -13,10 +13,11 @@ #include +#include "imview/interface/opengl_drawable.hpp" #include "imview/component/opengl/shader_program.hpp" namespace quickviz { -class Grid { +class Grid : public OpenGLDrawable { public: Grid(float grid_size = 10.0f, float spacing = 1.0f, glm::vec3 color = glm::vec3(0.5f, 0.5f, 0.5f)); @@ -24,8 +25,7 @@ class Grid { void SetLineColor(const glm::vec3& color, float alpha = 0.5f); - void Initialize(); - void Draw(const glm::mat4& projection, const glm::mat4& view); + void OnDraw(const glm::mat4& projection, const glm::mat4& view) override; private: void GenerateGrid(); diff --git a/src/imview/include/imview/component/opengl/triangle.hpp b/src/imview/include/imview/component/opengl/triangle.hpp new file mode 100644 index 0000000..6464603 --- /dev/null +++ b/src/imview/include/imview/component/opengl/triangle.hpp @@ -0,0 +1,45 @@ +/* + * @file triangle.hpp + * @date 11/4/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef QUICKVIZ_TRIANGLE_HPP +#define QUICKVIZ_TRIANGLE_HPP + +#include + +#include + +#include "imview/interface/opengl_drawable.hpp" +#include "imview/component/opengl/shader_program.hpp" + +namespace quickviz { +class Triangle : public OpenGLDrawable { + public: + Triangle(float size = 1.0f, glm::vec3 color = glm::vec3(0.5f, 0.5f, 0.5f)); + ~Triangle(); + + void SetColor(const glm::vec3& color, float alpha = 0.5f); + + void OnDraw(const glm::mat4& projection, const glm::mat4& view) override; + + private: + void GenerateTriangle(); + + // triangle parameters + float size_; + glm::vec3 color_; + float alpha_ = 0.5f; + + // OpenGL related + uint32_t vao_; + uint32_t vbo_; + std::vector vertices_; + ShaderProgram shader_; +}; +} // namespace quickviz + +#endif // QUICKVIZ_TRIANGLE_HPP \ No newline at end of file diff --git a/src/imview/include/imview/interface/opengl_drawable.hpp b/src/imview/include/imview/interface/opengl_drawable.hpp new file mode 100644 index 0000000..af4c107 --- /dev/null +++ b/src/imview/include/imview/interface/opengl_drawable.hpp @@ -0,0 +1,24 @@ +/* + * @file opengl_drawable.hpp + * @date 11/4/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef QUICKVIZ_OPENGL_DRAWABLE_HPP +#define QUICKVIZ_OPENGL_DRAWABLE_HPP + +#include + +namespace quickviz { +class OpenGLDrawable { + public: + virtual ~OpenGLDrawable() = default; + + /****** public methods ******/ + virtual void OnDraw(const glm::mat4& projection, const glm::mat4& view) = 0; +}; +} // namespace quickviz + +#endif // QUICKVIZ_OPENGL_DRAWABLE_HPP diff --git a/src/imview/include/imview/widget/gl_widget.hpp b/src/imview/include/imview/widget/gl_widget.hpp index 11ee65a..86dec84 100644 --- a/src/imview/include/imview/widget/gl_widget.hpp +++ b/src/imview/include/imview/widget/gl_widget.hpp @@ -11,9 +11,11 @@ #include #include +#include #include "imview/panel.hpp" #include "imview/component/opengl/frame_buffer.hpp" +#include "imview/interface/opengl_drawable.hpp" namespace quickviz { class GlWidget : public Panel { @@ -22,13 +24,20 @@ class GlWidget : public Panel { ~GlWidget() = default; // public methods - using GlRenderFunction = std::function; - void SetGlRenderFunction(GlRenderFunction func); + void AddOpenGLObject(const std::string& name, + std::unique_ptr object); + void RemoveOpenGLObject(const std::string& name); + void ClearOpenGLObjects(); + void UpdateView(const glm::mat4& projection, const glm::mat4& view); + void Draw() override; - private: - GlRenderFunction render_function_; + protected: std::unique_ptr frame_buffer_; + glm::mat4 projection_ = glm::mat4(1.0f); + glm::mat4 view_ = glm::mat4(1.0f); + std::unordered_map> + drawable_objects_; }; } // namespace quickviz diff --git a/src/imview/src/component/opengl/camera_controller.cpp b/src/imview/src/component/opengl/camera_controller.cpp new file mode 100644 index 0000000..1ca98bb --- /dev/null +++ b/src/imview/src/component/opengl/camera_controller.cpp @@ -0,0 +1,11 @@ +/* + * @file camera_controller.cpp + * @date 11/3/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/component/opengl/camera_controller.hpp" + +namespace quickviz {} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/component/opengl/frame_buffer.cpp b/src/imview/src/component/opengl/frame_buffer.cpp index 516a634..72b5a3f 100644 --- a/src/imview/src/component/opengl/frame_buffer.cpp +++ b/src/imview/src/component/opengl/frame_buffer.cpp @@ -63,16 +63,9 @@ void FrameBuffer::DestroyBuffers() { render_buffer_ = 0; } -void FrameBuffer::Bind(bool keep_aspect_ratio) const { +void FrameBuffer::Bind() const { glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_); - if (keep_aspect_ratio) { - float square_size = std::min(width_, height_); - int x_offset = (width_ - square_size) / 2; - int y_offset = (height_ - square_size) / 2; - glViewport(x_offset, y_offset, square_size, square_size); - } else { - glViewport(0, 0, width_, height_); - } + glViewport(0, 0, width_, height_); glEnable(GL_DEPTH_TEST); } @@ -84,7 +77,8 @@ void FrameBuffer::Clear(float r, float g, float b, float a) const { } void FrameBuffer::Resize(uint32_t width, uint32_t height) { - if (width == width_ && height == height_) return; + if ((width == width_ && height == height_) || (width == 0 || height == 0)) + return; width_ = width; height_ = height; diff --git a/src/imview/src/component/opengl/grid.cpp b/src/imview/src/component/opengl/grid.cpp index 96c5d3e..7b4d952 100644 --- a/src/imview/src/component/opengl/grid.cpp +++ b/src/imview/src/component/opengl/grid.cpp @@ -64,17 +64,13 @@ void Grid::SetLineColor(const glm::vec3& color, float alpha) { alpha_ = alpha; } -void Grid::Initialize() { - shader_.Use(); - shader_.SetUniform("lineColor", color_); - shader_.SetUniform("lineAlpha", alpha_); -} - -void Grid::Draw(const glm::mat4& projection, const glm::mat4& view) { +void Grid::OnDraw(const glm::mat4& projection, const glm::mat4& view) { shader_.Use(); shader_.SetUniform("projection", projection); shader_.SetUniform("view", view); shader_.SetUniform("model", glm::mat4(1.0f)); // Identity matrix for the grid + shader_.SetUniform("lineColor", color_); + shader_.SetUniform("lineAlpha", alpha_); glBindVertexArray(vao_); glDrawArrays(GL_LINES, 0, vertices_.size()); diff --git a/src/imview/src/component/opengl/triangle.cpp b/src/imview/src/component/opengl/triangle.cpp new file mode 100644 index 0000000..a23f1ab --- /dev/null +++ b/src/imview/src/component/opengl/triangle.cpp @@ -0,0 +1,108 @@ +/* + * @file triangle.cpp + * @date 11/4/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/component/opengl/triangle.hpp" + +#include "glad/glad.h" + +#include + +namespace quickviz { +namespace { +std::string vertex_shader_source = R"( +#version 330 core + +layout(location = 0) in vec3 aPos; +uniform mat4 projection; +uniform mat4 view; +uniform mat4 model; + +void main() { + gl_Position = projection * view * model * vec4(aPos, 1.0); +} +)"; + +std::string fragment_shader_source = R"( +#version 330 core + +out vec4 FragColor; +uniform vec3 lineColor; +uniform float lineAlpha; + +void main() { + FragColor = vec4(lineColor, lineAlpha); +} +)"; +} // namespace + +/////////////////////////////////////////////////////////////////////////////// + +Triangle::Triangle(float size, glm::vec3 color) : size_(size), color_(color) { + Shader vertex_shader(vertex_shader_source.c_str(), Shader::Type::kVertex); + Shader fragment_shader(fragment_shader_source.c_str(), + Shader::Type::kFragment); + shader_.AttachShader(vertex_shader); + shader_.AttachShader(fragment_shader); + if (!shader_.LinkProgram()) { + std::cout << "ERROR::GRID::SHADER_PROGRAM_LINKING_FAILED" << std::endl; + throw std::runtime_error("Shader program linking failed"); + } + GenerateTriangle(); +} + +Triangle::~Triangle() { + glDeleteVertexArrays(1, &vao_); + glDeleteBuffers(1, &vbo_); +} + +void Triangle::SetColor(const glm::vec3& color, float alpha) { + color_ = color; + alpha_ = alpha; +} + +void Triangle::OnDraw(const glm::mat4& projection, const glm::mat4& view) { + shader_.Use(); + shader_.SetUniform("projection", projection); + shader_.SetUniform("view", view); + shader_.SetUniform("model", glm::mat4(1.0f)); // Identity matrix for the grid + shader_.SetUniform("lineColor", color_); + shader_.SetUniform("lineAlpha", alpha_); + + glBindVertexArray(vao_); + glDrawArrays(GL_TRIANGLES, 0, vertices_.size()); + glBindVertexArray(0); +} + +void Triangle::GenerateTriangle() { + // clang-format off + vertices_ = { + glm::vec3(-0.5f, 0.0f, 0.0f), + glm::vec3(0.0f, 0.866f, 0.0f), + glm::vec3(0.5f, 0.0f, 0.0f) + }; + // clang-format on + + // create vertex array object + glGenVertexArrays(1, &vao_); + glBindVertexArray(vao_); + + // create vertex buffer object + glGenBuffers(1, &vbo_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, vertices_.size() * sizeof(glm::vec3), + vertices_.data(), GL_STATIC_DRAW); + + // set vertex attribute pointers + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); + glEnableVertexAttribArray(0); + + // unbind vbo and vao + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/widget/gl_widget.cpp b/src/imview/src/widget/gl_widget.cpp index cc01140..42b9bfd 100644 --- a/src/imview/src/widget/gl_widget.cpp +++ b/src/imview/src/widget/gl_widget.cpp @@ -17,8 +17,22 @@ GlWidget::GlWidget(const std::string& widget_name) : Panel(widget_name) { frame_buffer_ = std::make_unique(100, 50); } -void GlWidget::SetGlRenderFunction(GlWidget::GlRenderFunction func) { - render_function_ = func; +void GlWidget::AddOpenGLObject(const std::string& name, + std::unique_ptr object) { + drawable_objects_[name] = std::move(object); +} + +void GlWidget::RemoveOpenGLObject(const std::string& name) { + if (drawable_objects_.find(name) != drawable_objects_.end()) { + drawable_objects_.erase(name); + } +} + +void GlWidget::ClearOpenGLObjects() { drawable_objects_.clear(); } + +void GlWidget::UpdateView(const glm::mat4& projection, const glm::mat4& view) { + projection_ = projection; + view_ = view; } void GlWidget::Draw() { @@ -28,11 +42,15 @@ void GlWidget::Draw() { float width = content_size.x; float height = content_size.y; + // render to frame buffer frame_buffer_->Resize(width, height); frame_buffer_->Bind(); - if (render_function_ != nullptr) render_function_(*frame_buffer_.get()); + for (auto& obj : drawable_objects_) { + obj.second->OnDraw(projection_, view_); + } frame_buffer_->Unbind(); + // render frame buffer to ImGui ImVec2 uv0 = ImVec2(0, 1); ImVec2 uv1 = ImVec2(1, 0); ImVec4 tint_col = ImVec4(1, 1, 1, 1); diff --git a/src/imview/test/feature/CMakeLists.txt b/src/imview/test/feature/CMakeLists.txt index 32b20f5..d4cff86 100644 --- a/src/imview/test/feature/CMakeLists.txt +++ b/src/imview/test/feature/CMakeLists.txt @@ -18,5 +18,5 @@ target_link_libraries(test_cairo_widget PRIVATE imview) add_executable(test_implot_widget test_implot_widget.cpp) target_link_libraries(test_implot_widget PRIVATE imview) -add_executable(test_gl_scene_widget test_gl_scene_widget.cpp) -target_link_libraries(test_gl_scene_widget PRIVATE imview) +add_executable(test_gl_widget test_gl_widget.cpp) +target_link_libraries(test_gl_widget PRIVATE imview) diff --git a/src/imview/test/feature/test_gl_scene_widget.cpp b/src/imview/test/feature/test_gl_scene_widget.cpp deleted file mode 100644 index 9fb8ded..0000000 --- a/src/imview/test/feature/test_gl_scene_widget.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * test_cv_image_widget.cpp - * - * Created on: Jul 27, 2021 09:07 - * Description: - * - * Copyright (c) 2021 Ruixiang Du (rdu) - */ - -#include -#include - -#include - -#include "imview/viewer.hpp" -#include "imview/widget/gl_widget.hpp" - -using namespace quickviz; - -void RenderGL(const FrameBuffer& frame_buffer) { - frame_buffer.Clear(); - - // Define shapes enclosed within a pair of glBegin and glEnd - glBegin(GL_QUADS); // Each set of 4 vertices form a quad - glColor3f(1.0f, 0.0f, 0.0f); // Red - glVertex2f(-0.8f, 0.1f); // Define vertices in counter-clockwise (CCW) order - glVertex2f(-0.2f, 0.1f); // so that the normal (front-face) is facing you - glVertex2f(-0.2f, 0.7f); - glVertex2f(-0.8f, 0.7f); - - glColor3f(0.0f, 1.0f, 0.0f); // Green - glVertex2f(-0.7f, -0.6f); - glVertex2f(-0.1f, -0.6f); - glVertex2f(-0.1f, 0.0f); - glVertex2f(-0.7f, 0.0f); - - glColor3f(0.2f, 0.2f, 0.2f); // Dark Gray - glVertex2f(-0.9f, -0.7f); - glColor3f(1.0f, 1.0f, 1.0f); // White - glVertex2f(-0.5f, -0.7f); - glColor3f(0.2f, 0.2f, 0.2f); // Dark Gray - glVertex2f(-0.5f, -0.3f); - glColor3f(1.0f, 1.0f, 1.0f); // White - glVertex2f(-0.9f, -0.3f); - glEnd(); - - glBegin(GL_TRIANGLES); // Each set of 3 vertices form a triangle - glColor3f(0.0f, 0.0f, 1.0f); // Blue - glVertex2f(0.1f, -0.6f); - glVertex2f(0.7f, -0.6f); - glVertex2f(0.4f, -0.1f); - - glColor3f(1.0f, 0.0f, 0.0f); // Red - glVertex2f(0.3f, -0.4f); - glColor3f(0.0f, 1.0f, 0.0f); // Green - glVertex2f(0.9f, -0.4f); - glColor3f(0.0f, 0.0f, 1.0f); // Blue - glVertex2f(0.6f, -0.9f); - glEnd(); - - glBegin(GL_POLYGON); // These vertices form a closed polygon - glColor3f(1.0f, 1.0f, 0.0f); // Yellow - glVertex2f(0.4f, 0.2f); - glVertex2f(0.6f, 0.2f); - glVertex2f(0.7f, 0.4f); - glVertex2f(0.6f, 0.6f); - glVertex2f(0.4f, 0.6f); - glVertex2f(0.3f, 0.4f); - glEnd(); - - glFlush(); // Render now -} - -int main(int argc, char* argv[]) { - Viewer viewer; - - auto gl_widget = std::make_shared("OpenGL Scene"); - gl_widget->OnResize(300, 200); - gl_widget->SetPosition(0, 0); - gl_widget->SetGlRenderFunction(RenderGL); - viewer.AddSceneObject(gl_widget); - - viewer.Show(); - - return 0; -} \ No newline at end of file diff --git a/src/imview/test/feature/test_gl_widget.cpp b/src/imview/test/feature/test_gl_widget.cpp new file mode 100644 index 0000000..f81c957 --- /dev/null +++ b/src/imview/test/feature/test_gl_widget.cpp @@ -0,0 +1,38 @@ +/* + * test_cv_image_widget.cpp + * + * Created on: Jul 27, 2021 09:07 + * Description: + * + * Copyright (c) 2021 Ruixiang Du (rdu) + */ + +#include +#include + +#include +#include + +#include + +#include "imview/viewer.hpp" +#include "imview/widget/gl_widget.hpp" +#include "imview/component/opengl/triangle.hpp" + +using namespace quickviz; + +int main(int argc, char* argv[]) { + Viewer viewer; + + auto gl_widget = std::make_shared("OpenGL Scene"); + gl_widget->OnResize(300, 200); + gl_widget->SetPosition(0, 0); + gl_widget->UpdateView(glm::mat4(1.0f), glm::mat4(1.0f)); + auto triangle = std::make_unique(1.0f, glm::vec3(0.0f, 0.5f, 0.5f)); + gl_widget->AddOpenGLObject("triangle", std::move(triangle)); + viewer.AddSceneObject(gl_widget); + + viewer.Show(); + + return 0; +} \ No newline at end of file diff --git a/src/imview/test/test_camera.cpp b/src/imview/test/test_camera.cpp index b144362..82623eb 100644 --- a/src/imview/test/test_camera.cpp +++ b/src/imview/test/test_camera.cpp @@ -85,7 +85,6 @@ int main(int argc, char* argv[]) { // set up grid Grid grid(10.0f, 1.0f, glm::vec3(0.7f, 0.7f, 0.7f)); - grid.Initialize(); while (!win.ShouldClose()) { win.PollEvents(); @@ -102,7 +101,7 @@ int main(int argc, char* argv[]) { // render glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - grid.Draw(projection, view); + grid.OnDraw(projection, view); win.SwapBuffers(); } diff --git a/src/imview/test/test_grid.cpp b/src/imview/test/test_grid.cpp index f6b2c19..a6fc0aa 100644 --- a/src/imview/test/test_grid.cpp +++ b/src/imview/test/test_grid.cpp @@ -41,14 +41,13 @@ int main(int argc, char* argv[]) { // set up grid Grid grid(10.0f, 1.0f, glm::vec3(0.7f, 0.7f, 0.7f)); - grid.Initialize(); while (!win.ShouldClose()) { win.PollEvents(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - grid.Draw(projection, view); + grid.OnDraw(projection, view); win.SwapBuffers(); } From f3f75f091a6fa863e32d94690efe530459409854 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Tue, 5 Nov 2024 21:34:15 +0800 Subject: [PATCH 15/20] frame_buffer: updated aspect ratio handling --- src/app/panels/scene_panel.cpp | 1 + .../imview/component/opengl/frame_buffer.hpp | 4 ++- .../src/component/opengl/frame_buffer.cpp | 17 ++++++++-- src/imview/src/widget/gl_widget.cpp | 34 ++++++++++--------- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/app/panels/scene_panel.cpp b/src/app/panels/scene_panel.cpp index ec82c66..85fffc5 100644 --- a/src/app/panels/scene_panel.cpp +++ b/src/app/panels/scene_panel.cpp @@ -26,6 +26,7 @@ ScenePanel::ScenePanel(const std::string& panel_name) : GlWidget(panel_name) { void ScenePanel::Draw() { ImVec2 content_size = ImGui::GetContentRegionAvail(); + // Orthographic projection for a top-down view float aspect_ratio = static_cast(content_size.x) / static_cast(content_size.y); diff --git a/src/imview/include/imview/component/opengl/frame_buffer.hpp b/src/imview/include/imview/component/opengl/frame_buffer.hpp index ccbc349..43498af 100644 --- a/src/imview/include/imview/component/opengl/frame_buffer.hpp +++ b/src/imview/include/imview/component/opengl/frame_buffer.hpp @@ -24,7 +24,7 @@ class FrameBuffer { FrameBuffer& operator=(FrameBuffer&&) = delete; // public methods - void Bind() const; + void Bind(bool lock_aspect_ratio = true) const; void Unbind() const; void Clear(float r = 0.0, float g = 0.0, float b = 0.0, float a = 1.0) const; uint32_t GetTextureId() const { return texture_buffer_; } @@ -37,6 +37,8 @@ class FrameBuffer { void CreateBuffers(); void DestroyBuffers(); + float aspect_ratio_ = 1.0; + bool lock_aspect_ratio_ = true; uint32_t width_; uint32_t height_; uint32_t frame_buffer_; diff --git a/src/imview/src/component/opengl/frame_buffer.cpp b/src/imview/src/component/opengl/frame_buffer.cpp index 72b5a3f..aceae2c 100644 --- a/src/imview/src/component/opengl/frame_buffer.cpp +++ b/src/imview/src/component/opengl/frame_buffer.cpp @@ -19,6 +19,7 @@ FrameBuffer::FrameBuffer(uint32_t width, uint32_t height) frame_buffer_(0), texture_buffer_(0), render_buffer_(0) { + aspect_ratio_ = static_cast(width) / static_cast(height); CreateBuffers(); } @@ -63,9 +64,21 @@ void FrameBuffer::DestroyBuffers() { render_buffer_ = 0; } -void FrameBuffer::Bind() const { +void FrameBuffer::Bind(bool lock_aspect_ratio) const { glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_); - glViewport(0, 0, width_, height_); + + if (lock_aspect_ratio) { + float expected_width = height_ * aspect_ratio_; + if (expected_width > width_) { + float actual_height = width_ / aspect_ratio_; + glViewport(0, (height_ - actual_height) / 2, width_, actual_height); + } else { + float actual_width = expected_width; + glViewport((width_ - actual_width) / 2, 0, actual_width, height_); + } + } else { + glViewport(0, 0, width_, height_); + } glEnable(GL_DEPTH_TEST); } diff --git a/src/imview/src/widget/gl_widget.cpp b/src/imview/src/widget/gl_widget.cpp index 42b9bfd..e2628e8 100644 --- a/src/imview/src/widget/gl_widget.cpp +++ b/src/imview/src/widget/gl_widget.cpp @@ -13,8 +13,6 @@ GlWidget::GlWidget(const std::string& widget_name) : Panel(widget_name) { this->SetAutoLayout(false); this->SetWindowNoMenuButton(); this->SetNoBackground(true); - - frame_buffer_ = std::make_unique(100, 50); } void GlWidget::AddOpenGLObject(const std::string& name, @@ -42,21 +40,25 @@ void GlWidget::Draw() { float width = content_size.x; float height = content_size.y; - // render to frame buffer - frame_buffer_->Resize(width, height); - frame_buffer_->Bind(); - for (auto& obj : drawable_objects_) { - obj.second->OnDraw(projection_, view_); + if (frame_buffer_ != nullptr) { + // render to frame buffer + frame_buffer_->Resize(width, height); + frame_buffer_->Bind(); + for (auto& obj : drawable_objects_) { + obj.second->OnDraw(projection_, view_); + } + frame_buffer_->Unbind(); + + // render frame buffer to ImGui + ImVec2 uv0 = ImVec2(0, 1); + ImVec2 uv1 = ImVec2(1, 0); + ImVec4 tint_col = ImVec4(1, 1, 1, 1); + ImVec4 border_col = ImVec4(0, 0, 0, 0); + ImGui::Image((void*)(intptr_t)frame_buffer_->GetTextureId(), + ImVec2(width, height), uv0, uv1, tint_col, border_col); + } else { + frame_buffer_ = std::make_unique(width, height); } - frame_buffer_->Unbind(); - - // render frame buffer to ImGui - ImVec2 uv0 = ImVec2(0, 1); - ImVec2 uv1 = ImVec2(1, 0); - ImVec4 tint_col = ImVec4(1, 1, 1, 1); - ImVec4 border_col = ImVec4(0, 0, 0, 0); - ImGui::Image((void*)(intptr_t)frame_buffer_->GetTextureId(), - ImVec2(width, height), uv0, uv1, tint_col, border_col); } End(); } From 4b3eb84627fa3313250f285556622320de7c8d55 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Wed, 6 Nov 2024 00:17:20 +0800 Subject: [PATCH 16/20] opengl: added camera controller, to be tweaked --- src/app/panels/scene_panel.cpp | 17 ++--- src/app/panels/scene_panel.hpp | 4 + .../imview/component/opengl/camera.hpp | 24 ++++-- .../component/opengl/camera_controller.hpp | 23 +++++- .../include/imview/component/opengl/grid.hpp | 2 + .../imview/component/opengl/triangle.hpp | 2 + .../imview/interface/opengl_drawable.hpp | 2 + src/imview/src/component/opengl/camera.cpp | 27 +++++++ .../component/opengl/camera_controller.cpp | 74 ++++++++++++++++++- .../src/component/opengl/frame_buffer.cpp | 1 - src/imview/src/component/opengl/grid.cpp | 4 + src/imview/src/component/opengl/triangle.cpp | 4 + src/imview/test/test_camera.cpp | 49 +++++++++--- 13 files changed, 202 insertions(+), 31 deletions(-) diff --git a/src/app/panels/scene_panel.cpp b/src/app/panels/scene_panel.cpp index 85fffc5..8a5682c 100644 --- a/src/app/panels/scene_panel.cpp +++ b/src/app/panels/scene_panel.cpp @@ -20,6 +20,9 @@ ScenePanel::ScenePanel(const std::string& panel_name) : GlWidget(panel_name) { this->SetNoTitleBar(true); this->SetNoBackground(true); + camera_ = + std::make_unique(glm::vec3(0.0f, 3.0f, 8.0f), -90.0f, -25.0f); + auto grid = std::make_unique(10.0f, 1.0f, glm::vec3(0.7f, 0.7f, 0.7f)); this->AddOpenGLObject("grid", std::move(grid)); } @@ -27,19 +30,11 @@ ScenePanel::ScenePanel(const std::string& panel_name) : GlWidget(panel_name) { void ScenePanel::Draw() { ImVec2 content_size = ImGui::GetContentRegionAvail(); - // Orthographic projection for a top-down view + // get view matrices from camera float aspect_ratio = static_cast(content_size.x) / static_cast(content_size.y); - // std::cout << "aspect ratio: " << aspect_ratio << std::endl; - glm::mat4 projection = - glm::perspective(glm::radians(45.0f), aspect_ratio, 0.1f, 100.0f); - - // Simple view matrix looking at an angle - glm::mat4 view = glm::lookAt( - glm::vec3(10.0f, 10.0f, 10.0f), // Camera positioned at an angle - glm::vec3(0.0f, 0.0f, 0.0f), // Looking at the origin - glm::vec3(0.0f, 1.0f, 0.0f) // Up vector pointing along the Y-axis - ); + glm::mat4 projection = camera_->GetProjectionMatrix(aspect_ratio); + glm::mat4 view = camera_->GetViewMatrix(); UpdateView(projection, view); diff --git a/src/app/panels/scene_panel.hpp b/src/app/panels/scene_panel.hpp index b0b5ed0..e6f8f9c 100644 --- a/src/app/panels/scene_panel.hpp +++ b/src/app/panels/scene_panel.hpp @@ -13,6 +13,7 @@ #include "imview/widget/gl_widget.hpp" #include "imview/component/opengl/grid.hpp" +#include "imview/component/opengl/camera.hpp" namespace quickviz { class ScenePanel : public GlWidget { @@ -20,6 +21,9 @@ class ScenePanel : public GlWidget { ScenePanel(const std::string& panel_name); void Draw() override; + + private: + std::unique_ptr camera_; }; } // namespace quickviz diff --git a/src/imview/include/imview/component/opengl/camera.hpp b/src/imview/include/imview/component/opengl/camera.hpp index acdacb0..48fd82d 100644 --- a/src/imview/include/imview/component/opengl/camera.hpp +++ b/src/imview/include/imview/component/opengl/camera.hpp @@ -40,26 +40,36 @@ class Camera { }; public: - enum class Movement { kForward, kBackward, kLeft, kRight }; + enum class Movement { kForward, kBackward, kLeft, kRight, kUp, kDown }; public: Camera(glm::vec3 position, float yaw, float pitch, float fov = default_fov); - void SetWorldUpVector(glm::vec3 up) { world_up_ = up; } - - void Reset(); + // public methods glm::mat4 GetViewMatrix() const; glm::mat4 GetProjectionMatrix(float aspect_ratio, float z_near = 0.1f, float z_far = 100.0f) const; + void Reset(); + void SetWorldUpVector(glm::vec3 up); + void LookAt(const glm::vec3& target); + glm::vec3 GetFront() const { return current_state_.front; } + + void SetPosition(const glm::vec3& position); + glm::vec3 GetPosition() const { return current_state_.position; } + void SetYaw(float yaw); + float GetYaw() const { return current_state_.yaw; } + void SetPitch(float pitch); + float GetPitch() const { return current_state_.pitch; } + void SetFOV(float fov) { fov_ = fov; } + float GetFOV() const { return fov_; } + float GetMovementSpeed() const { return movement_speed_; } + void ProcessKeyboard(Movement direction, float dt); void ProcessMouseMovement(float x_offset, float y_offset, bool constrain_pitch = true); void ProcessMouseScroll(float y_offset); - glm::vec3 GetPosition() const { return current_state_.position; } - glm::vec3 GetFront() const { return current_state_.front; } - private: void UpdateCameraVectors(); diff --git a/src/imview/include/imview/component/opengl/camera_controller.hpp b/src/imview/include/imview/component/opengl/camera_controller.hpp index 6cfeb93..d0573f8 100644 --- a/src/imview/include/imview/component/opengl/camera_controller.hpp +++ b/src/imview/include/imview/component/opengl/camera_controller.hpp @@ -12,7 +12,28 @@ #include "imview/component/opengl/camera.hpp" namespace quickviz { -class CameraController {}; +class CameraController { + public: + enum class Mode { kFirstPerson, kOrbit, kTopDown, kFreeLook }; + using CameraMovement = Camera::Movement; + + public: + CameraController(Camera& camera); + + void SetMode(Mode mode); + void ProcessKeyboard(CameraMovement direction, float delta_time); + void ProcessMouseMovement(float x_offset, float y_offset); + void ProcessMouseScroll(float y_offset); + + private: + void UpdateOrbitPosition(); + + Camera& camera_; + Mode mode_ = Mode::kOrbit; + glm::vec3 orbit_target_ = glm::vec3(0.0f, 0.0f, 0.0f); + float orbit_distance_ = 10.0f; + float top_down_height_ = 10.0f; +}; } // namespace quickviz #endif // QUICKVIZ_CAMERA_CONTROLLER_HPP \ No newline at end of file diff --git a/src/imview/include/imview/component/opengl/grid.hpp b/src/imview/include/imview/component/opengl/grid.hpp index f3a3940..181528b 100644 --- a/src/imview/include/imview/component/opengl/grid.hpp +++ b/src/imview/include/imview/component/opengl/grid.hpp @@ -25,6 +25,8 @@ class Grid : public OpenGLDrawable { void SetLineColor(const glm::vec3& color, float alpha = 0.5f); + void InitGraphicsResources() override; + void DeinitGraphicsResources() override; void OnDraw(const glm::mat4& projection, const glm::mat4& view) override; private: diff --git a/src/imview/include/imview/component/opengl/triangle.hpp b/src/imview/include/imview/component/opengl/triangle.hpp index 6464603..74dea8c 100644 --- a/src/imview/include/imview/component/opengl/triangle.hpp +++ b/src/imview/include/imview/component/opengl/triangle.hpp @@ -24,6 +24,8 @@ class Triangle : public OpenGLDrawable { void SetColor(const glm::vec3& color, float alpha = 0.5f); + void InitGraphicsResources() override; + void DeinitGraphicsResources() override; void OnDraw(const glm::mat4& projection, const glm::mat4& view) override; private: diff --git a/src/imview/include/imview/interface/opengl_drawable.hpp b/src/imview/include/imview/interface/opengl_drawable.hpp index af4c107..7eeec67 100644 --- a/src/imview/include/imview/interface/opengl_drawable.hpp +++ b/src/imview/include/imview/interface/opengl_drawable.hpp @@ -17,6 +17,8 @@ class OpenGLDrawable { virtual ~OpenGLDrawable() = default; /****** public methods ******/ + virtual void InitGraphicsResources() = 0; + virtual void DeinitGraphicsResources() = 0; virtual void OnDraw(const glm::mat4& projection, const glm::mat4& view) = 0; }; } // namespace quickviz diff --git a/src/imview/src/component/opengl/camera.cpp b/src/imview/src/component/opengl/camera.cpp index e71a255..7fc4243 100644 --- a/src/imview/src/component/opengl/camera.cpp +++ b/src/imview/src/component/opengl/camera.cpp @@ -33,6 +33,11 @@ void Camera::Reset() { UpdateCameraVectors(); } +void Camera::SetWorldUpVector(glm::vec3 up) { + world_up_ = up; + UpdateCameraVectors(); +} + glm::mat4 Camera::GetViewMatrix() const { return glm::lookAt(current_state_.position, current_state_.position + current_state_.front, @@ -44,6 +49,28 @@ glm::mat4 Camera::GetProjectionMatrix(float aspect_ratio, float z_near, return glm::perspective(glm::radians(fov_), aspect_ratio, z_near, z_far); } +void Camera::SetPosition(const glm::vec3& position) { + current_state_.position = position; +} + +void Camera::SetYaw(float yaw) { + current_state_.yaw = yaw; + UpdateCameraVectors(); +} + +void Camera::SetPitch(float pitch) { + current_state_.pitch = pitch; + UpdateCameraVectors(); +} + +void Camera::LookAt(const glm::vec3& target) { + current_state_.front = glm::normalize(target - current_state_.position); + current_state_.right = + glm::normalize(glm::cross(current_state_.front, world_up_)); + current_state_.up = + glm::normalize(glm::cross(current_state_.right, current_state_.front)); +} + void Camera::UpdateCameraVectors() { glm::vec3 front; front.x = std::cos(glm::radians(current_state_.yaw)) * diff --git a/src/imview/src/component/opengl/camera_controller.cpp b/src/imview/src/component/opengl/camera_controller.cpp index 1ca98bb..cddf8e2 100644 --- a/src/imview/src/component/opengl/camera_controller.cpp +++ b/src/imview/src/component/opengl/camera_controller.cpp @@ -8,4 +8,76 @@ #include "imview/component/opengl/camera_controller.hpp" -namespace quickviz {} // namespace quickviz \ No newline at end of file +namespace quickviz { +CameraController::CameraController(Camera& camera) : camera_(camera) {} + +void CameraController::SetMode(CameraController::Mode mode) { + mode_ = mode; + if (mode == Mode::kTopDown) { + camera_.SetPosition(glm::vec3(0.0f, top_down_height_, 0.0f)); + camera_.SetPitch(-90.0f); + camera_.SetYaw(0.0f); + } +} + +void CameraController::ProcessKeyboard( + CameraController::CameraMovement direction, float delta_time) { + if (mode_ == Mode::kOrbit) return; + if (mode_ == Mode::kTopDown) { + float velocity = camera_.GetMovementSpeed() * delta_time; + glm::vec3 position = camera_.GetPosition(); + // Move only along X and Z axes + if (direction == CameraMovement::kForward) position.z -= velocity; + if (direction == CameraMovement::kBackward) position.z += velocity; + if (direction == CameraMovement::kLeft) position.x -= velocity; + if (direction == CameraMovement::kRight) position.x += velocity; + + camera_.SetPosition(position); // Update position without changing height + } + camera_.ProcessKeyboard(direction, delta_time); +} + +void CameraController::ProcessMouseMovement(float x_offset, float y_offset) { + switch (mode_) { + case Mode::kFirstPerson: + case Mode::kFreeLook: + camera_.ProcessMouseMovement(x_offset, y_offset); + break; + case Mode::kOrbit: + camera_.ProcessMouseMovement(x_offset, y_offset); + UpdateOrbitPosition(); + break; + case Mode::kTopDown: + // Ignore mouse movement for top-down view + break; + } +} + +void CameraController::ProcessMouseScroll(float y_offset) { + if (mode_ == Mode::kOrbit) { + orbit_distance_ -= y_offset; + if (orbit_distance_ < 1.0f) orbit_distance_ = 1.0f; + UpdateOrbitPosition(); + } else if (mode_ == Mode::kTopDown) { + glm::vec3 position = camera_.GetPosition(); + position.y -= y_offset; // Adjust height (Y position) with scroll + if (position.y < 1.0f) position.y = 1.0f; // Set a minimum height + camera_.SetPosition(position); + } else { + camera_.ProcessMouseScroll(y_offset); + } +} + +void CameraController::UpdateOrbitPosition() { + float cam_x = orbit_target_.x + orbit_distance_ * + cos(glm::radians(camera_.GetYaw())) * + cos(glm::radians(camera_.GetPitch())); + float cam_y = + orbit_target_.y + orbit_distance_ * sin(glm::radians(camera_.GetPitch())); + float cam_z = orbit_target_.z + orbit_distance_ * + sin(glm::radians(camera_.GetYaw())) * + cos(glm::radians(camera_.GetPitch())); + camera_.SetPosition(glm::vec3(cam_x, cam_y, cam_z)); + camera_.LookAt(orbit_target_); +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/component/opengl/frame_buffer.cpp b/src/imview/src/component/opengl/frame_buffer.cpp index aceae2c..ebd94e9 100644 --- a/src/imview/src/component/opengl/frame_buffer.cpp +++ b/src/imview/src/component/opengl/frame_buffer.cpp @@ -66,7 +66,6 @@ void FrameBuffer::DestroyBuffers() { void FrameBuffer::Bind(bool lock_aspect_ratio) const { glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_); - if (lock_aspect_ratio) { float expected_width = height_ * aspect_ratio_; if (expected_width > width_) { diff --git a/src/imview/src/component/opengl/grid.cpp b/src/imview/src/component/opengl/grid.cpp index 7b4d952..59ae56f 100644 --- a/src/imview/src/component/opengl/grid.cpp +++ b/src/imview/src/component/opengl/grid.cpp @@ -64,6 +64,10 @@ void Grid::SetLineColor(const glm::vec3& color, float alpha) { alpha_ = alpha; } +void Grid::InitGraphicsResources() {} + +void Grid::DeinitGraphicsResources() {} + void Grid::OnDraw(const glm::mat4& projection, const glm::mat4& view) { shader_.Use(); shader_.SetUniform("projection", projection); diff --git a/src/imview/src/component/opengl/triangle.cpp b/src/imview/src/component/opengl/triangle.cpp index a23f1ab..2a8f817 100644 --- a/src/imview/src/component/opengl/triangle.cpp +++ b/src/imview/src/component/opengl/triangle.cpp @@ -65,6 +65,10 @@ void Triangle::SetColor(const glm::vec3& color, float alpha) { alpha_ = alpha; } +void Triangle::InitGraphicsResources() {} + +void Triangle::DeinitGraphicsResources() {} + void Triangle::OnDraw(const glm::mat4& projection, const glm::mat4& view) { shader_.Use(); shader_.SetUniform("projection", projection); diff --git a/src/imview/test/test_camera.cpp b/src/imview/test/test_camera.cpp index 82623eb..c815137 100644 --- a/src/imview/test/test_camera.cpp +++ b/src/imview/test/test_camera.cpp @@ -16,6 +16,7 @@ #include "imview/window.hpp" #include "imview/component/opengl/grid.hpp" #include "imview/component/opengl/camera.hpp" +#include "imview/component/opengl/camera_controller.hpp" using namespace quickviz; @@ -27,23 +28,48 @@ float lastX = 1920 / 2.0f; float lastY = 1080 / 2.0f; Camera camera(glm::vec3(0.0f, 3.0f, 8.0f), -90.0f, -25.0f); +CameraController camera_controller(camera); void ProcessInput(GLFWwindow* window) { + // if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { + // std::cout << "W key pressed" << std::endl; + // camera.ProcessKeyboard(Camera::Movement::kForward, deltaTime); + // } + // if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { + // std::cout << "S key pressed" << std::endl; + // camera.ProcessKeyboard(Camera::Movement::kBackward, deltaTime); + // } + // if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { + // std::cout << "A key pressed" << std::endl; + // camera.ProcessKeyboard(Camera::Movement::kLeft, deltaTime); + // } + // if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { + // std::cout << "D key pressed" << std::endl; + // camera.ProcessKeyboard(Camera::Movement::kRight, deltaTime); + // } if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { - std::cout << "W key pressed" << std::endl; - camera.ProcessKeyboard(Camera::Movement::kForward, deltaTime); + camera_controller.ProcessKeyboard( + CameraController::CameraMovement::kForward, deltaTime); } if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { - std::cout << "S key pressed" << std::endl; - camera.ProcessKeyboard(Camera::Movement::kBackward, deltaTime); + camera_controller.ProcessKeyboard( + CameraController::CameraMovement::kBackward, deltaTime); } if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { - std::cout << "A key pressed" << std::endl; - camera.ProcessKeyboard(Camera::Movement::kLeft, deltaTime); + camera_controller.ProcessKeyboard(CameraController::CameraMovement::kLeft, + deltaTime); } if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { - std::cout << "D key pressed" << std::endl; - camera.ProcessKeyboard(Camera::Movement::kRight, deltaTime); + camera_controller.ProcessKeyboard(CameraController::CameraMovement::kRight, + deltaTime); + } + if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) { + camera_controller.ProcessKeyboard(CameraController::CameraMovement::kUp, + deltaTime); + } + if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) { + camera_controller.ProcessKeyboard(CameraController::CameraMovement::kDown, + deltaTime); } if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) { camera.Reset(); @@ -63,11 +89,13 @@ void MouseCallback(GLFWwindow* window, double xpos, double ypos) { lastX = xpos; lastY = ypos; - camera.ProcessMouseMovement(xoffset, yoffset); + // camera.ProcessMouseMovement(xoffset, yoffset); + camera_controller.ProcessMouseMovement(xoffset, yoffset); } void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { - camera.ProcessMouseScroll(yoffset); + // camera.ProcessMouseScroll(yoffset); + camera_controller.ProcessMouseScroll(yoffset); } int main(int argc, char* argv[]) { @@ -75,6 +103,7 @@ int main(int argc, char* argv[]) { int height = 1080; Window win("Test Window", width, height); + camera_controller.SetMode(CameraController::Mode::kTopDown); glfwSetCursorPosCallback(win.GetWindowObject(), MouseCallback); glfwSetScrollCallback(win.GetWindowObject(), ScrollCallback); glfwSetInputMode(win.GetWindowObject(), GLFW_CURSOR, GLFW_CURSOR_DISABLED); From 3a1feb4d6bfcbcca90e5f12fc5d951079c73e2a0 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Wed, 6 Nov 2024 22:27:46 +0800 Subject: [PATCH 17/20] event: updated event, added event source for better filtering --- docs/screenshots/quickviz-v0.1.png | Bin 0 -> 84899 bytes .../component/event/async_event_emitter.hpp | 6 +++--- .../include/imview/component/event/event.hpp | 17 ++++++++++++++-- .../imview/component/event/event_emitter.hpp | 6 +++--- .../component/opengl/camera_controller.hpp | 1 + .../component/opengl/camera_controller.cpp | 19 ++++++++++-------- src/imview/test/test_async_event.cpp | 11 +++++----- src/imview/test/test_camera.cpp | 4 ++-- src/imview/test/test_event.cpp | 7 ++++--- 9 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 docs/screenshots/quickviz-v0.1.png diff --git a/docs/screenshots/quickviz-v0.1.png b/docs/screenshots/quickviz-v0.1.png new file mode 100644 index 0000000000000000000000000000000000000000..dd743640b37d3e1fa319082966b9a2b155aedac7 GIT binary patch literal 84899 zcmeFZbyStz_b>V&5-MS$NU4-4je<0aiiCimB8{ZNrn^yI!61}I(pN#HLpEKay7Ncd;J{WbZ!l<@JOG=2`>=^bu7a8S24b#Q)YXM&hnSzDU$ z*c;iIm{{3AvUZpyEtdi!9+XJd&g7xPV{5B3cOP4tAQ~pdXZVHBC|f(75#$%VbcX+m zn2?B=;FU9~x6fSUztZ;W`aJ|WgUDaMdebGl{u1U1WRNrT@d(jk!DX$6%?7ww} zl9KI=Eaj=J6DKx=WLg)_G2O8J`tF@RgYi+%6BKPXW#04br~i_D>~8Y&{gk19Q5MLY z@%c9M7yq+>yD1kO%cKnz46pg6*sIkwIi{ab&s&w+D8yUd=SlH$lrFwxnCW>TT!CFa zXJJ<1e%d2NMMe8+X4~r=$`U-{vNw75ZlQn+#Lim@wypb-mghuD`)vX@e znprj8NOcX3$PZ!R0yFj8&Od9CB;|rq(%fv5)a30K7Xs)d#qCwoiqdHdMH0$8l5rKQ!<-X4L)V%g+%b#*5uCR%>{cbKg3@V4eBV(}gmpBcS3rIU56ZjC=XsrRX^qeJ7p)t`B?UHqsQ z$*cRey}doqO}9`-Ia9ARML4|p^3Ud9zuuP0l}ZK%27W%ak93dzXB*7-`>T`I4r9@2 zR6-~D^m@v4bHs#8g4Wm9K`icGkhc`c2bTJrwy?BpBuMWNJqC{@)V3rkcDQc^iu6V) z@)jD0W`YD{<$bz)L9@u=@yC5__gZr&yQMUlQZI%pc$X%J*!gR~fIf{ClbPl5qV}U- zcAOkxV(qS5VO;7tF7t!8%Xkr_qTP_VFv9fKW6M1~JzcZR!?nnHHX|tT^HAoo%BKb} zW8uPxh=?AC*4u@X8O4ImGd(Cri)YK+dsoX=O};%p!r+)G<+(nbs=s9pn~^gVB<_@w zk?FlPS-7*kVZS+FLl_QK(VOmx6}4aJ!g~pOt}oSnkPl5Mg5hQEfD$Bq{`}c|)xz9d zi&)Yf=%G9tp(>}X-5t|A+t_4ziz&Q(JM+vbSjz2`&!4#n@$Ocp`1m-x!q_;cUtEe2 z+;y|wI~%xIQK4g;%DA=pA(4sr;+Z0gd*pdX333Q}SZK`Q@dfAK@dG#!=0RKQA(%dQKI z%@P+|tCIQZ6-Tedn7-lB80)4@;$myL_s-^!^tMs&a)xvIeMJikizZP*tORj5lt}R2 zCX^5)XXO*5JQrFN`9|tE6L7Hhc;gX~U#U;~t7xT5wwA4RR^RzE*MC2$UsAonB<_Tf zTK=qZFWVQkIwljgI;AhSu&_{;WU45QXXF((FksB2X);I}l^%<4X!r=jP)wG7Wjc@|(v!H%?`>!O$T@{pQ)Ld&Kp zdSY`@e@D#ux1)yp;mwcITU>l)Ylh^s%&`Mi{+W)A)7b8eEr!*5mGdu8TolA~X3><> zg52=j25B<2#q+!ix))lZq3d`riE_V)H6X##P@^(jLQ5j>M~ zr}0u77%gpWA-myP5$Y$Q!_^GdO66W#sG2$DO=JD2=tF=rX3c-!h`wxbAEh*^4!Csb z((rTEpa-+-xu%WveBQ(cs?(RwCu}c$RKE}|wy6h0B^q7W<4T+*wu?hP$Ds1*it1Y6 zXj5GAFUPui0!R9TY(w9U2g@iPtygrqZ+qf-l7d8}j=|!$ zZ8C}`QdZw6wQZ$Z(WVkml?!4Lw&AI$oWq7sohFelQJb)}u{;_1K1l2W zXp*+nVl>`3SaRhSI6kLdR|0A$raP9wkmzp?p0sb*G#ilyfz9pEb2pjmuapk~D`^xg zDcqs&?FkDD{q5W*Sqxet*5SF-EQbn1Si=ZvvHCW8S3}Zz%-0Ksf+imog9u>HgQ!1# z{8)3cP~?zX$H(mK>=Z|~N!la}T`Td*U7(>-QxBW~4Sn}h*?c29qx0X9n?J}7q-9EUl&P;SZ7xSa-V-5UWbl@qFjA^09 z3$<*3Ak6waR~uiSl@)yW^)LA9?D!Z)aga$;gK-qW&i?wm0owy`uehq5za+0c`vPU} z8s}+H2bmL0PMr@$S9|PpXUjH5Q4ooX7J60z?X|PO+Uk|Mr8`;-QQreN3D1t^9iP%m zX^vGwJ$z(-=H=y?jJG5)NFLDUjTC2!wQ6)sd;3r4C`pXU%2(zWv)Cfn21j}=TjkViKg{j^;NbhbM48`W$8L6<~CKBlMhfa{dM?-vJ*evH8= z73yUCT1pq&|IZu((+`R;FfjbgelhXscGw?eC=+>PKP(UZ91lSE4E-b;{L=^_W8Dob z;N|&KSNV5q5t+&`7LewQ3}7$%@wsrpd)Xo~`JK+pgxKCIWbChq_V$(F2Kj}h)?{=a z&=1yF)<5vkPN&d6FAm=PXF;m`PYrfy5TB?2aYp8+_lpP%Czq9#MJPHvFfuZlm>BNg zYauh=He1p#LnvMOrO2@SH#okLtSXv@?!N7n6=?v1npp-{rMBmT3E4HwsQczEp2~r3 zmYDx`?D7Tgt;>yZ;?98<#g3B^4ZOwNsD;<_nExKYC@Lt&`twy^21NNBmm9wbt z1GiN4YyP)WHxKA?-6CE(LB(q!)O9MmrlqAtE%fxCMHo(&H?BmBtnz*4ixd|=A^`%& z2m(aUZy>Xg&|B)xDG!e7-mCPRwN-xCe24IiegY_{xc|CA!&0=PP%?!q@jcOYdXJAim&vGt$Hm} z`gkC~%Xq-KU$EE=r%e^_b@g~qJGMF>64HgT)lxnc=`9D;*8)BU8A?}oyd5WY*(}cS ze%a2}Y(C3m^Lx1UA(D%oD2OfAij3ugYDnTZG02}z?$b6=haXIG5) zp!S(f9^6l8a&q#D{2<^vumNcTo__=vBbl=Py2op4-DF|7ZoDJ2PYdtePs&ou=QaOo zbIxBH2lh+QyzP@Y?@ltR4MKPcA(7t>>KwP%@Q3WsDjR`GX?JeHwY?fn*13vPiYjuNpCTvZaO ztqlhYvp{A^F_6sZgg#e795G>sn84_@HsD$#-ung>w9L{Kt?FQwgkZ0w_ZB5y5HAp` zMate>^(7yf%EdQLb>*h2X`rwV#i^yUp4QCnQ`u~F?%-6Eq6JOTJ6N21Z0WDq7${#E z_zMks|9-_=@7}u@zq#~Ly7B9?Lqdk%p5qdvrDt7g#09N;OHg4ibzg0Qy%aRXMd^4h z|Gf8^VQ+n%f{rfd)_UymjjX{EE!zYX$q<%@sskHjX(hwX%KGiPS4FL?47^quP={XN5VC7AU>5?`XP(D;&vC$ynW+&(PEC~ zFu&CgWo1&`R>hX662w>)Jr0~yNK&JLeUMlmd*20~RS;$_0cMyUaP-9FBAAru7!(%CF{-+r-CB=e3sm>Y8H(_z|Hg-KhTD3I7(#A}!|QZLU{ z(W1thg2_SEkD4K(Z)bbe!c*A}qCOO21Tcv3g0nW*m`9@yuE+*gV1gY4J0=xABwc`Y z7h&)C03K*p2QkHPCU~G;x$Y_1A>k42KSo(th>7N@NINO5#A|R2gjaw%lC=co!PbO8 zl#1^H@v$ewIYV%OErUht5>S5Kn}i^>a6CCi(5k0^ib0rL{iCw(q@!XaZzF&?L6iEh z%?XWit)WfeX9OU(VbolrrYut5OB8jN%2LE(R1sW<-^jJJwcY*sYtMrF@F@D{`}ICR zkr9pb=3D@XF@)|TcA{1Ow3);r2u_`}m#Sk=5gDp`A;eL>H2H>(j>acsf zcec=Q*|hO(I854jsxv!U!06`B$v?ReEO!hyrPp{;GUOk&iGS|TuoWFKr9oseKL&jS_5`x5MYJ4FT=*Knrq)EZ_Y7kK)Doa z-y~YN^2;I@!pZyTWe`XTXO^v-qw$4FLqzGg4rwCC15tKrM>Zz ztHx_{1L)h3aP@C0QbgmB;X*=us#fWqC{{##c4%v^xwpO9GZf>{T;{o93Ly%l7REVr z^Fa$xFAiXEHV?R4>b6|}_l;KoqlB+IqLJEoU%7V#k9GvegtPn~S6&hEa4hRcH1S8k zC8tU3c>Iygo3H42CSIE4g1V)l;X?)h1XTMibefCqT?hx}CMsI|Q4Fj^494QoKUzZl zZ(mROFK3+`%T_!soRT{Ehbr-ymX>z6H1Ez92_j6}Z7t0&-_of%F{rRx@5A#QSH7*Q z3;6HM3y`KLcAi%#tiMm(r zG5zg@LQxmo7`JW_rc@+a*iwskqjKaP&mTwQ6Xe?ZqJI%ze&!GzcWr;AZ#(g$c7Ao> z!6$v5Q`u-FWM1R8GWEtNSd;@;9lgf)G3o9F$4#a`SOqiEUU+$l?4;h8UIvfB02QmY zr$)gN=lq$R#>{JiBo=BJZNCl3hd|mZu!v=E+DNLl+2PH#A(i1L6f7RCi|W?v_u{?1 znr`yAELvODW!Es~{>nD04#qcvGl67YG_7>3LQ;!Y21LNHlBi3UL0{|K&^;U4GSuoV&JaaV0ZV%=w4Q^|5uA)n>3 z(cRZIMm7rxI99nJB`|ZLY}T#~+JYWybGRs5YcLUI+dF00oGifL4ub~@=bkPKnYZ;8 zZZ0(B^p$NSL-)jm!ku1bD(|WHERAF*V-u`?7qQEunN|0hLb>WdaX~`-;;d_TckkO( z=GAycw=r>%qWgcgkEhwqw;$p-_DFr(k<&GN<%9LydL=IJPhzaBnwE-_37_6qv&&bP zPmVznmI;;e=^{WVjNrtG9l1?SBN{r<37f;xi)E`!ktg*%1R#w_4z9QeAX=p?x!KH4 zQzL-55b(KELgsfNkTj)X7UDpyQbv)RUD-l-<|ww?s&7o|AK-a?lKN?=amxo50t7FZ z9S<}-fFQJZwvf}mZ1MF}PD7LH!|hF2=oJl>GL9NXA*o0&LU)&Q zUw*zd(;ppGbr_*8o%{P0Uz2lruHD74Oz)u(Wgan$_B6^ zyit2rHb=x0BDAe^CxxC98e<~YfyreXb7+WBSSUJT2PyYqT5lINS%2!rUiaC!Xd#xS z5?4Mp`JgL<&n4FSXG7V37da|IOqHhTUF+DPn-J&RE9&>hiuj~glB?|JtRtS(dEF6N zsu%vux@@E!qH2HpnMl0$B=N+>`@=DP>&nX8AHC}<;>b>DUWZrP&ALV?eE;cbX zbm0(P$&a6oY3lfR$yUe;14@MqvZtr#>C>l$*BudE%ifYgdI7_7Hu=o>Bmh%MuP*LE zZeiz-v373B%3H3diygZQUi4?=m`^wxL0;zE#rtWW>@NQqCnlfT>*k+G3DGA1Ploir zL_H&H(IVDX8XbQGRVF;@KgC=B#Rm6uF;FoUs9&By9#pNz>z9doZ_Q6R?v9;!G5UYq z_`i(%)L?JAK2N!v9Z6&aJc$j2(SCvVxentw%X;FLzrN_K_%?`Kg##Y+qwW8etP-#{ zWZB;5wpKhToSboA01%Yvv|WFZ5AUikC2db=)E>$ks%DBNBzQ~Znn|mtF8G`l-`PB5 z_dBn{7{%8F$3+4@)0|Sms^<*~vfjRZyWlS^5dw)ojp^P$BfWf!p_YWSosRSLWn%5; z9ZmokJL}*1hAASq%i-&RV=W(55>kGj z+?_z;TXVr99-9=s2?;x!Ckx#cF3X3cX2i$J(uiJx1eCedY9Yw_1xrgvGt*xhvm9@J zZ-!>a;W*^(g*tn@WTb|^?rYSWfBkERj}Pa5HmOLB8GzKK%RHGk>SU-Tw%3li_E%Ea z|9C=?vAo$9;}uRwNKjvP+?g!5!t`18qYQmQ!TcJKBREtzTDkLnrD+k5uWR5d@7aF4 zy|rNmF2Jm7vS9(z{3i2*-*@x9r7NeSpB*}B0{J>PsA)*NYTfV`t><|Zu;_Ua!*Oqy zZ7zhTlvKos7~{J?ZaL){t@iE6teh4VgiTS*5!4}mt5L!GOD(;Nb(`QK zwu|t|@A{A(hZZ!QeurMn_!jxh75?toEKffgrnh#ba;K{zMj=E{G~wZT462`Bz7_X2 zv%KW7tJ~7p8@4X%@~s6pw~F_z38)j@*-r8pZrcfEqnGks1 z>xURawi=hzN4&oqc22!NWTY2=Ojbm#p-zi+vUtgI2b1wC_%`fTzb13Mz`Z%)TS?yGp~a| zUX*Jg!<|Mogp^cH0>n3_AD@uguaw7y$9JsOqI;<6y(SBZ)rp_iCbNOr?xsC43|4Ed z5c~6P1kalEgC;?8aIsEE>W%(p$8M(LcJChamod95NvW6Bp9hQPL&2bhh4ysMO1E`n zr2cyM{WO?&XP=bUQZov|o%=kUeGE~TM(m$wsfB>)HJj#_M|U`GuiG}Zcf-*lw=S03 zDEP{`S@)g!$L8h=vk;;{5_>9VvFWju6&&Po0rGCp#{L#ACpKCyJmd3_l3pJ;5iLP9 zi}vc!1!)%@el6RQuiwWg>Vyd~{*hRTMlgc2ncd2J6UJnuFz@FBr-heT#DFl_%&_h}ZMSn4RZmA~EbR)Y8z7bn68 zkoVZm$QKVn%WSga4GWOp5sw}Boj20bJ02T(JUl!w9b!vlQ(e}>s)gnkJ0YaBVrTSs z)1%-(}9B(}dD z1JtiAQukJRz4=4Qq^_Jmsu)e4K;86EU^&jI+cJ_xXJxAd zLXj@`V!!dFZrtLj`v50mVWtwf&o8{u&&C*A@@|a~D85I2zN@}Vi zBpY%tJXakdnTjT%AgPr}wV)JpzEQg6erV#QXB)OWTK=yDRf) z`c(I?6z7=Ozg_KrOl;Q2l~%%vQtwby++19y*zLh!TIqkCl~q>`c|z(u0qIs3QoQtD z04!E@{a*>%e*=d9=<_upPI(cWQ1*FZZ!3^Qg?9S=ERJn=h|8JDrnRptV^ZMQW%ncY zxKvw~_7wk_o#j^v1TezrVP{ItSb?ovGH6OFQ#W9|9LTPvt>yt%ztdaPs(kPBpY%oI zbKy1m%9_$u$-<%he8QJjsh05@()07Jt>yYVKZ63{f`w5{knXI7`|?ycgaFDNe@0ck z%fPswQ~vYa`Wls;2QzEa)1sqM^LZ(sfB$$VD`be5RM*H~5%i~Ji3e9}3}CzgY<=(2 zpY*d&x_luk-$Zx5HE+?}(^!X=7orEP48Pc?m#lg_f9aO^6t zsA#c4=*F~4qw!vd@o?=C&E67;zO`x*)ChQsZ=_A-_jyGNPv@upey_c11tCNqK~DN@ z7#p!CI-!vS+ys|;veVq}KOV#oKFuMNH`92(<{WUgyx#aqa__t12LO4Qst zhuL09dAlE1DMoypOIRbfYF0zpAVr2^*mlaN>Yo9hugPwF6k_qZGE`H35wbtGlUfE^ z{#n5zh7Ym}{lp-%QdkFGH;x zq$wd!*Worj*8IK|&#)_@Wshk%HAH`rSMn9Mf>+&`ZPAtNgnx=vZ#3bUosrZ zA+C#~PL51}8sB~K!r)Jn|9n33@23Cp#OA*-2>;PC{r_G5=hLtMUA!F0fcrYTAD@Y_ z_ELAZrK!i~{yCz1^ncjyB=`J;I~ltlRi=iRw-BNI+Xn{zzdo7#-?caYjZ^>AqME6g2!99ZwL zb?XrQ=kLEBY|HwPPO+}jaOhjqnZojj_)e*2?TG%cK$jbBrdljR^wm+BUH5S-u#Gp&DGx2THe$ui;THFEof@3Y7$_gQuRRPvvYs% zDdbSUn6giot6S%8sU4N^Y5V6(r>J!-O}m`W;O@i%5BQ|Y+CU{G62o%Ulra8~(8YT? zF$$fCDl7I$290MLjbs{)=l%zA{h!2^vKN)|?)yvaJN^n)PVQc)l7V;lzBf`3T;;rA-i^;Y5f6HvM&jCcfOyFwx<4MErB7@ zm`nFR5UJE&9y38ZjiMp%%n2_pf{p5INnd~W{Pm~BrI1DdAqG`x2VZ$K8r#oTBdz5P zP0flvJfWzk9NVv6@$-G;&k>4wOweYrvAuCiVB;I_$e%`k=eaE%HE2M+qz}y;cmG67 z!$b>y0$K*LvZXMWrtxm2G2>xGJ9NSZtkF8@yjQgy$2pPX{&P7+OSx`J6r>n>lr33L zlt?rpz3wM?-G{%CUOA%2Oz-YK-H&|SKGCA$(-?y?N7#o8hQUu9%efcnwa+1Fx0@oR zTxhZnQ?}a^P4fLz73owJ6_UiY{gyvJe*9@7cFJIJFJ}pN7i}O#Skd=?O-!bebgrWU z+GUYhx=)4bzCxC>c13f&wqbqDo>-#8jrK9ACDGWCOn>M=K%);p@XnU@e!#f$9BK#4F$>GF$ChIkWgNTQ@`ac3 z)-e`34C@DfSxD9i-u1Ix(^1a zrdoMUOL@XeCy)m>3hpzkYcYUS2uq^LQ*EBslswk7G?)TYDcL8UO0DTJOLS~hD!!mY zVH}l6I)ZxJgUZ^`zr8u^M^)Q)pW6KnwEt?>_PPCj*Sl0CvxR+vPT)S-{jJsXo%VDo z=ur~ArGq)4l+qs2+a9qE9qhwsacEe`Sz9Q2Thuu4%S@w^a9#qxFN5c*mgP7flJ)Md z@Vun*yrHg2B=0P&ATbN30Bz&9(H?`sy36w9-3}Oav%LZGh{3%@f24ttlZT}Qhb%!w2)JO%LO24W*)};H=rZzc} z(kR@Vw_45rAi<)_sRv;)0zBcv6T#JgyUM9M#?QMU#!sqh2jBa9veVXaoP>9u<&i}4 zwW(r$%9{t76h9+X%`c)Rn2+Q0l+K(YcRh0K!a2&mXO;&2-<~kqC`e#dh?&3Kw)oY!3;rVyrkyBEHE!TkgQ&?XTtM|aKGwdV7rxbDpJerKwVr%9^)nBNY zQ6($_8NAw1FCx`Z0aPq=0qavUFB&{xPmbKY6CXHW&s^oiUt#hRIN6d#CJ|(32aT>s zS@?7wHyc_@gN}k9il6G2xB>hb=-S6zVkJtxTk{Qt{X$dNl?`GM3dT3^_CmkAn2;N2O!D-r$D zbz&rBoHpSMUY`QNmw0Zp=5RMG48>?PKvc@%eHk&@f{Uz+3vkHXrGJwFjXGUvhZmU} z2)ljk_M7d|YeX!mS#S+rhC62?t9{-HTP6tA^7fW>Lrt(Zw{(fubn_1c`If!?9j-mr z%((_TUt#p!|THL8hkbf|EO0dFQ1foC|AzucTr z78yJM!xv*}#I{R+utP*#jh6T9QZ#AYd96zk(r%n4iYN)+>{3EHH% z?23izD>Zm830LiMwmoLew&p!nWXObp9i4yOZY%Uyu_taqYtkh{AxCJgBemm-p&*S$ zS??0%OoC6MjA8Uymh=t_8-K<~4UW1mnIo@!Lcmngw|`Z@0X zJD{n3w&TGpPUo8oc!($XMf_|5m9W34L4Ml4UeK z;;&h;sxw#f5We1^5>^}HKV%RAnuQ>UePLU0755KU@QI7I=Zg#lXb~UF&VxbUB?hnb z(nxJv`=tN3DlT(kd-n`t@D4etw(HJz{>V^`bOeiDITno9k0<4VK{fh=HAWoSQF0Qd zM_;*qGvb(!lII>h5*+<)aMj6xwioDBd)6CD^*M%Y295?tgKRg5n-2+uK4|ajZrtA) z0`@uaQ`JCX&%s)yL4i;g92>LHlLnn$=oPwxUh!hb>`AcwsFffq%nGhCDl@c$bNFg4 zMP6y&@3uPRF^W`^jIk&B@1VkaFq@1qw8p+>IPIG;FM_GV8Yzt)4X!y_V=R8kB@7*m z(!@wfAw3Gm&SC$ePN&~ZA?%;$!8+im=glQCPLOFoQGnH;i-+h8Ez!O9($R(lI?23N?Oc%>nI->}cwqnl2Ipwxk2_?~4W} zew(J&DL%M_y4bEidYs*sE5}fX1@U2Qzt(`a5{OpOnN;dwLGY-qP~wadjdGKi5{TbS zrLv=Gd+?(=n9*Y~@O9&oaPTYgcy(Ca{tpHLD)KA|LdRo>2!H6`Y{H8#Zg-tZ_{#hM zUBc5l{M8>TUg84~#7_NEwb{*VuBj&`cc9^4AmtQ7P%|j>eho*-OjwYV`sXW#Le^R( z7roT1Q(%_0-GWih7F9%$yfpTRg!w@+Uf)KpWJlhnqf16$CJoMMSBSdkIMXz#(Twhw zVZ+kf^M72E@Vi3MUHKyz7-4X9#E?cIl@Z59VP9VvYap)h=AY+5^ zfdWf-DoLALUzKL3DmePCz`rQo)SCoB!qdyI7v9BRB?#SojNreSbKAu;!5o1I5BP#=#k%yrS<|wN$uoH^{nL0R7S(7`wrbc>&E;^Um zP>ay{nx|hUY=s!5!8;oM%C*ufjHPhz*-qeOcjfB@bP1#fzk2}96{EtalSnj|lpXXH z_KJ9Z?kpd;{tDmYFNxsDbtAH=*n%Kc!JSC zU#AUfZ@|25D6}hx=V$B%b3JfWJ_=!XQRw!d&U zt4Tauv$0yv!vYX}b8dZk;nKz{ z5~TKPgtqc%L}=lNv6W~v`WV^uugB2Bt4gRtwRs|UkYED!aC<|0ZtPbqj)yaN%`Xb| zWM(cWZItUbN^m@Uxul7L-|y;~Muw^haL&^BxuErYs%)c4CHRKuhK8vHLGe>~GFPC0 zK|b8YDZZh#b&&&xyjhMs9yut=bqOdCWaSH;y4NVKX39>BFcMETngjfpo5h5Tjv6AP zHY>>3H>Y#BfvFLbT!vB9+fKVuIe3jm#t=ym{-=T$L>3zwhyH#@1DyWxmZ!fbY>gPL zZ-v%ZO4JAR;P`;xc63zCw81G9-hKHXgHw5tkRTKV9sGzs$GTUYaE+xJ6_V-mhZogl zM$G&W1oz<<4o_vTK2~)KL^P3FyW(=xLod^)1o}}kKmLS<*-ae3v=ERi<}y)jpfls? z+}EV?M&B1DK<2`aUVv(qH&F@G2A%j_dtsn5$#+EIRnKvi%TcTdawMHBEaBdwmYG>l zAyZ`KC}npR%rMO4Pyu;)ZHGb7WIDcr4>8S=U{yM09JMFs!sBLGrt?Sq- zeiL^rW4GaVIvbu*&EV-~Xh664R{=)&r(t|vlo=0J9=a+}0v>#@xx%PpaMCD#IqEUb z=mBI^U8&7*_sgV2SwSCRKtQGw9GqVsxR)XIjN}6iDnl^BW22G`GptHGcsudjF zZj`(n#r2J_P{b^hUZXTA(CUVvsLBU9mAQrWnY=UAVT=+W7`eK;K>hwoT8$IG$MU5O z_|4@N?Dp%7zdqf_z8hM&5Bbro#ZS~M2y%=V)oQS%$7DE|01GIqU?8tI8Fe}qf*o&h zCl<0wfx6u|<8rixB-VFa8CIf1cvI;ZZE<__Ac81VrQq^7f{~9GgCU+xo_C2a!eM#Y zZFEf!Ash4O2jCk=@bQfCwIlsCJJjK*`$(QUHHH$M)Nzcq1Uh2p_0$H;q+s`33AF;= z+@xc$rTk$vG99K^$7mh|iA}zn0`&_8kHk=~S=D(w{q~&)kzMVkWe9)Jwa>zf=}?`+ zq~wUgfT^Zp4RawB&ZI2%_W!~~rl8oRIEPFphS+YV{ROBSJYpRn#-(gn094#Q^lekQu#j{JUi_}2GW!r zrnzYp_A`~S?)-#xT){t&%(z0SRRiN8z%doSVL=q-wJQcql2P`wqg0MA!;ff3dwfM# z4Vso=SzYL|Vq8z!u%IttS=(W9C}l5`5*=zZbsC4P*ZC)^V$~XEuc>0GPuK47lUlE3G*|4`X%9#&)18B~ng3^!!hFuPb zqE`)jk=K!fi6<9lYH;rJGX|!pX~SM)P&>SAG>RaHBMq}ygl4}x@#g|d5(Ts?B%@uW zO?#oG(Wq)d!;lxQ1nO&W8`(^_M_@T0)zjHXzubvOUsL#qg4l*XYS}e5 zr!2P4s>}a)W%!BHo&xdlXn$IZU+_%J6<*bYP|n_r4;Ytt1jZrW$lx~@3f3nZf~)a3ILlg4r&%_hk#xO&#eM$KBjh}8 z$Yfn`qYQQ`S`uj+xl`#G?Ww{{-_ttJ`V~S{W;AMixo~Q%3|Gc?@;X%No6>G|9-nNO z+7R4`mk=!A%QaJkG0s=RJ_&7<;o#JILhazP$%?0|&LwSNr_5R5tM2OiS4>t)qot9h zcZVk$H=%7AhGOwN?32&F67dfri*W4j^Q%OpKxC1i2TMF7s$Bu(*sJbBJH0rT^&I-P z7~-nwc4-&QbmL~BKS)N%0U}ttS~cRveAP)i5G9F^h@s{%(p_1CTIQ=vCo5hi^oDtJzhazw#+b!z zhj@&%B$&J*EqhHGaf4>fN&Erb13EBEe|{_1nC!KKhtoI!ehX;ISuXqd5uhN z2-$tR3I#gEM_p$HzC?UyeM(fIL%DYt>!Vq>@8Wlsi<$=Phbrt9SnlTlSi)Kb1=69@Xb2w@+IGo!?i5c;+;PSY_L7Qg$Bz(Hx z3kK$Y!ebCrM_Ob2#ApFp){{xs3_l6CR1K<=W!Z(wW7SDgHNzx67DN%!D<-A5WyyU$ z_U+!Bn=zF+wX!E+n+y(XCrEJ8N^l-y45dK&^{_Ic9J}9gnRH@ckI3hE4dT`3=3;)^)B7D_VnPUPVNQLqrq7fQ= z-F_tN#CI0>7vHsiL^S#niWM1h=j#-WpGMX~9*O_$Q-65CchNLWmv43K#U$w-OabiJ~`zuiW6hazhyLDFw3QUH$CBGVpQ%oijjO z`X`o*GT$epy4OJ2eQ$LR+`E;HDD<*K0<-L|d< z6E5S>W&3T5S&jJp+tPDij2(P4IMhi=o_IpLLZ^YX zsxV@fOGoiO;`0eJZ_A@OaGIF}mxrO41@J7x@KQ1(DQ!S2=WCwvGmE-|S(i`F18qS> znhi?1ZDXJhcCkK}49Bxu52s1tEq*}Qk;7C%3xbr?wQrOTI$^)qb4?HN;XW=5|L!*L zuIz?DST%&l!0Iet&$asn&q{0#xst35UJKW*J(U0YWQIw1sCI3G({3o3bvg1A+5>!6 z@_Wk*UJaaH$f1V(Y1qTmQ0=-y`62Ys4I*IJ6 zea4kwyTnq;9ZvhoOp)p zOkGSy7mB@O;#x8OX3S&~z+3pKk@ccK9ui!OX~!!JdXy#u;{TMCKlLsUUL7 zhBkO}L=Wp*4q*8V_YLM%wQ3u_t^vKAs#0T$Q;pxnRzsM8yhqWgz+ake4YD9UHeh)u zQ4*MJ7Htw0N?{_tb_iJWu5;h$k(UzCK;oJg;=_Af5dK9+;p?u1Eq|IthGQM}m#d`- ze$B#sR=R@wH{W&FfXz8d;(#7j-1RzP6SQw1Qo(*62w)jL>+KsX&R2WhTX?||_yc*A znc1Z!6#QKN+C&iDu8H9z3svuppUXLP)7*#pXU-&x=gFvoL(W1|(c`Wt<@Jxw6c?Y8 zoT{hehxgbsvgpe&Dw15s!cAjo!Eb1D$L=X(cP7DM$Zt|*ZyQZ~4V5dnWOm37Dk+iv zSIhf;<8SsT`nlZgSS`zm8=_*`aXtYyzB3N|6PfgnTryF()${zjDZln>R_1%lw#~3s zr=cTdm>WTzPZ_hxL=6ww?Z19!J|}d3_&HDQ5g@~=PTMvEBCO!mvB`Os5^VfXqAwIy zY`7Tp2x|iycXTynsn!-`6$3NZp1);_9dtjc{e~U!iR}7nbTQ`}U5eCT+MO@CvrlLmUd-8ledzDl^jef2drlE^c7ufu?59fu7r|9r2 z?(Fci-K}9ZJQT&lHG}rR?z#^t2>SYrr|uZqKEzi1!4awoT?|PsyQ`sM&bNr+7f^Nh z9`?{}!%$8vn+k?~QkYY~q`no!X+9o0hB+wV0@xtc2CafM-C((N`Vda+Yjzc2TEW}& z+gJ`28RIfJ5~Qbwj`MKz>>Ui(x>FV;J$bcuj@Z*TMI7hG+)jo`n&+IrwHv?WVX~%R z*>TJ>&WTPnS!y)(4x!?O#Koz;-Z&`v0{x$7A8CVvZ7F#87TUrCg^Vdx$VlJJDvL*9>mA9 zGslQ8=R5P?5-pK#rn_5tTD>j%q{%_3UOA^dbpz{z(ecPV*<}c1dd9CL1GZ&JZ}7{= zVtu+oG1(`(K=At64~nC-CT&hx=T0TkYgbHtXE~L{*=4A*K!))9U3(^3chWt2{))ca zscfb+n8r2vn)uBpB<>5Puh6OxzB3nL#1Os}HvOsZ?!H~E@!lA~2ajZcPRA~nlZcK& zS1*Rvn`BB01Nu5{dMK*2?&Ry}*@uwjKCkA3^Pn|S(YB_Lwp{Qg^HFX0X4A3C`k z9*X{VB{SJ{TwWiI`2?%u+jT5$@z$)T5>Ev`F|`(Mq`z9-8558#LQk5LJN>QKIH0ud zlzaDLX+{=zw*;@&5h~Tc?1GMLz9V5>)2r?^rb5n;-(Z4XSFj!Kx&xi8-VQL;^D?3} zlrN9ne2v;`?1v9V`$dhcSGSuh{}m|qj5qA-*ZaSv#ZK-x)A&~{pa)#=t8CWtOQ%DG zppSo-H<$nY{J`GHJbG|zqc0+uW#@sH%a)t#>CHTntRvd?0a@G6x@OCFq=*;a6;%zK zF{2vjQ~A?!uM7D5V7ApbuvB+ZvJp-N1ut*yA2;4wfMa+Wwt7*tEx2A8BL&e!!M-6dAZlSqIFY&L78tun z3Rk~A5`;)a=K4#~o1vmN0qw~M`Bz;%TVo?w^9U^zcL`tLHX8?_r5#tt3th21(=?p+w1CtogOkncwN9z;V}K0Rg$7N-pHPN}kIO zJUIU0xe;C0ULh^A6=>wnso5)Zz+TmWLyt}0z)>4=L9JHc)ztiXw8?;H1&+-f$L5V7 zlIO2S$!|x2*0}x!EIT+S1(EPqfzVATB)VL(XysmgVIKgr4#@4dtL1|2yCp-sx!>;AZlRoerf3EfdqwYWm|q3(^!)H^%HhM{e9lWhm!Wx4 z0qaHcR##nF`0t$WgYNnTP56%ht80L3hhGl{o;QB8TaNT=^S*s?jc%`K?uL9rPovKp zqd%~<`sl-YQ1d$4kqZm|o7!`4YFQDaL)GQ*3yGu8R>@bBVR!_FzZGV6{ai2<0Ao&Q z<)3<#eRVn)j=hio6IJ09W(_1;z&(R=Nes|eb1fT!tg1R34pf2`E0EoWtbR~C3-&mi z_d$bOV4pYYegU?qFzBUFx-QK3;wA?alBQhU>z4%O(bCJ%gPu*0+r3b_ZU=7;z`cdz zjMrI1ud_wwp+V;1@8n17*rB_C_YSP*3e(Vdn)H14^IN>>$KT6R>cSRa zbSr3%TV7|6*(*rCt*z4C9Qoa%iB3^?^lQ{7<73cj@J_*zaLg!RI-fw4v2`48>NpXE z_57vhN>VTC-F@vrAu7m$v9)2C>l_JCkkvb;hoe1?LJ40bC!ma_C}iUhB!V0FkqEN{ z5+Y2lpF~0UT&T+5p?46(RMq%!TmZBf-D7M(JB!ot-AkY{oG4?PJ5k04!YEW2t)oC0 z7qCyF8hg08?mSvtt!-#P_zIpVJ`{YdzT4wDRRicTpLH!eH*cs?+ zAM2phPwfJ!hQoHtaRUb4XLT~ld&LM1o$39nDoyygYPslZxgc}S#RnbK88m)Qt41TP zW9?tBJ-+HtkNzI=lM1~O`J`t`S~Y4mPM&YCy>`EDw~$~*&P8z>hwqllR2mLf8a`pb zQh!kJjRusQx!!(PP>(N!mPn|8?jCS;)Lz%Cfz9~wc9$Mw44ICA<0p(+bD?7+NfZqm z_$~WWlR-2F0(%AJYnLZriA1JZQV!ObaFmiY7um?!k`)$IL`yG&1RP2F+}BwMdD9aT zh}(y#K6&~SV;m7M?1Tnhzf_f8zv%&eK|0h@-n{9VL8A;;FxJsG73>smqU8|3pQ4^% zh&EcZ>@OB~18-H|!m7g6o(2rV6kmp!8?)sb50~!6V-eS$rWvC@QgjKs7MN!2EDpmc zsC{@7J{7WC_63X>My46>6=w#ht+#!rcs~a+=m_TayPNlOHqcWC6Gj?}8>b)F@kZCd zu_@Dewpt~xI`hVi&0chh2P_V}dwzzM{WR1p>JryQ(*+HozJDFa3HG10QzHAPoXGjt^X7N> zZz7C{YxBV){vDZ-aIb(0iFE8`{vs)V!oTVv5&n-5GjIC2!=lLtrS(;xFseV}MhiUr z5BAA_yi30xI1I(kTiENS7cbDczlliUNwXG)Qb3k&*@xk=k2Y8bv@F zN$GFRR-f}eKhAfqbDbaO&o|egGqY#zSoi9+W|s68JA5E;c>J(L_9i}=)|UUen>vdm#~n#&>HX~on>_1xaRP2LZj@t!?)d$dg?FkfDeAs zMg@pVU3EhbF$86bC8eE^S4~}1Yp;)P-I%LZ4s%+N zQkBtsr-<80UMXaTXbUn9YcLFJXfhzr@XvgR&NW4N{KP#?B3_R|DdZuWX~<}5V^%27 zxl;!zp8n^FjtWZGJk$5U%DEY33Rg9lCczzO@aZa^-IYm=7>;om$kKO!Fcb0< z@@u78wds?4t%7~6g3)GP_A7sFx_pFRpuY5ZNzm*UjzHi};Q0Y5mCX%Ij?yO>r#Mw4 zJnTXyHUkY7W(lU^3xSe@`YN*?L0nEIgk0rZ6*sDS;bx0^)o&sn{>Bt}NC1S7k|}uiQAYX9M=vgLeDDqgDDQRd2F%_%affO3 z6bki=rRC?Xv039y+)wQ7Z|GCWJ<(^8{aCh~jwI8n60(=9LVpdJECrt-8jX@9ZZi^& z3wH-w%vt`LaJYL_xF-trS@xQY0FN?L`WoE(z{*#y_E7-mk0d+OWII!uRDK4$(a)l= zC*^z6hHs-s4<%4!FGFylc+_i?+LZC#!Z8_cNnUv);Rx?P4Pu|t!voZdN#4*CT`(T} zfPmmqRHI9&#S%<~1mwGZs~9zIhkOd;4rCviuLYM&!1w+T)!1~G^mD0nl@K-YiZXVg z-*PD(o3xMfg% zHn=eE!V{=6=D=C2kS#($OCsQy!s9`` zxO|a;TkzNf1U*pvSK--!_8GAIUp)))cYx&wO<3w8@eKAMbd9%+|CB2c-{F*z4xnJ2 z!!{`fk3Ga0)$X)|n+(Chl;J_M8Pj6r`mq`0$>cM{iTHP8V!aV_iYyr?dIFrI8dt=S z^Y}9qbq*IMPFmmvm~ifYga47!dq51A*<<&Su1BD2LnLJPDym^6dJV4Vz1e$Ilz8kn z60CWgzK%O?^8_QGbN3tD3|^fR4U8Q{Fd2dKk8!{nAbR-YG=iu0LZJFJ6r3|$;jnmw z0X;+VI1$(H7+$<_Cq|TD3xkADdt3nA-+0@zlVGs#gf zJ|osAh&V^l{l-601!e=Phxy?*Z_H62Y$a-fl5^|taVjpb{6xX`cd|1G78}LcqvOt; zKasH$qZbgCVZ><0O${;ThStaSC>Br?LVt--^DDp3Vie8kOMei2y^= zyv>LeId3pJxC?`St=M{01xf1p8ubX#DFTT08VLrG)#?Oj@XQc!IxE3vuG_uecL+Uf zPdv*5Ishuc!ICsq2$XHf>++oN!xcozu5eFXfCf;}EP<`JVTPi&P7n_`YE1}Zh*0;O zEJ^uV(PA3azYF>e0M!kwblZdCFv&55wHhn)u2NJDFi6g`wm(avKKHILAeve70z}O zZWt02-TTC#VIctrk(sBrk$Xe*+L{&1IGyH-~_N>6IAj> z@54&)w;bJ26s?(}kUsZtS5Tw_i~!WZG!M7W|A0>J5s>|xgpH)DFqTpBdPR3)+4|(sxcHy1ylglwh7`X6k)g^ z=>vm*AoCEXo3CvL5^W-8iwt18;0z?$UBV-K*zWF;=u8mi{PJrgKq}s0_7ogLb9I>? zAGHX{F5r4#E`lql%baH1-pGg@<&C~REg=|oh(SI3m?>h@r1QvDP{C%g4_m*tGcbeo z#~i@R)zgc^DKTa`F|^|~y6{pk?{*$CO%&?Q?zbR+_Ww%gn&7#|JJR&&yiJSeqeVgG zwikR>X(ApK69G2_OdljeP@->#15Pm`uH!oj~MT7~ep*E_K9r1g8P(9Lg zkjM+OtBlfkD=|a-UvN48G3M$5Rw!^^$O5Hd6hgE)lh}BWSTcr;WMcj&kLMhXliD*FBPplgYI6iqtwi1X-dB|CBo9e+Ki z!Tq$P9~lj!>KzbWTkQ+H=2L>&N*>oaJx=wIhqT z5Xqv$XL)#^9WwV(m&u#Yg-&(!A#k{HPK`T;_n3*0GfHDR;?gIlX(M)vLQa?>@q`o3 zE%QwNq8>smZdn9ReaP)x;8qH3 zWgbFNB91JlCWT;iK}pFp5}heT^8OYiFXC3zG?TdS2HZpijPVkN29gHxzC2fOeBCq{ zD3n->aJaWuV!v?*l>phRBuEiKcA*n6(gvAx;PU`J$k#SegWQD2z}+#q*C>jiMNwB0 z2YDt+yl&ZUN3sh@olfB3pn)dFAYdg`IntzuxeU*81v9^Hzu19{(Opfl5iWL25g+*y zr{DmWA6F(eibBf{$6|5nyQFs8#3kgM4tkoobOgc*S&tuWCpP_ti+{Fez~u?Ta-j*kRmIy;TXZK=!F(4BQ_l^#Bkn$1~5u(Mxc|r1F^| zhf<;#29(~k6V-P|&MP!CPVE0hGehr234RI2LSr_qn^%x0kti>N8OSvl%%!%8YcSo? zbb_a^6BVDe4iW^+SN8rr9svyioGIov?_e0=W^XmY8+S0%b;$i`4`3)@?Mq**p@r@^ z01%urXTLj%<4w46mi#^5LIy4h2@c3s%o13EjQKiDWI7a{=8kxC!q{1+0_ZfA{peY^ zC}o3TMvY(@`2QULbc3Fveyl>47tj%t*$O{$4kP`Yh)}h;8qW-1-sp3q&XY@tny9ZY6|Dap_s#E*`|nmv9quh3!niLB#P+=kECHD)X-dQhx731C zFmYJoKHjAoy-nL9q26=$OvINX1jjk>7;a=RcSpTprp~x48UN@g*?I+hfhgG3{dD8g zH;<_GsZx#c9%|y9yiEUi%$jo6jAw0^PWa3jFDV}Pu_9+drb^pn?-@U)#2H?!^S9#T zNE{Q{8mh?%Q^Xz_p#o&D5)g_W+aU5dHFqAoRI(O)6GGYA4bCb>7D4d3P)7A_t+Xt$e8hxpTr*(J;`?xB{`KANx8z_ z%RZr#J*Z9xnt%s^FdB#6(Av}$Voq9kCUuAoMnLKc(gd*7^s#)vOqyN$Rk&Nj`mr2x zf``313NE?rpNY8pJZR?1ufx#)vvE_vnPge>Px^*6l?vQty8A9yCOor;>&ZZ$%VqdL z|M%vG?aRcRH{d(f*=|q@1$%4pNb>Wcqo|4RY3h_KcX~M|;I~FOUh7#2#+{o;|zqoP6h<;3~32Be@W&nW)zZJ!bO)rRd z&6Z)|G`#e*r+Cq_h)J_qk@<<5{KVFS8UTLNZyy{OLufc|X(jR?3nDP?H!lEZ{?%Ra#+ zL#ue)1D~$HIIO5>5C6#VM|ddmW8aWi1hS+=N!3R|8Ddd5vL?bm@IO2nuwQUHS)9(dXeGjBvMKCLjzA z^uqn2Qg^PYS@2FQW?M7}hN@u^AQ8zj@ghyZ))0B|${(0A15EE`TjH)_l21RPL;3$& zkH>+V;s_n`)1RMnxs;bp2~{ZhNGL*>2CgGxkor=I*t^KaeN~wx@B}i^P%;W`nmOFs z1+2~Fq@4~>U;9{vw5Lb*;cA0Nwl(qxX}%K53wneOUcG!qqRCnV$oSS|&^&*dgXQp0 zqd*o8g%YTnz|B;$`Bz56@qk|lz%eK9{{v?Om1kS5C<5adq-#biD&!A^!hT~(YC~a2 z%19_8nR@w=MNRw?M@(_H4DrhWrTu!`aJ->1Z>dZXx)HP{kp9~>YA;jx50aJu(Nd6a zkRX4kfSDXjQY(s+7|yj_3+w^l4Hvtf{{uinj%Ew@Ye07g-Ny|9#j2CI*7QXCt}!1` zam>B<0>K)k=K;sL`K>M?e=tJLQQ>er4R$y$pVOE`AmG&H`JVCII>SF3W%INjB%S z;RXoy(#jLkKdpu3(r_Ga_=-R;0-E}}WfN;|WRk)JQCGMp7_wa(L{#QM&pbwV6mMCw z0Dmlv)hY>QT5~C>Ilw)&AGbQ$b;kqomK}<%alcSBlKpFp9NSEcU`B(&=_B}KfnLXI zj%}Pm1^nWcCtIQE6`2s@)T#y81dzXp{Hu5Yy*XdQVe|?Qz|uW!8rQ?hrBEWm8U?cf z>V(?_l}RI9B0`IVdV(ciP2j(B9=TsLO^YWZN&DVse($bI4{@UP{Yqd4N7sH8Yg z?2*!z?G4l97W{CzVOM1S3}2-b zjU-+|MnEYH(7UyW8=H~dDKd4UD_ z_6zt-z?Si?%M+u&iBUDfl|F<(YXL!T*2w=owd`Duo4&_oDI=5*PX9zr)B-3cY{ZWN z7Bw`H9iS!vRa0Ol0c6LK|1s-Ak^l#q$4bEI|7SzkmHS#Em$$-`c|7f&BUZ zY=pQ^O#D0jKWG035-8OF&sbnmFb7HoCLca5sW}QZ)1PhP29QW4!ybw!dfG93N*AfzIZ>E93M|MOWSDbW1)HfKKca-(Zr^?e#icYg zG+aJy5*|I8lbR~|YEmg1`mZgr+%%GqkRZ7vDCz6971L?n z7fvU;j;UUrMW^)Nj$ph)!R0R@0e!Zbm$Z#bNVaR|f63Ruw$8fDy=xz`nv3#|N{UR^ z4G$kGv#!Xxlf)R?p6Ry8*|S1ruK~q-G#6av+PUg_We@k?kGvJ+;Zds?@ri^QLTZD) z3%a#dWd>f`=a1a~Gcw3pB12?fKv-B<{B^s0DC4jbHB3+cF;5{j&jlNmBd?&K9<;g0 z=h;oqz{tqL%c~yIo7t`zFT7#Ez{rqemn7`9+8P&Z_f@6ZZMk1DUB(hKLcRf|<35}6 zw6?U!a>gn&Hr}8Vu*)Cw+odkhEz`Y!|Gw9cZ$U1^ zHE6>xCgB~!SznW+RdisOsShIZ&sIZ%MOOAW3B1o{(+~N0?fUjSalK{MTZABg$>U>8Zus?pRvR23tawTP9Tan4M0LnFY*=Zl7{e zrlLOEqY+VRH!3WYq?DPF@nCSkFweSzlP6BTqRECo?`F{EP()(>*RL^FL)T?Q+?F)p zBc1Iw{}2>~K}nmWRr5#sW%+}0{`S9UGZ^g1JeiMzeCB*(baiTJF*pgMUiY-=+n1RTa_d=g z!)*KU`E{t@X*H4BI8<$);!?DXSVcTdm8&b&P{ zi=~2eqVT_!Vm?L(BLDKVsD!qDm0srVQbJ*tq6xhmR6im*c`~wRwl}vgudnu=fV8Y^ zyK!+geBqJk#EGuy&gT}Q<*oF6h5Gj*H1zcH4gIz?J-0pI-R&5va?R^7kF0WCWKHPI z%Fbrqm>X1Q)ABK#7F0s3kNd0@*irJyho3$jA)i(DGBh;5f$bkdKv95GiLlshR9G=i zSWz8)r>g-EKPowyGo<;=6Z_f%D6nSL8j(qKlB91$s6+nKiolsJ<*GAt!RElBh&7f2Bt?1BO~QE0g{$h)4SC*Zn)cTBbmG;s(n#H2dva72r@) zh0elBGv|*SRxamA8OgP^w~L@nGBYz%w85kLXKR@K6>9YF1d%|G{x@gI0BV}c%z_!Y z2`Xka+av#GDrgAmt4gQY%nxP}CUkDXzw4;n$}eArmQv!7Z;<`laFXEP$sf7!Kh7)= zSp*nkrzw!8MlI~JQaYcvyWR6o6C7K(;~Sby848@@KU*Lo5&pCNe>$Ux=roV)vnrS~ z|JR;UlU+qm>^-^nxu}W8p^ADvj(g=%_4W1T_Tz<`d0K2-T+20K7Cdn-!?q@+rme`3 zvya@<^|Tr&EO`9egYSWg;c&VA0N_4?K~Ob`DX%YgCOJXBPf@f$z3d-HMuWPwkUsOM6Y&rRYCXN`bqfcZ+5V*l^!yE(jUvqXee~5)9Ggy zV+@1pH17$W-I^j zPh`K%z71uSx_>*G_BA52K9P?(Bv5WRYXedi`s|E`@T0T{BRJAN=ZQjz3Q856c5?}uJc93#AsPn z!g{ots;R1mU;WS&ofKIxGV+ctf{uVN3Bvy)(t6((5fElz(*TCbXxmIkb)M`XDsZ;DOp z{eals%DC(L&udQ}6Igc+m3i&VT7d49u{2thE8QMLL|T;do8NYQ{Ln9dyQp4uc5!QM z4T?74_oChHVd<%boNg`G>?Zr|SiYdQ{P6hK>egBhL#*D_kiM(2JvpQoe6yiY+MN?8 zepOAr9Cn>AFd0d+4?0Edf)#V0{X``jwd?hlcpa`ie%k+~dbtMC{a801H_CRAP|X#~ z(rUW6lo})nDDQ4A_dp5E^msmNtA($_y=4m8hzhmVLn*elP$I9atX=K4c-`hFSti=z z=ToY}0r-M+PQAps%9HgQRCtK;{d!{J^3kJ5XIp6f*h}nY3iT1GIoFqmwjHieh$x)% z$y}kn2`K>oK63}`Rc?v`JryXrDAaYL@V0dId}m2GYPGhG;{$-m!}N?f^O zfXICBtUH>zy79t5!mW{vEZ45J?sL4)E3A?S{0b|#8_xO~!?DqTe~9i(5U_4~`eJo$ zLp|@kpzYqidt9gaM$NR9?{;%+DB)km?a=kQ(>$)ze?ugt(BEk*SqyYe^mW@I4H8Px zucNV|EE>~NS|Y8C=3aYb&ZMo9G-DYY7WaUDBX2485;jn31W{TxmQ z+8gp4D7xKdwp`BkCH9AYwqM_>Mnr`>>UnQ2sOD)EnDXB%HEB6Ddfh6DvpT2&kG zh{!UFWdq>N^4r_nJ5ZUpFHc)pjsg2f=|MuO{lW1ce+BS_@$VT4+Me=))+K*2uXRE6 zKCsg&_xY?1Dt^#4wXFJbqY>OR5Gx9AHV)U|xi@WH)X7A^c?^f!z^W_tINjfST{Sc` zEDH|l0W)T#eRFYi=3sEv#cBYfZ#DR?`J!P9sL_ziI#lV5_SOMb4R@fmm>!3NKD=aa z4ov_{ZMP~z3@xs@^><^QTl%BUTchz3hno6Onry}u!?e3b>9X+kTYh<%?VuW1zCCa6 z-@I=8(ShV5&s~_V8GrQN#hN;M!`k)lC%qQBWc$#{_I<=fERlDX_Wj38Tle=yL%RI_ z?-X=sc0Nub?6IqhG$CsRaD;2NlVWTiThsn8xaJvQ|a2ZYw}8CtRZEl-zO3M zz$=FP;6v{h)Wm&|dr?tQ8A{Y5bGyshF}ks@UI81;T=c(ll0%L|xBOMBX#_~fW@A;T zk-V`nFrN6!RgV+>l3FZlx-(%u!CwAAaCp&;&#L!!NG!Xf{S3SKh6WATTS-Q15{t+2afZ57=RZG7sC$h?I@eNmupHAA$U%;WFIl!3Q-CXJ$ zUaoO&t23era<{^1F4rxVPRr=9nD({^nugeS#om{e|60piB zw>@jhpU`)(+77sy5m8Pr7vZ+_H3w`j(~?1YImRAC{|kDYv%Ar3u9`D!=g;=sGB44A zN*8gI*RzXK{12w`hjc3JMkQWObz>zC-_+EI#&MiqLt9|hocqAUty->|wc2U9j`?G^ zu$3%A_2~1LF6HNWufH0#iv?MzAseQL3I33(zUFFIMHQ4Yf$vGG5FK`=NVs zH1o9cyesU2U%rfiRYWT#ojY^pO=rT{(Uu2aQXhf^uYJa#!Zr_V8V1Igd@fejZgNAv zoI2kk%{zBOwF(UMXJ`I7ipY(@KVdcUz8I_mhUj$;E`xU(0^>d-_>j;@`^g)z1HI5FQ>Lzmejfm;E^ns`}35 zpi_kMEcx@b@;-&uX~Gq?`}f1ae`DZhmb|*Q%JzdYU%x^L5lFo(>en3_P@qJCwfErL zJ$viMM^7@#J29Aku!_<_(|iEZ^&AC7M!Ia+Tw(Qc7gPz_s6X6Kg^HMp5v8+RiaYBi zSO6(`_W{^YtnAk{oM$?;Zro2hBfvB_=)ZqYM@MJLBwwdmrb0O}_1UvyF6&>f)rudI zERIyp7j0Pg&n;Ci^+J+-_QJw~yu?2r)O)gHr!IYo$W$mZ-gZZ9OuMS>3%>Bt{C7G9 zUca7@Ym}J3mXeaX?!CDdzx&~&=IDqO)@6fF;Qg7i{snVxD3sC}s)WmO;Jemzo$D2w za&xQJ7Z(@LJn}(8vEDuYC9OoE_H-?)zrX*~)Kq24jQ1O`qd~i!nu(?Dj_)+z{{5So zi>qj41C4l;&YjEd=yx6A3IemNj*AZ7s6XTvJQSEyw!XpoaE+Hf&v}DE@DTioi7);$n3n`_Jjm;y!?QQ?RrlP1ma1EXPuJ+DOv8gKF z7Z+S{7NY`#x_crgLE&)=ZxVVIE-vM%q_%B>hVxS{i|)cnXyTuT8e`s6WMruqdzSmY z(aDzjH9aM={0DbE;;9XtYCTRcH8pQoi;^RbodzRe_c}Fu4!zW@&UmZ-{6TQ%Xpa6T zj4l1?8N^xM3XJ)vgJVo{lvv{tlzZEa0*QAI{k&p44N3*-eZ26GE9ahlgxqe@qLy2A zBAY%ZCkL$;3s(D{kCf40{UhtV2HbPU~`rZOcvkWEpXlC=vTW{ zl`GRS(9<(>Nf?3$F!QA)f5Pa@X)1EGV*OzuaFIMH{0KaDKu14vx-)+ENu~4k`yV`{ z1%msb$d*#2ZGr-q=6IcN^s85?VA_Fck0GO+FF(?mz)&SMjlrraYj&FRQC{$Ea`B&QBM)CExX_J z$$5y2p8gip{n7$QyIJ|&Qm7K7ZbxRx3{vYRRg}%Lh9-IkE0P*A}Z%{&3+81lZ-j^1W>BwRA<~;jSYW- z9Tl%k5!PQ30(=n}U|P-9)B0vV#3%d=R_{r2c94qrqeXl^zhCISolxA~-L;lHJlI*5 zrDXvyMu3#aK5Ac=R&3JJW?c633Hh|u-fu^qggfs5fx%8(Io;&CJ+;`D7&vI$U)z(F ztCS=R#zi!}jxGPzgQ1dZ4qqhqOU++Yxy=^_(fa);9oDtw2QAt1OX`iP)i$>Fe1W(- zIFzm}Rg9au`IJ~y%4bifqMfSw)2F$r`9hJY9gx*8D8y@ed8#`(ODb0n=kJ%@H7yy! zSf+sEzjkY>x@syW!K_&K`f{|k%X~pal!(^=%njkk?jwFd`TTk2`!nP5qMqH-LKV*U zdE%}+ekXVm6r}B{&RGSNlkK^7AtK|{;;{9P&*9)P1?#r7zax&f%&U(TVSYpNvS2|L zj#gQ%7o@vx@QFo7MQK=MJHIXD47LWHZ0MKZ z^X;xZiURX>yGsM$r+?$OzbnUju~@|BEB6bEYieQG#GC_sLe~0J$>QSThzuy(4bCe4 zISHdw8sCL$qfYGwl;V3batv2iN8LvYiYGtK(uRrW;^7~&9Nx6G8b5H7QwyURopO@f z-`N;At1O!HFpx0My~tqo*G3t6zCpa&W#E2TXT6%ReHMbOrL8N z=rZJ(5{#zYcfEV}wbad<3X9ei!+kj>vbKt2xf2r;YIyHP?XO#Ug$1I zr2>P9Rb%t2-Y<`re4Cj;e}48%y|4)CPsdsML-|j$ieWRJI1MmXq;Ex?@=&gmpniEZ zGczkUHMOC(15B&?frJ)`YG2344@-xm-MJu06&+7GYF80kC7@7#f_+PP-*bEj>w%<*NWMXn?Ol9(ASbJKo#5^ObG?+ktn5hkd2;l32)>(dqoqkYRSqVHp78v>l6rKDTtds=m2O$`l)mygx`EkI4 z-puu>FAhO%!lHUP))N{~qe%dy2zXeEaHuw1*D2Py0Gb8Z6}CqS2oeWe>PaQm-51CG zC9&AJbTB+zpdu)X0Fx{Pv?5wf`@zlN(vs22%KUjif@V7iP%(=|eW?os+Q!8AVIf3; zpjeo$@6LKmpH)u`hn&~!ly<4#{!Z}wUOR0)$0v-TwCK;DZ-Y1g!NGjde2+3#bl=&> zr{+@_75V}grL#^Dq+syc-w?nEPL;Nrc0;VM_p>S;c<8#lZTwr4h= zgs%0^FIOlX!^u2d9(f8fxbE-;|zjFt1e^p}3l$XaS zdCf~7J$e)s9i1uUJcBcqEByeTU9RBTef+$Fjh+1txc#E$dUNPP2J?X`N3U7F^MlfA zJj;&8Y7yQ<8yOk&d=I{;08zCGUr~w5*rXn^23OM3)NsYJ%7X{lfXLvN@g3Ugw<&(G z_udgPLCr2n6h3-%L~NxYaA&b1BHp?;JA5foRL9Y=_}$>*&U%kB9~cMV7m62jeAHoH z^4)my9633f+|WzazOGVoaoDi%*W@QQkd9#KuTMXE0%iHa^*Ak&elL6M)Naioti;Xj zB_+R2D#944>}zzU$Dr(SE2V&2{^r79`rCNEjh(##zU4k`1zU39ILZ>+cbn`k72+Q| z#SF7$LD7?>iv8>UpuD6^V#M;Kg+}lnj&%oAJs#TAE$v3?xnYch67gpkc;dExecx*D{!(b4HD2>t`}6~LP2dxapPNY z^FFZFX36Vy`>j(>ECK@9{i)EmZ{v3yx~`H?3v0-RQAg6d{`_*o-+u*fV{TN|yhJ)g z;_yz2f8E`tpz&;=0=e!`kUHsy9=E{y0ILI`gX{KVq8p3D+U`op8H6O1KQm(ux}jcS znbVZ~{=6Y}E-S%a)!aObJ_oWU5Pk0j4X+HSeqA3=oMn*H%992c;=nkq*X{ZK!X93% zI9*B3j}S`5gik-y!^FGk4n0acJV}0z8-lqI0@^Mpa%qPhJ7N8W`b=_Gqg7>DBhFz+ zcpBmamV*QB^7>(m!|ob$8iRk-ooq_P1Am|WLdb#}?c(U}z?wn{53lAA2j&`c#V1dm zS}wyRms-17VL9qZ}3Y-OWWxUawLR z)mN`x-ROx=8KG+dk2+|&=@&TStvBXQ`X88qgJ+{5kaTWSgi$VHx_&bkoRC>yzN#E< zRn+H5w_n%+zuf=`Q)am;aC4qJdo~?d05cPlEa-~8>$6+nCHU0_CR^S>4Jb9|$W{ij zr6OB1e#&%puSSgDym`}gxzy5YHHWDT+yG^kJu<-FUV%qlu@Xyt5D6M)mQvCiaEj6Olf-8`cMc44wAXU&Rv&9&w3?dwb?bg-v!NAGP;+#8%yaKmyj(i0 zudghcZ}oCnE<8;qYc=0*i{MG^YFm4}*siDkU}Xs`{Pn{!?Y=BUD&MV9_go01BQrz{ zM`{tbY;e#Zx-sB{dmGo4*LN5<7Cc`#y(@7Ybp=mLXbn5p3dA{j0U+P&Dndd+%IwDt z`0U5-NF44d#MxHR+dIM^v%1axU$BB$+Ehj?I-rtNARUn61$R13xW zErb2iX(q)aenkK+I9hgh{F=9P>+c6Zum|&onSMp%sSjmdQd|LlF{smROC|&U2fXv8%YDzm z&vAZ~Syx+cPzRhCTs7RS(*OZeAa;4;Vuj;#2a+o0*` zXf_D>=IfSDLz7(hHp^l-c6B3MY*WM$Zd`Y;Lj^7;>y?Qs5%N+K?l7mKI<3t$I#yPB zWM&grZn^?>>bsA+#tWMT%e<_Ti@3f&X1Oy?D)C_9y(7`_m4W<7jPLsEYZkt^w;3Ui zN9Z+#szpP=EOacsDZ~^i%I+knvEaZgNPmm5q_X2#55QJhz~<+{{_kJJTUcOnExf#% zXeE7E_e=}ZsQ9bb>F92fP+otf!Slq*YsYX`UJc?Tpeku!nTZJxPeVd9pyRW0a`pp~ zx=x)>(ipR;16->0+U#aZ@q07b64w6B0Z~;n(`88bM8D}_>92}Pb#--R9vmekct23_ zpi8P5V+=}Zb4yFR9amg%rR#h@TFh=go4K!0Q^C-;><;vRM0SE`_q8FwE4*0xNv-wS z&{TV@wSv<-`lO!QV&H^^;*ilq4;fg5Jo7{dVlossD{nz6!n@t5P;U3u%+Jq`0|kuK zVzJAc@T@%i)TLsey_tiL{H&?Ab|IENu8#3muw33)`b z@5*f%(>+@ z

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

S*&4RFMI|-l*WA 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 literal 0 HcmV?d00001 diff --git a/src/imview/include/imview/component/event/async_event_emitter.hpp b/src/imview/include/imview/component/event/async_event_emitter.hpp index 8b08fdf..d8ba6e2 100644 --- a/src/imview/include/imview/component/event/async_event_emitter.hpp +++ b/src/imview/include/imview/component/event/async_event_emitter.hpp @@ -13,9 +13,9 @@ namespace quickviz { class AsyncEventEmitter { public: - template - void Emit(const std::string& event_name, Args... args) { - auto event = std::make_shared(event_name, args...); + template + void Emit(EventSource type, const std::string& event_name, Args... args) { + auto event = std::make_shared(type, event_name, args...); AsyncEventDispatcher::GetInstance().Dispatch(event); } }; diff --git a/src/imview/include/imview/component/event/event.hpp b/src/imview/include/imview/component/event/event.hpp index cd789b4..e5b6602 100644 --- a/src/imview/include/imview/component/event/event.hpp +++ b/src/imview/include/imview/component/event/event.hpp @@ -14,9 +14,20 @@ #include namespace quickviz { +enum class EventSource : int { + kNone = 0, + kKeyboard, + kMouse, + kMouseButton, + kUiElement, + kApplicaton, + kCustomEvent +}; + class BaseEvent { public: virtual ~BaseEvent() = default; + virtual EventSource GetSource() const = 0; virtual std::string GetName() const = 0; }; @@ -24,10 +35,11 @@ template class Event : public BaseEvent { public: // Constructor to create an event with given arguments - Event(std::string name, Args... args) - : name_(name), data_(std::make_tuple(args...)) {} + Event(EventSource type, const std::string& name, Args... args) + : type_(type), name_(name), data_(std::make_tuple(args...)) {} // Get the stored data + EventSource GetSource() const override { return type_; } std::string GetName() const override { return name_; } const std::tuple& GetData() const { return data_; } @@ -39,6 +51,7 @@ class Event : public BaseEvent { } private: + EventSource type_; std::string name_; std::tuple data_; diff --git a/src/imview/include/imview/component/event/event_emitter.hpp b/src/imview/include/imview/component/event/event_emitter.hpp index 45e57f5..ec66b6b 100644 --- a/src/imview/include/imview/component/event/event_emitter.hpp +++ b/src/imview/include/imview/component/event/event_emitter.hpp @@ -13,9 +13,9 @@ namespace quickviz { class EventEmitter { public: - template - void Emit(const std::string& event_name, Args... args) { - auto event = std::make_shared(event_name, args...); + template + void Emit(EventSource type, const std::string& event_name, Args... args) { + auto event = std::make_shared(type, event_name, args...); EventDispatcher::GetInstance().Dispatch(event); } }; diff --git a/src/imview/include/imview/component/opengl/camera_controller.hpp b/src/imview/include/imview/component/opengl/camera_controller.hpp index d0573f8..5657533 100644 --- a/src/imview/include/imview/component/opengl/camera_controller.hpp +++ b/src/imview/include/imview/component/opengl/camera_controller.hpp @@ -20,6 +20,7 @@ class CameraController { public: CameraController(Camera& camera); + void Reset(); void SetMode(Mode mode); void ProcessKeyboard(CameraMovement direction, float delta_time); void ProcessMouseMovement(float x_offset, float y_offset); diff --git a/src/imview/src/component/opengl/camera_controller.cpp b/src/imview/src/component/opengl/camera_controller.cpp index cddf8e2..0c1d29a 100644 --- a/src/imview/src/component/opengl/camera_controller.cpp +++ b/src/imview/src/component/opengl/camera_controller.cpp @@ -11,6 +11,8 @@ namespace quickviz { CameraController::CameraController(Camera& camera) : camera_(camera) {} +void CameraController::Reset() { camera_.Reset(); } + void CameraController::SetMode(CameraController::Mode mode) { mode_ = mode; if (mode == Mode::kTopDown) { @@ -26,15 +28,16 @@ void CameraController::ProcessKeyboard( if (mode_ == Mode::kTopDown) { float velocity = camera_.GetMovementSpeed() * delta_time; glm::vec3 position = camera_.GetPosition(); - // Move only along X and Z axes - if (direction == CameraMovement::kForward) position.z -= velocity; - if (direction == CameraMovement::kBackward) position.z += velocity; - if (direction == CameraMovement::kLeft) position.x -= velocity; - if (direction == CameraMovement::kRight) position.x += velocity; - - camera_.SetPosition(position); // Update position without changing height + if (direction == CameraMovement::kUp) position.y -= velocity; + if (direction == CameraMovement::kDown) position.y += velocity; + if (direction == CameraMovement::kForward) position.x -= velocity; + if (direction == CameraMovement::kBackward) position.x += velocity; + if (direction == CameraMovement::kLeft) position.z += velocity; + if (direction == CameraMovement::kRight) position.z -= velocity; + camera_.SetPosition(position); + } else { + camera_.ProcessKeyboard(direction, delta_time); } - camera_.ProcessKeyboard(direction, delta_time); } void CameraController::ProcessMouseMovement(float x_offset, float y_offset) { diff --git a/src/imview/test/test_async_event.cpp b/src/imview/test/test_async_event.cpp index a1da4f2..22b335e 100644 --- a/src/imview/test/test_async_event.cpp +++ b/src/imview/test/test_async_event.cpp @@ -15,7 +15,8 @@ using namespace quickviz; int main(int argc, char* argv[]) { - Event event("test_event", 42, 3.14, "hello"); + Event event(EventSource::kApplicaton, "test_event", + 42, 3.14, "hello"); event.Print(); auto name = event.GetName(); @@ -38,10 +39,10 @@ int main(int argc, char* argv[]) { }); AsyncEventEmitter emitter; - emitter.Emit>("test_event", 42, 3.14, - "hello"); - emitter.Emit>("test_event", 21, 6.28, - "hello again"); + emitter.Emit>( + EventSource::kApplicaton, "test_event", 42, 3.14, "hello"); + emitter.Emit>( + EventSource::kApplicaton, "test_event", 21, 6.28, "hello again"); // AsyncEventDispatcher::GetInstance().HandleEvents(); std::thread handler_thread(&AsyncEventDispatcher::HandleEvents, diff --git a/src/imview/test/test_camera.cpp b/src/imview/test/test_camera.cpp index c815137..6aeeefa 100644 --- a/src/imview/test/test_camera.cpp +++ b/src/imview/test/test_camera.cpp @@ -72,7 +72,7 @@ void ProcessInput(GLFWwindow* window) { deltaTime); } if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) { - camera.Reset(); + camera_controller.Reset(); } } @@ -103,7 +103,7 @@ int main(int argc, char* argv[]) { int height = 1080; Window win("Test Window", width, height); - camera_controller.SetMode(CameraController::Mode::kTopDown); + camera_controller.SetMode(CameraController::Mode::kOrbit); glfwSetCursorPosCallback(win.GetWindowObject(), MouseCallback); glfwSetScrollCallback(win.GetWindowObject(), ScrollCallback); glfwSetInputMode(win.GetWindowObject(), GLFW_CURSOR, GLFW_CURSOR_DISABLED); diff --git a/src/imview/test/test_event.cpp b/src/imview/test/test_event.cpp index 0ba71df..f1bc03c 100644 --- a/src/imview/test/test_event.cpp +++ b/src/imview/test/test_event.cpp @@ -15,7 +15,8 @@ using namespace quickviz; int main(int argc, char* argv[]) { - Event event("test_event", 42, 3.14, "hello"); + Event event(EventSource::kApplicaton, "test_event", + 42, 3.14, "hello"); event.Print(); auto name = event.GetName(); @@ -39,8 +40,8 @@ int main(int argc, char* argv[]) { }); EventEmitter emitter; - emitter.Emit>("test_event", 42, 3.14, - "hello"); + emitter.Emit>( + EventSource::kApplicaton, "test_event", 42, 3.14, "hello"); return 0; } From a36bcf9bbce1021d73651895cec30842ff3dd3e2 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Thu, 14 Nov 2024 22:38:50 +0800 Subject: [PATCH 18/20] cairo: fixed cairo for opengl 3.3 --- .../imview/component/cairo_context.hpp | 5 ++-- src/imview/src/component/cairo_context.cpp | 27 +++++++++++++++++-- src/imview/src/widget/cairo_widget.cpp | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/imview/include/imview/component/cairo_context.hpp b/src/imview/include/imview/component/cairo_context.hpp index 1f12eed..79c5e93 100644 --- a/src/imview/include/imview/component/cairo_context.hpp +++ b/src/imview/include/imview/component/cairo_context.hpp @@ -11,7 +11,6 @@ #define CAIRO_CONTEXT_HPP #include -#include #include #include @@ -51,7 +50,7 @@ class CairoContext { void PopScale(); // GL-related functions can only be called after a GL context has been created - GLuint RenderToGlTexture(); + uint32_t RenderToGlTexture(); // get object of Cairo context and surface cairo_t* GetCairoObject() { return cr_; } @@ -67,7 +66,7 @@ class CairoContext { cairo_surface_t* surface_ = nullptr; cairo_t* cr_ = nullptr; - GLuint image_texture_; + uint32_t image_texture_; float aspect_ratio_ = 1.0; diff --git a/src/imview/src/component/cairo_context.cpp b/src/imview/src/component/cairo_context.cpp index d4dccfb..704f55e 100644 --- a/src/imview/src/component/cairo_context.cpp +++ b/src/imview/src/component/cairo_context.cpp @@ -9,6 +9,8 @@ #include "imview/component/cairo_context.hpp" +#include + #include namespace quickviz { @@ -89,13 +91,34 @@ void CairoContext::PopScale() { scaler_stack_.pop(); } -GLuint CairoContext::RenderToGlTexture() { +uint32_t CairoContext::RenderToGlTexture() { // bind texture glBindTexture(GL_TEXTURE_2D, image_texture_); // convert surface to OpenGL texture unsigned char* data = cairo_image_surface_get_data(surface_); - glTexImage2D(GL_TEXTURE_2D, 0, 4, width_, height_, 0, GL_BGRA, + cairo_format_t format = cairo_image_surface_get_format(surface_); + GLenum gl_format; + if (format == CAIRO_FORMAT_ARGB32) { + gl_format = GL_RGBA; + for (int i = 0; i < width_ * height_; ++i) { + unsigned char* pixel = &data[i * 4]; + unsigned char a = pixel[3]; + unsigned char r = pixel[2]; + unsigned char g = pixel[1]; + unsigned char b = pixel[0]; + pixel[0] = r; + pixel[1] = g; + pixel[2] = b; + pixel[3] = a; + } + } else if (format == CAIRO_FORMAT_RGB24) { + gl_format = GL_RGB; + } else { + throw std::runtime_error( + "[ERROR] render_to_gl_texture() - Unsupported Cairo format\n"); + } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width_, height_, 0, gl_format, GL_UNSIGNED_BYTE, data); // unbind texture diff --git a/src/imview/src/widget/cairo_widget.cpp b/src/imview/src/widget/cairo_widget.cpp index 82c8f3b..20a4e90 100644 --- a/src/imview/src/widget/cairo_widget.cpp +++ b/src/imview/src/widget/cairo_widget.cpp @@ -92,7 +92,7 @@ void CairoWidget::Render(const ImVec2& uv0, const ImVec2& uv1, draw_func(ctx_->GetCairoObject(), ctx_->GetAspectRatio()); // render cairo content to OpenGL texture - GLuint image = ctx_->RenderToGlTexture(); + uint32_t image = ctx_->RenderToGlTexture(); ImGui::Image((void*)(intptr_t)image, ImGui::GetContentRegionAvail(), uv0, uv1, tint_col, border_col); } From f0c3da0a21f532d0df2affa28037f3dc566d24c1 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Fri, 15 Nov 2024 21:19:47 +0800 Subject: [PATCH 19/20] cairo_context: removed the need to do conversion --- src/imview/src/component/cairo_context.cpp | 29 ++++++++++------------ 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/imview/src/component/cairo_context.cpp b/src/imview/src/component/cairo_context.cpp index 704f55e..685e6d3 100644 --- a/src/imview/src/component/cairo_context.cpp +++ b/src/imview/src/component/cairo_context.cpp @@ -100,26 +100,23 @@ uint32_t CairoContext::RenderToGlTexture() { cairo_format_t format = cairo_image_surface_get_format(surface_); GLenum gl_format; if (format == CAIRO_FORMAT_ARGB32) { - gl_format = GL_RGBA; - for (int i = 0; i < width_ * height_; ++i) { - unsigned char* pixel = &data[i * 4]; - unsigned char a = pixel[3]; - unsigned char r = pixel[2]; - unsigned char g = pixel[1]; - unsigned char b = pixel[0]; - pixel[0] = r; - pixel[1] = g; - pixel[2] = b; - pixel[3] = a; - } + gl_format = GL_BGRA; // Note: GL_BGRA requires OpenGL 3.2+ } else if (format == CAIRO_FORMAT_RGB24) { gl_format = GL_RGB; } else { - throw std::runtime_error( - "[ERROR] render_to_gl_texture() - Unsupported Cairo format\n"); + std::cerr << "Error: Unsupported Cairo image format." << std::endl; + return 0; } - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width_, height_, 0, gl_format, - GL_UNSIGNED_BYTE, data); + glTexImage2D(GL_TEXTURE_2D, // target + 0, // level + GL_RGBA, // internal format + width_, // width + height_, // height + 0, // border + gl_format, // format + GL_UNSIGNED_BYTE, // type + data // pixels + ); // unbind texture glBindTexture(GL_TEXTURE_2D, 0); From e582bd1c2213e2a936626a11a063556eedf13c93 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Sat, 16 Nov 2024 21:01:24 +0800 Subject: [PATCH 20/20] thread_safe_queue: sync changes from the version in libxmotion --- .../component/event/thread_safe_queue.hpp | 96 ++++++++++++------- 1 file changed, 59 insertions(+), 37 deletions(-) diff --git a/src/imview/include/imview/component/event/thread_safe_queue.hpp b/src/imview/include/imview/component/event/thread_safe_queue.hpp index 8c93f65..282c897 100644 --- a/src/imview/include/imview/component/event/thread_safe_queue.hpp +++ b/src/imview/include/imview/component/event/thread_safe_queue.hpp @@ -1,10 +1,10 @@ /* - * @file thread_safe_queue.hpp - * @date 10/10/24 - * @brief - * - * @copyright Copyright (c) 2024 Ruixiang Du (rdu) - */ +* @file thread_safe_queue.hpp +* @date 10/10/24 +* @brief +* +* @copyright Copyright (c) 2024 Ruixiang Du (rdu) +*/ #ifndef QUICKVIZ_THREAD_SAFE_QUEUE_HPP #define QUICKVIZ_THREAD_SAFE_QUEUE_HPP @@ -16,37 +16,59 @@ namespace quickviz { template class ThreadSafeQueue { - public: - // Push data into the queue - void Push(const T& data) { - std::lock_guard lock(mutex_); - queue_.push(data); - condition_.notify_one(); // Notify one waiting thread - } - - // Pop data from the queue (blocking) - T Pop() { - std::unique_lock lock(mutex_); - condition_.wait(lock, [this] { return !queue_.empty(); }); - T data = queue_.front(); - queue_.pop(); - return data; - } - - // Try to pop data from the queue (non-blocking) - bool TryPop(T& data) { - std::lock_guard lock(mutex_); - if (queue_.empty()) return false; - data = queue_.front(); - queue_.pop(); - return true; - } - - private: - std::queue queue_; - std::mutex mutex_; - std::condition_variable condition_; +public: + ThreadSafeQueue() = default; + ~ThreadSafeQueue() = default; + + // do not allow copy + ThreadSafeQueue(const ThreadSafeQueue&) = delete; + ThreadSafeQueue& operator=(const ThreadSafeQueue&) = delete; + + // allow move + ThreadSafeQueue(ThreadSafeQueue&&) noexcept { + std::lock_guard lock(mutex_); + queue_ = std::move(queue_); + } + + ThreadSafeQueue& operator=(ThreadSafeQueue&& other) noexcept { + if (this != &other) { + std::lock_guard lock(mutex_); + std::lock_guard lock_other(other.mutex_); + queue_ = std::move(other.queue_); + } + return *this; + } + + // push data into the queue + void Push(const T& data) { + std::lock_guard lock(mutex_); + queue_.push(data); + condition_.notify_one(); // Notify one waiting thread + } + + // pop data from the queue (blocking) + T Pop() { + std::unique_lock lock(mutex_); + condition_.wait(lock, [this] { return !queue_.empty(); }); + T data = queue_.front(); + queue_.pop(); + return data; + } + + // try to pop data from the queue (non-blocking) + bool TryPop(T& data) { + std::lock_guard lock(mutex_); + if (queue_.empty()) return false; + data = queue_.front(); + queue_.pop(); + return true; + } + +private: + std::queue queue_; + std::mutex mutex_; + std::condition_variable condition_; }; -} // namespace quickviz +} // namespace xmotion #endif // QUICKVIZ_THREAD_SAFE_QUEUE_HPP