From 7570f5b623557453bfa1bffaf85811feb0b6f2e0 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 25 Apr 2024 10:40:30 -0700 Subject: [PATCH 01/92] imgui attempt 1 --- dependencies/imgui/CMakeLists.txt | 11 +++++++++-- src/client/CMakeLists.txt | 20 ++++++++++---------- src/client/client.cpp | 30 +++++++++++++++++++++++++++++- src/client/main.cpp | 14 ++++---------- 4 files changed, 52 insertions(+), 23 deletions(-) diff --git a/dependencies/imgui/CMakeLists.txt b/dependencies/imgui/CMakeLists.txt index 20be0641..cb70c763 100644 --- a/dependencies/imgui/CMakeLists.txt +++ b/dependencies/imgui/CMakeLists.txt @@ -10,8 +10,14 @@ FetchContent_MakeAvailable(imgui) set(imgui-directory ${CMAKE_BINARY_DIR}/_deps/imgui-src/ PARENT_SCOPE) set(imgui-directory ${CMAKE_BINARY_DIR}/_deps/imgui-src/) -set(imgui-source -${imgui-directory}/imconfig.h +set(IMGUI_INCLUDE_DIRS + ${imgui-directory} + ${imgui-directory}/backends + PARENT_SCOPE +) + +set(IMGUI_SOURCES + ${imgui-directory}/imconfig.h ${imgui-directory}/imgui.h ${imgui-directory}/imgui.cpp ${imgui-directory}/imgui_draw.cpp @@ -24,5 +30,6 @@ ${imgui-directory}/imconfig.h ${imgui-directory}/imgui_demo.cpp ${imgui-directory}/backends/imgui_impl_glfw.cpp ${imgui-directory}/backends/imgui_impl_opengl3.cpp + ${imgui-directory}misc/cpp/imgui_stdlib.cpp PARENT_SCOPE ) \ No newline at end of file diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 6f4e3f0c..f7d7a838 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -1,26 +1,26 @@ set(TARGET_NAME client) set(LIB_NAME game_client_lib) +# Subprojects +# potential review later +add_subdirectory(../../dependencies/glfw ${CMAKE_BINARY_DIR}/glfw) +add_subdirectory(../../dependencies/glm ${CMAKE_BINARY_DIR}/glm) +add_subdirectory(../../dependencies/imgui ${CMAKE_BINARY_DIR}/imgui) +add_subdirectory(../../dependencies/glew ${CMAKE_BINARY_DIR}/glew) + set(FILES camera.cpp client.cpp cube.cpp util.cpp lobbyfinder.cpp - ${imgui-source} + ${IMGUI_SOURCES} ) # OpenGL set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL REQUIRED) -# Subprojects -# potential review later -add_subdirectory(../../dependencies/glfw ${CMAKE_BINARY_DIR}/glfw) -add_subdirectory(../../dependencies/glm ${CMAKE_BINARY_DIR}/glm) -add_subdirectory(../../dependencies/imgui ${CMAKE_BINARY_DIR}/imgui) -add_subdirectory(../../dependencies/glew ${CMAKE_BINARY_DIR}/glew) - add_library(${LIB_NAME} STATIC ${FILES}) target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) @@ -34,13 +34,13 @@ target_link_libraries(${LIB_NAME} Boost::serialization nlohmann_json::nlohmann_json ) -target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${IMGUI_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static) add_executable(${TARGET_NAME} main.cpp) target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${IMGUI_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") target_link_libraries(${TARGET_NAME} PRIVATE game_shared_lib ${LIB_NAME}) target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static) diff --git a/src/client/client.cpp b/src/client/client.cpp index 8b224a9d..c8d9bdbb 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -3,6 +3,10 @@ #include #include #include +#include "imgui.h" +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" + #include #include @@ -84,10 +88,26 @@ int Client::init() { return false; } + // Set up IMGUI + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& imGuiIO = ImGui::GetIO(); + imGuiIO.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + imGuiIO.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + // imGuiIO.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // IF using Docking Branch + + // Setup Platform/Renderer backends + ImGui_ImplGlfw_InitForOpenGL(window, true); // Second param install_callback=true will install GLFW callbacks and chain to existing ones. + ImGui_ImplOpenGL3_Init(); + return 0; } int Client::cleanup() { + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + glDeleteProgram(shaderProgram); return 0; } @@ -97,10 +117,18 @@ void Client::displayCallback() { /* Render here */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - if (this->gameState.phase == GamePhase::GAME) { + if (this->gameState.phase == GamePhase::TITLE_SCREEN) { + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + ImGui::ShowDemoWindow(); // Show demo window! :) + } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); } + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + /* Poll for and process events */ glfwPollEvents(); glfwSwapBuffers(window); diff --git a/src/client/main.cpp b/src/client/main.cpp index 801cdf62..cc2958a5 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -50,17 +50,11 @@ int main(int argc, char* argv[]) LobbyFinder lobby_finder(context, config); Client client(context, config); if (config.client.lobby_discovery) { - // TODO: once we have UI, there should be a way to connect based on - // this. Right now, there isn't really a way to react to the information - // the LobbyFinder is gathering. - std::cerr << "Error: lobby discovery not enabled yet for client-side." - << std::endl; - std::exit(1); - // lobby_finder.startSearching(); - } else { - client.connectAndListen(config.network.server_ip); + lobby_finder.startSearching(); } - + // } else { + // client.connectAndListen(config.network.server_ip); + // } if (client.init() == -1) { exit(EXIT_FAILURE); From 85cd764142894a9409221fb90b5757a89462c8f4 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 25 Apr 2024 11:31:41 -0700 Subject: [PATCH 02/92] start custom gui --- dependencies/imgui/CMakeLists.txt | 6 ------ include/client/gui/element.hpp | 3 +++ include/client/gui/gui.hpp | 12 ++++++++++++ src/client/client.cpp | 18 +++++++++++------- src/client/main.cpp | 6 +++--- 5 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 include/client/gui/element.hpp create mode 100644 include/client/gui/gui.hpp diff --git a/dependencies/imgui/CMakeLists.txt b/dependencies/imgui/CMakeLists.txt index cb70c763..29e78a2b 100644 --- a/dependencies/imgui/CMakeLists.txt +++ b/dependencies/imgui/CMakeLists.txt @@ -17,15 +17,9 @@ set(IMGUI_INCLUDE_DIRS ) set(IMGUI_SOURCES - ${imgui-directory}/imconfig.h - ${imgui-directory}/imgui.h ${imgui-directory}/imgui.cpp ${imgui-directory}/imgui_draw.cpp - ${imgui-directory}/imgui_internal.h ${imgui-directory}/imgui_widgets.cpp - ${imgui-directory}/imstb_rectpack.h - ${imgui-directory}/imstb_textedit.h - ${imgui-directory}/imstb_truetype.h ${imgui-directory}/imgui_tables.cpp ${imgui-directory}/imgui_demo.cpp ${imgui-directory}/backends/imgui_impl_glfw.cpp diff --git a/include/client/gui/element.hpp b/include/client/gui/element.hpp new file mode 100644 index 00000000..976bf1f5 --- /dev/null +++ b/include/client/gui/element.hpp @@ -0,0 +1,3 @@ +namespace gui { + +} diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp new file mode 100644 index 00000000..a7aa9a3c --- /dev/null +++ b/include/client/gui/gui.hpp @@ -0,0 +1,12 @@ +#include "client/core.hpp" + +namespace gui { + class Window { + public: + Window(); + + void render(GLUint shader); + + private: + }; +} diff --git a/src/client/client.cpp b/src/client/client.cpp index c8d9bdbb..c3741f28 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -28,7 +28,8 @@ Client::Client(boost::asio::io_context& io_context, GameConfig config): resolver(io_context), socket(io_context), config(config), - gameState(GamePhase::TITLE_SCREEN, config) + gameState(GamePhase::TITLE_SCREEN, config), + session(nullptr) { } @@ -117,11 +118,12 @@ void Client::displayCallback() { /* Render here */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + if (this->gameState.phase == GamePhase::TITLE_SCREEN) { - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - ImGui::ShowDemoWindow(); // Show demo window! :) + } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); } @@ -147,12 +149,14 @@ void Client::idleCallback(boost::asio::io_context& context) { if(is_held_down) movement.value() += glm::vec3(0.0f, -cubeMovementDelta, 0.0f); - if (movement.has_value()) { + if (movement.has_value() && this->session != nullptr) { auto eid = 0; this->session->sendEventAsync(Event(eid, EventType::MoveRelative, MoveRelativeEvent(eid, movement.value()))); } - processServerInput(context); + if (this->session != nullptr) { + processServerInput(context); + } } void Client::processServerInput(boost::asio::io_context& context) { diff --git a/src/client/main.cpp b/src/client/main.cpp index cc2958a5..d2a9b231 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -49,9 +49,9 @@ int main(int argc, char* argv[]) boost::asio::io_context context; LobbyFinder lobby_finder(context, config); Client client(context, config); - if (config.client.lobby_discovery) { - lobby_finder.startSearching(); - } + // if (config.client.lobby_discovery) { + // lobby_finder.startSearching(); + // } // } else { // client.connectAndListen(config.network.server_ip); // } From fb455edc1fb49c29b0445415092b2eab6b6b5dd2 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 25 Apr 2024 12:38:53 -0700 Subject: [PATCH 03/92] start laying out widgets --- include/client/gui/gui.hpp | 18 ++++++---- include/client/gui/widget.hpp | 36 +++++++++++++++++++ .../gui/{element.hpp => widgettype.hpp} | 4 +++ 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 include/client/gui/widget.hpp rename include/client/gui/{element.hpp => widgettype.hpp} (54%) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index a7aa9a3c..40756e8f 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -1,12 +1,16 @@ -#include "client/core.hpp" +#pragma once + +// #include "client/core.hpp" namespace gui { - class Window { - public: - Window(); - void render(GLUint shader); +class Window { + public: + Window(); + + void render(); + + private: +}; - private: - }; } diff --git a/include/client/gui/widget.hpp b/include/client/gui/widget.hpp new file mode 100644 index 00000000..0179a36e --- /dev/null +++ b/include/client/gui/widget.hpp @@ -0,0 +1,36 @@ +#pragma once + +// #include "client/core.hpp" + +#include +#include +#include + +namespace gui { + +using WidgetCallback = std::function; + +class Widget { + public: + Widget(std::string asset_path); + + Widget& place(float r_x, float r_y); + Widget& onClick(WidgetCallback callback); + Widget& onHover(WidgetCallback callback); + + virtual void render() = 0; + + private: + std::vector onClicks; + std::vector onHovers; + + /// @brief relative x position on screen from -1 -> 1 + float r_x { 0.0f }; + /// @brief relative y position on screen from -1 -> 1 + float r_y { 0.0f }; + + /// @brief filepath to image for the widget + std::string asset_path; +}; + +} diff --git a/include/client/gui/element.hpp b/include/client/gui/widgettype.hpp similarity index 54% rename from include/client/gui/element.hpp rename to include/client/gui/widgettype.hpp index 976bf1f5..a8a18115 100644 --- a/include/client/gui/element.hpp +++ b/include/client/gui/widgettype.hpp @@ -1,3 +1,7 @@ +#pragma once + namespace gui { + + } From 49c1f470c17c27960c649ba006cbef77f821c852 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 26 Apr 2024 10:57:22 -0700 Subject: [PATCH 04/92] more gui setup --- include/client/gui/font.hpp | 37 ++++++++++++++++ include/client/gui/gui.hpp | 17 +++---- include/client/gui/widget.hpp | 36 --------------- include/client/gui/widget/dyntext.hpp | 28 ++++++++++++ include/client/gui/widget/options.hpp | 26 +++++++++++ include/client/gui/widget/type.hpp | 11 +++++ include/client/gui/widget/widget.hpp | 54 ++++++++++++++++++++++ include/client/gui/widgettype.hpp | 7 --- include/client/gui/window.hpp | 14 ++++++ src/client/gui/font.cpp | 22 +++++++++ src/client/gui/widget/widget.cpp | 64 +++++++++++++++++++++++++++ 11 files changed, 261 insertions(+), 55 deletions(-) create mode 100644 include/client/gui/font.hpp delete mode 100644 include/client/gui/widget.hpp create mode 100644 include/client/gui/widget/dyntext.hpp create mode 100644 include/client/gui/widget/options.hpp create mode 100644 include/client/gui/widget/type.hpp create mode 100644 include/client/gui/widget/widget.hpp delete mode 100644 include/client/gui/widgettype.hpp create mode 100644 include/client/gui/window.hpp create mode 100644 src/client/gui/font.cpp create mode 100644 src/client/gui/widget/widget.cpp diff --git a/include/client/gui/font.hpp b/include/client/gui/font.hpp new file mode 100644 index 00000000..1fc9db07 --- /dev/null +++ b/include/client/gui/font.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace gui { + +enum class Font { + READABLE, +}; + +std::string getFontPath(Font font) { + switch (font) { + + } +} + +enum class FontSize { + TINY, + SMALL, + MEDIUM, + LARGE, + HUGE +}; + +constexpr std::size_t getFontSizePt(FontSize size) { + switch (size) { + case FontSize::TINY: return 8; + case FontSize::SMALL: return 12; + default: + case FontSize::MEDIUM: return 16; + case FontSize::LARGE: return 24; + case FontSize::HUGE: return 36; + } +} + +} \ No newline at end of file diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 40756e8f..df19c21b 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -2,15 +2,8 @@ // #include "client/core.hpp" -namespace gui { - -class Window { - public: - Window(); - - void render(); - - private: -}; - -} +// Include all gui headers so everyone else just needs to include this file +#include "client/gui/widget/options.hpp" +#include "client/gui/widget/type.hpp" +#include "client/gui/widget/widget.hpp" +#include "client/gui/window.hpp" \ No newline at end of file diff --git a/include/client/gui/widget.hpp b/include/client/gui/widget.hpp deleted file mode 100644 index 0179a36e..00000000 --- a/include/client/gui/widget.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -// #include "client/core.hpp" - -#include -#include -#include - -namespace gui { - -using WidgetCallback = std::function; - -class Widget { - public: - Widget(std::string asset_path); - - Widget& place(float r_x, float r_y); - Widget& onClick(WidgetCallback callback); - Widget& onHover(WidgetCallback callback); - - virtual void render() = 0; - - private: - std::vector onClicks; - std::vector onHovers; - - /// @brief relative x position on screen from -1 -> 1 - float r_x { 0.0f }; - /// @brief relative y position on screen from -1 -> 1 - float r_y { 0.0f }; - - /// @brief filepath to image for the widget - std::string asset_path; -}; - -} diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp new file mode 100644 index 00000000..e021a20e --- /dev/null +++ b/include/client/gui/widget/dyntext.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "client/gui/widget/widget.hpp" +#include "client/gui/font.hpp" + +namespace gui { +namespace widget { + +class DynText : public Widget { +public: + struct Options { + Font font {Font::READABLE}; + FontSize font_size {FontSize::MEDIUM}; + }; + + DynText(std::string text, Options options = {}); + + void render() override; + +private: + Options options; + +}; + +} +} diff --git a/include/client/gui/widget/options.hpp b/include/client/gui/widget/options.hpp new file mode 100644 index 00000000..5498ab72 --- /dev/null +++ b/include/client/gui/widget/options.hpp @@ -0,0 +1,26 @@ +#pragma once + +namespace gui { + +enum class HAlign { + LEFT, + CENTER, + RIGHT, + + NONE +}; + +enum class VAlign { + TOP, + CENTER, + BOTTOM, + + NONE +}; + +enum class JustifyContent { + VERTICAL, + HORIZONTAL +}; + +} \ No newline at end of file diff --git a/include/client/gui/widget/type.hpp b/include/client/gui/widget/type.hpp new file mode 100644 index 00000000..cd235767 --- /dev/null +++ b/include/client/gui/widget/type.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace gui { +namespace widget { + +enum class Type { + DynText +}; + +} +} diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp new file mode 100644 index 00000000..3bc3224c --- /dev/null +++ b/include/client/gui/widget/widget.hpp @@ -0,0 +1,54 @@ +#pragma once + +// #include "client/core.hpp" +#include "client/gui/widget/type.hpp" +#include "client/gui/widget/options.hpp" + +#include +#include +#include + +namespace gui { +namespace widget { + +using Callback = std::function; +using CallbackHandle = std::size_t; + +class Widget { +public: + explicit Widget(Type type); + + Widget& setSize(std::size_t width, std::size_t height); + Widget& setAlign(VAlign valign, HAlign halign); + Widget& setAlign(VAlign valign); + Widget& setAlign(HAlign halign); + Widget& addOnClick(Callback callback, CallbackHandle& handle); + Widget& addOnClick(Callback callback); + Widget& addOnHover(Callback callback, CallbackHandle& handle); + Widget& addOnHover(Callback callback); + + void removeOnClick(CallbackHandle handle); + void removeOnHover(CallbackHandle handle); + + virtual void render() = 0; + + [[nodiscard]] Type getType() const; + +protected: + Type type; + std::size_t width {0}; + std::size_t height {0}; + + VAlign valign {VAlign::NONE}; + HAlign halign {HAlign::NONE}; + + std::unordered_map on_clicks; + std::unordered_map on_hovers; + +private: + CallbackHandle next_click_handle {0}; + CallbackHandle next_hover_handle {0}; +}; + +} +} diff --git a/include/client/gui/widgettype.hpp b/include/client/gui/widgettype.hpp deleted file mode 100644 index a8a18115..00000000 --- a/include/client/gui/widgettype.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -namespace gui { - - - -} diff --git a/include/client/gui/window.hpp b/include/client/gui/window.hpp new file mode 100644 index 00000000..82df2ca2 --- /dev/null +++ b/include/client/gui/window.hpp @@ -0,0 +1,14 @@ +#include "client/gui/gui.hpp" + +namespace gui { + +class Window { +public: + Window(); + + void render(); + +private: +}; + +} \ No newline at end of file diff --git a/src/client/gui/font.cpp b/src/client/gui/font.cpp new file mode 100644 index 00000000..f054fc12 --- /dev/null +++ b/src/client/gui/font.cpp @@ -0,0 +1,22 @@ +#include "client/gui/font.hpp" + +namespace gui { + +std::string getFontPath(Font font) { + switch (font) { + case Font::READABLE: return "/path/to/readable/font"; + } +} + +constexpr std::size_t getFontSizePt(FontSize size) { + switch (size) { + case FontSize::TINY: return 8; + case FontSize::SMALL: return 12; + default: + case FontSize::MEDIUM: return 16; + case FontSize::LARGE: return 24; + case FontSize::HUGE: return 36; + } +} + +} diff --git a/src/client/gui/widget/widget.cpp b/src/client/gui/widget/widget.cpp new file mode 100644 index 00000000..73979706 --- /dev/null +++ b/src/client/gui/widget/widget.cpp @@ -0,0 +1,64 @@ +#include "client/gui/widget/widget.hpp" + +namespace gui { +namespace widget { + +Widget::Widget(Type type): + type(type) +{ + +} + +Widget& Widget::setSize(std::size_t width, std::size_t height) { + this->width = width; + this->height = height; + return *this; +} + +Widget& Widget::setAlign(VAlign valign, HAlign halign) { + this->valign = valign; + this->halign = halign; +} + +Widget& Widget::setAlign(HAlign halign) { + this->setAlign(VAlign::NONE, halign); +} + +Widget& Widget::setAlign(VAlign valign) { + this->setAlign(valign, HAlign::NONE); +} + +Widget& Widget::addOnClick(Callback callback, CallbackHandle& handle) { + handle = this->next_click_handle++; + this->on_clicks.insert({handle, callback}); +} + +Widget& Widget::addOnClick(Callback callback) { + CallbackHandle handle = 0; + this->addOnClick(callback, handle); +} + +Widget& Widget::addOnHover(Callback callback, CallbackHandle& handle) { + handle = this->next_hover_handle++; + this->on_hovers.insert({handle, callback}); +} + +Widget& Widget::addOnHover(Callback callback) { + CallbackHandle handle = 0; + this->addOnHover(callback, handle); +} + +void Widget::removeOnClick(CallbackHandle handle) { + this->on_clicks.erase(handle); +} + +void Widget::removeOnHover(CallbackHandle handle) { + this->on_hovers.erase(handle); +} + +Type Widget::getType() const { + return this->type; +} + +} +} From 02309290ae95aeeb2bcf545f3c0fd2ed21ec410a Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 26 Apr 2024 12:35:08 -0700 Subject: [PATCH 05/92] add untested font loading code --- dependencies/freetype/CMakeLists.txt | 9 +++ fonts/Lato-Regular.ttf | Bin 0 -> 75152 bytes include/client/gui/font.hpp | 37 ---------- include/client/gui/font/font.hpp | 29 ++++++++ include/client/gui/font/loader.hpp | 39 ++++++++++ include/client/gui/widget/dyntext.hpp | 10 +-- include/client/gui/widget/options.hpp | 2 +- include/client/gui/widget/type.hpp | 4 +- include/client/gui/widget/widget.hpp | 4 +- include/shared/utilities/root_path.hpp | 8 ++ src/client/CMakeLists.txt | 9 ++- src/client/client.cpp | 17 ++--- src/client/gui/font.cpp | 22 ------ src/client/gui/font/font.cpp | 16 ++++ src/client/gui/font/loader.cpp | 97 +++++++++++++++++++++++++ src/client/main.cpp | 3 + src/shared/utilities/config.cpp | 8 +- src/shared/utilities/root_path.cpp | 15 ++++ 18 files changed, 236 insertions(+), 93 deletions(-) create mode 100644 dependencies/freetype/CMakeLists.txt create mode 100644 fonts/Lato-Regular.ttf delete mode 100644 include/client/gui/font.hpp create mode 100644 include/client/gui/font/font.hpp create mode 100644 include/client/gui/font/loader.hpp create mode 100644 include/shared/utilities/root_path.hpp delete mode 100644 src/client/gui/font.cpp create mode 100644 src/client/gui/font/font.cpp create mode 100644 src/client/gui/font/loader.cpp create mode 100644 src/shared/utilities/root_path.cpp diff --git a/dependencies/freetype/CMakeLists.txt b/dependencies/freetype/CMakeLists.txt new file mode 100644 index 00000000..b62c76f2 --- /dev/null +++ b/dependencies/freetype/CMakeLists.txt @@ -0,0 +1,9 @@ +#shoutout chatgpt + +FetchContent_Declare( + freetype + GIT_REPOSITORY https://gitlab.freedesktop.org/freetype/freetype.git + GIT_TAG VER-2-13-2 +) + +FetchContent_MakeAvailable(freetype) \ No newline at end of file diff --git a/fonts/Lato-Regular.ttf b/fonts/Lato-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bb2e8875a993f9c7d9e45d0eeffa839550cc6287 GIT binary patch literal 75152 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 -#include - -namespace gui { - -enum class Font { - READABLE, -}; - -std::string getFontPath(Font font) { - switch (font) { - - } -} - -enum class FontSize { - TINY, - SMALL, - MEDIUM, - LARGE, - HUGE -}; - -constexpr std::size_t getFontSizePt(FontSize size) { - switch (size) { - case FontSize::TINY: return 8; - case FontSize::SMALL: return 12; - default: - case FontSize::MEDIUM: return 16; - case FontSize::LARGE: return 24; - case FontSize::HUGE: return 36; - } -} - -} \ No newline at end of file diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp new file mode 100644 index 00000000..ef0c5d5c --- /dev/null +++ b/include/client/gui/font/font.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "shared/utilities/root_path.hpp" + +#include +#include + +namespace gui::font { + +enum class Font { + MENU, + TEXT, +}; + +enum FontSizePx { + SMALL = 12, + MEDIUM = 24, + LARGE = 36 +}; + +enum class FontColor { + BLACK, + RED, + BLUE +}; + +std::string getFilepath(Font font); + +} diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp new file mode 100644 index 00000000..0fc9ec42 --- /dev/null +++ b/include/client/gui/font/loader.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "client/core.hpp" +#include "client/gui/font/font.hpp" + +#include + +namespace gui::font { + +// modified from https://learnopengl.com/In-Practice/Text-Rendering + +struct Character { + unsigned int texture_id; /// id handle for glyph texture + glm::ivec2 size; /// size of glyph + glm::ivec2 bearing; /// offset from baseline to left/top of glyph + unsigned int advance; /// offset to advance to next glyph +}; + +struct font_pair_hash { + std::size_t operator()(const std::pair& p) const; +} + +class Loader { +public: + Loader() = default; + + bool init(); + +private: + void _loadFont(Font font); + + std::unordered_map< + std::pair, + std::unordered_map, + font_pair_hash + > font_map; +}; + +} diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index e021a20e..ae5daf2e 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -3,16 +3,15 @@ #include #include "client/gui/widget/widget.hpp" -#include "client/gui/font.hpp" +#include "client/gui/font/font.hpp" -namespace gui { -namespace widget { +namespace gui::widget { class DynText : public Widget { public: struct Options { - Font font {Font::READABLE}; - FontSize font_size {FontSize::MEDIUM}; + font::Font font {font::Font::TEXT}; + font::FontSizePx font_size {font::FontSizePx::MEDIUM}; }; DynText(std::string text, Options options = {}); @@ -25,4 +24,3 @@ class DynText : public Widget { }; } -} diff --git a/include/client/gui/widget/options.hpp b/include/client/gui/widget/options.hpp index 5498ab72..ff84d19e 100644 --- a/include/client/gui/widget/options.hpp +++ b/include/client/gui/widget/options.hpp @@ -1,6 +1,6 @@ #pragma once -namespace gui { +namespace gui::widget { enum class HAlign { LEFT, diff --git a/include/client/gui/widget/type.hpp b/include/client/gui/widget/type.hpp index cd235767..774bf49e 100644 --- a/include/client/gui/widget/type.hpp +++ b/include/client/gui/widget/type.hpp @@ -1,11 +1,9 @@ #pragma once -namespace gui { -namespace widget { +namespace gui::widget { enum class Type { DynText }; } -} diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index 3bc3224c..4818822a 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -8,8 +8,7 @@ #include #include -namespace gui { -namespace widget { +namespace gui::widget { using Callback = std::function; using CallbackHandle = std::size_t; @@ -51,4 +50,3 @@ class Widget { }; } -} diff --git a/include/shared/utilities/root_path.hpp b/include/shared/utilities/root_path.hpp new file mode 100644 index 00000000..0a63274f --- /dev/null +++ b/include/shared/utilities/root_path.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +/** + * Helper function to get a filepath of the root repo, useful for loading in files + */ +boost::filesystem::path getRepoRoot(); \ No newline at end of file diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index f7d7a838..54ebceb9 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(../../dependencies/glfw ${CMAKE_BINARY_DIR}/glfw) add_subdirectory(../../dependencies/glm ${CMAKE_BINARY_DIR}/glm) add_subdirectory(../../dependencies/imgui ${CMAKE_BINARY_DIR}/imgui) add_subdirectory(../../dependencies/glew ${CMAKE_BINARY_DIR}/glew) +add_subdirectory(../../dependencies/freetype ${CMAKE_BINARY_DIR}/freetype) set(FILES camera.cpp @@ -34,15 +35,15 @@ target_link_libraries(${LIB_NAME} Boost::serialization nlohmann_json::nlohmann_json ) -target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${IMGUI_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") -target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static) +target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${IMGUI_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static freetype) add_executable(${TARGET_NAME} main.cpp) target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${IMGUI_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${IMGUI_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") target_link_libraries(${TARGET_NAME} PRIVATE game_shared_lib ${LIB_NAME}) -target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static) +target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static freetype) target_include_directories(${TARGET_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) target_link_libraries(${TARGET_NAME} diff --git a/src/client/client.cpp b/src/client/client.cpp index c3741f28..b44339fd 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -89,17 +89,12 @@ int Client::init() { return false; } - // Set up IMGUI - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& imGuiIO = ImGui::GetIO(); - imGuiIO.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls - imGuiIO.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls - // imGuiIO.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // IF using Docking Branch - - // Setup Platform/Renderer backends - ImGui_ImplGlfw_InitForOpenGL(window, true); // Second param install_callback=true will install GLFW callbacks and chain to existing ones. - ImGui_ImplOpenGL3_Init(); + FT_Library ft; + if (FT_Init_FreeType(&ft)) { + std::cerr << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; + return false; + } + return 0; } diff --git a/src/client/gui/font.cpp b/src/client/gui/font.cpp deleted file mode 100644 index f054fc12..00000000 --- a/src/client/gui/font.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "client/gui/font.hpp" - -namespace gui { - -std::string getFontPath(Font font) { - switch (font) { - case Font::READABLE: return "/path/to/readable/font"; - } -} - -constexpr std::size_t getFontSizePt(FontSize size) { - switch (size) { - case FontSize::TINY: return 8; - case FontSize::SMALL: return 12; - default: - case FontSize::MEDIUM: return 16; - case FontSize::LARGE: return 24; - case FontSize::HUGE: return 36; - } -} - -} diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp new file mode 100644 index 00000000..255f3603 --- /dev/null +++ b/src/client/gui/font/font.cpp @@ -0,0 +1,16 @@ +#include "client/gui/font/font.hpp" + +#include "shared/utilities/root_path.hpp" + +namespace gui::font { + +std::string getFilepath(Font font) { + auto dir = getRepoRoot() / "fonts"; + switch (font) { + case Font::MENU: return (dir / "Lato-Regular.tff").string(); + default: + case Font::TEXT: return (dir / "Lato-Regular.tff").string(); + } +} + +} diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp new file mode 100644 index 00000000..50bf55c7 --- /dev/null +++ b/src/client/gui/font/loader.cpp @@ -0,0 +1,97 @@ +#include "client/gui/font/loader.hpp" + +#include + +// freetype needs this extra include for whatever unholy reason +#include +#include FT_FREETYPE_H + +#include "shared/utilities/root_path.hpp" + +namespace gui::font { + +std::size_t font_pair_hash::operator()(const std::pair& p) { + // idk if this is actually doing what I think it is doing + return std::hash(static_cast(p.first) << 32 ^ p.second); +} + +bool Loader::init() { + FT_Library ft; + if (FT_Init_FreeType(&ft)) { + std::cerr << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; + return false; + } + + // we mess with some alignment when creating the textures, + // so this is supposed to prevent seg faults related to that + glPixelStorei(GL_UNPACK_ALIGNMVkxENT, 1); + + if (!this->_loadFont(Font::MENU)) { + return false; + } + if (!this->_loadFont(Font::TEXT)) { + return false; + } + + return true; +} + +bool Loader::_loadFont(Font font) { + auto path = font::getFilepath(font); + + FT_Face face; + if (FT_New_Face(ft, path, 0, &face)) { + std::cout << "ERROR::FREETYPE: Failed to load font at " << path << std::endl; + return -1; + } + + for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE}) { + FT_Set_Pixel_Sizes(face, 0, font_size); + std::unordered_map characters; + for (unsigned char c = 0; c < 128; c++) { + // load character glyph + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { + std::cerr << "ERROR::FREETYTPE: Failed to load Glyph " << c << std::endl; + return false; + } + + // generate texture + unsigned int texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RED, + face->glyph->bitmap.width, + face->glyph->bitmap.rows, + 0, + GL_RED, + GL_UNSIGNED_BYTE, + face->glyph->bitmap.buffer + ); + // set texture options + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // now store character for later use + Character character = { + texture, + glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), + glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), + face->glyph->advance.x + }; + characters.insert({c, character}); + } + + this->font_map.insert({{font, font_size}, characters}); + } + + FT_Done_Face(face); + FT_Done_FreeType(ft); + + return true; +} + +} diff --git a/src/client/main.cpp b/src/client/main.cpp index d2a9b231..cf606f52 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -1,6 +1,7 @@ #include #include + #include #include "client/client.hpp" @@ -59,6 +60,8 @@ int main(int argc, char* argv[]) if (client.init() == -1) { exit(EXIT_FAILURE); } + + GLFWwindow* window = client.getWindow(); if (!window) exit(EXIT_FAILURE); diff --git a/src/shared/utilities/config.cpp b/src/shared/utilities/config.cpp index 3cd0ae59..6a4534a6 100644 --- a/src/shared/utilities/config.cpp +++ b/src/shared/utilities/config.cpp @@ -1,6 +1,6 @@ #include "shared/utilities/config.hpp" -#include +#include "shared/utilities/root_path.hpp" #include #include @@ -16,11 +16,7 @@ GameConfig GameConfig::parse(int argc, char** argv) { // cppcheck-suppress const exit(1); } - // With the cmake setup we know the executables are three directories down from the root of the - // repository, where the config file lives - boost::filesystem::path root_path = boost::dll::program_location().parent_path().parent_path().parent_path(); - - boost::filesystem::path filepath = root_path / "config.json"; + boost::filesystem::path filepath = getRepoRoot() / "config.json"; if (argc == 2) { filepath = argv[1]; } diff --git a/src/shared/utilities/root_path.cpp b/src/shared/utilities/root_path.cpp new file mode 100644 index 00000000..3a28f7ed --- /dev/null +++ b/src/shared/utilities/root_path.cpp @@ -0,0 +1,15 @@ +#include "shared/utilities/root_path.hpp" + +#include +#include + +boost::filesystem::path getRepoRoot() { + /** + * build + * bin + * client + * server + * ... + */ + return boost::dll::program_location().parent_path().parent_path().parent_path(); +} \ No newline at end of file From c4a51a4d5b4bcacc05a39cd7dfbe49b15aa34b8e Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 27 Apr 2024 12:55:22 -0700 Subject: [PATCH 06/92] add zlib to nix shell --- shell.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/shell.nix b/shell.nix index 01be7ef5..f398b3e7 100644 --- a/shell.nix +++ b/shell.nix @@ -9,6 +9,7 @@ mkShell { gnumake gcc13 gdb + zlib wayland wayland-scanner From a9bc44cbd85883a19f5db217b673b283779ea075 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 27 Apr 2024 17:07:32 -0700 Subject: [PATCH 07/92] add assimp and stb --- CMakeLists.txt | 6 ++++ dependencies/assimp/CMakeLists.txt | 15 ++++++++++ dependencies/glew/CMakeLists.txt | 6 ++-- dependencies/stb/CMakeLists.txt | 11 +++++++ src/client/CMakeLists.txt | 48 ++++++++++++++++++++++-------- src/client/tests/CMakeLists.txt | 16 ++++++++-- 6 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 dependencies/assimp/CMakeLists.txt create mode 100644 dependencies/stb/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ec3ce19..15a4e224 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,12 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS True) # Tell CMake to ignore warnings from stuff in our dependencies. Surely GLM # knows what it is doing... https://7tv.app/emotes/61e8fae862858c6406126ced +IF (NOT WIN32) + set_property( + DIRECTORY build + PROPERTY COMPILE_OPTIONS "-w" + ) +ENDIF() # Add google test to CMake add_subdirectory(dependencies/google-test) diff --git a/dependencies/assimp/CMakeLists.txt b/dependencies/assimp/CMakeLists.txt new file mode 100644 index 00000000..13eb3a9c --- /dev/null +++ b/dependencies/assimp/CMakeLists.txt @@ -0,0 +1,15 @@ +include(FetchContent) + +FetchContent_Declare(assimp + GIT_REPOSITORY https://github.com/assimp/assimp + GIT_TAG v5.3.1 + GIT_PROGRESS TRUE +) + +FetchContent_MakeAvailable(assimp) + +set(ASSIMP_INCLUDE_DIRS + ${CMAKE_BINARY_DIR}/_deps/assimp-src/include/ + ${CMAKE_BINARY_DIR}/_deps/assimp-build/include/ + PARENT_SCOPE +) diff --git a/dependencies/glew/CMakeLists.txt b/dependencies/glew/CMakeLists.txt index 8a750c9b..3cf51284 100644 --- a/dependencies/glew/CMakeLists.txt +++ b/dependencies/glew/CMakeLists.txt @@ -8,6 +8,6 @@ FetchContent_Declare(glew FetchContent_MakeAvailable(glew) -# find_package(GLEW 2.0 REQUIRED) -# target_link_libraries(Starting GLEW::GLEW) - +set(GLEW_INCLUDE_DIRS + ${CMAKE_BINARY_DIR}/_deps/glew-src/include + PARENT_SCOPE) diff --git a/dependencies/stb/CMakeLists.txt b/dependencies/stb/CMakeLists.txt new file mode 100644 index 00000000..84bd7aba --- /dev/null +++ b/dependencies/stb/CMakeLists.txt @@ -0,0 +1,11 @@ +include(FetchContent) + +FetchContent_Declare(stb + GIT_REPOSITORY https://github.com/nothings/stb + GIT_PROGRESS TRUE +) +FetchContent_MakeAvailable(stb) + +set(STB_INCLUDE_DIRS + ${CMAKE_BINARY_DIR}/_deps/stb-src/ + PARENT_SCOPE) diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 225e7b3a..e2f27751 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -7,7 +7,8 @@ set(FILES cube.cpp util.cpp lobbyfinder.cpp - shaders.cpp + shader.cpp + model.cpp ${imgui-source} ) @@ -21,10 +22,22 @@ add_subdirectory(../../dependencies/glfw ${CMAKE_BINARY_DIR}/glfw) add_subdirectory(../../dependencies/glm ${CMAKE_BINARY_DIR}/glm) add_subdirectory(../../dependencies/imgui ${CMAKE_BINARY_DIR}/imgui) add_subdirectory(../../dependencies/glew ${CMAKE_BINARY_DIR}/glew) +add_subdirectory(../../dependencies/assimp ${CMAKE_BINARY_DIR}/assimp) +add_subdirectory(../../dependencies/stb ${CMAKE_BINARY_DIR}/stb) add_library(${LIB_NAME} STATIC ${FILES}) target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) +target_include_directories(${LIB_NAME} + PRIVATE + ${BOOST_LIBRARY_INCLUDES} + ${OPENGL_INCLUDE_DIRS} + glfw + glm + ${imgui-directory} + ${GLEW_INCLUDE_DIRS} + ${ASSIMP_INCLUDE_DIRS} + ${STB_INCLUDE_DIRS} +) target_link_libraries(${LIB_NAME} PRIVATE game_shared_lib @@ -34,27 +47,38 @@ target_link_libraries(${LIB_NAME} Boost::program_options Boost::serialization nlohmann_json::nlohmann_json + glm + glfw + libglew_static + assimp ) -target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") -target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static) add_executable(${TARGET_NAME} main.cpp) - target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_include_directories(${TARGET_NAME} + PRIVATE + ${BOOST_LIBRARY_INCLUDES} + ${OPENGL_INCLUDE_DIRS} + glfw + glm + ${imgui-directory} + ${GLEW_INCLUDE_DIRS} + ${ASSIMP_INCLUDE_DIRS} + ${STB_INCLUDE_DIRS} +) target_link_libraries(${TARGET_NAME} PRIVATE game_shared_lib ${LIB_NAME}) -target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static) - -target_include_directories(${TARGET_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) -target_link_libraries(${TARGET_NAME} - PRIVATE +target_link_libraries(${TARGET_NAME} + PRIVATE Boost::asio Boost::filesystem Boost::thread Boost::program_options Boost::serialization nlohmann_json::nlohmann_json + glm + glfw + libglew_static + assimp ) - add_subdirectory(tests) # define client unit tests diff --git a/src/client/tests/CMakeLists.txt b/src/client/tests/CMakeLists.txt index fbe4eb69..88ba2017 100644 --- a/src/client/tests/CMakeLists.txt +++ b/src/client/tests/CMakeLists.txt @@ -12,6 +12,17 @@ target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) target_link_libraries(${TARGET_NAME} PUBLIC gtest_main) add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) target_include_directories(${TARGET_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) +target_include_directories(${TARGET_NAME} + PRIVATE + ${OPENGL_INCLUDE_DIRS} + glfw + glm + ${imgui-directory} + ${GLEW_INCLUDE_DIRS} + ${ASSIMP_INCLUDE_DIRS} + ${STB_INCLUDE_DIRS} +) + target_link_libraries(${TARGET_NAME} PRIVATE Boost::asio @@ -20,9 +31,10 @@ target_link_libraries(${TARGET_NAME} Boost::program_options Boost::serialization nlohmann_json::nlohmann_json + glm + glfw + libglew_static ) -target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") -target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static) # setup make target set(RUN_TESTS_TARGET "run_${TARGET_NAME}") From 470f26f6b1b79570abceefedd1a8b7acb64fccdc Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 27 Apr 2024 17:37:04 -0700 Subject: [PATCH 08/92] model loading using assimp library to load 3d models --- include/client/client.hpp | 11 +- include/client/model.hpp | 614 +++++++++++++++++++++++++++++++++++++ include/client/shader.hpp | 27 ++ include/client/shaders.hpp | 5 - include/client/util.hpp | 17 - src/client/client.cpp | 54 ++-- src/client/model.cpp | 273 +++++++++++++++++ src/client/shader.cpp | 110 +++++++ src/client/shaders.cpp | 19 -- src/client/util.cpp | 77 ----- 10 files changed, 1066 insertions(+), 141 deletions(-) create mode 100644 include/client/model.hpp create mode 100644 include/client/shader.hpp delete mode 100644 include/client/shaders.hpp create mode 100644 src/client/model.cpp create mode 100644 src/client/shader.cpp delete mode 100644 src/client/shaders.cpp diff --git a/include/client/client.hpp b/include/client/client.hpp index 8582c57b..12e67537 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -9,8 +9,11 @@ #include #include +#include #include "client/cube.hpp" +#include "client/shader.hpp" +#include "client/model.hpp" #include "client/util.hpp" #include "client/lobbyfinder.hpp" @@ -51,8 +54,10 @@ class Client { SharedGameState gameState; - GLuint cubeShaderProgram; - float cubeMovementDelta = 0.05f; + std::shared_ptr cubeShader; + + std::unique_ptr playerModel; + float playerMovementDelta = 0.05f; GLFWwindow *window; @@ -69,5 +74,7 @@ class Client { /// @brief Generate endpoints the client can connect to basic_resolver_results endpoints; std::shared_ptr session; + + boost::filesystem::path root_path; }; diff --git a/include/client/model.hpp b/include/client/model.hpp new file mode 100644 index 00000000..d178abad --- /dev/null +++ b/include/client/model.hpp @@ -0,0 +1,614 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "assimp/material.h" +#include "client/shader.hpp" + +struct Vertex { + glm::vec3 position; + glm::vec3 normal; + glm::vec2 textureCoords; +}; + +class Texture { + public: + Texture(const std::string& filepath, const aiTextureType& type); + unsigned int getID() const; + std::string getType() const; + private: + unsigned int ID; + std::string type; +}; + +class Mesh { + public: + std::vector vertices; + // std::vector indices; + // std::vector textures; + + std::vector positions; + std::vector normals; + std::vector indices; + std::vector textures; + + Mesh(std::vector vertices, std::vector indices, std::vector textures); + void Draw(std::shared_ptr shader, glm::mat4 modelView) const; + private: + // render data opengl needs + GLuint VAO, VBO, EBO; + // GLuint VAO, VBO_positions, VBO_normals, EBO; +}; + + +class Model { + public: + /** + * Loads Model from a given filename. Can be of format + * .obj, .blend or any of the formats that assimp supports + * @see https://assimp-docs.readthedocs.io/en/latest/about/introduction.html?highlight=obj#introduction + * + * @param Filepath to model file. + */ + Model(const std::string& filepath); + + /** + * Draws all the meshes of a given model + * + * @param Shader to use while drawing all the + * meshes of the model + */ + void Draw(std::shared_ptr shader); + + void Update(const glm::vec3& new_pos); + void Scale(const float& new_factor); + private: + // model data + std::vector meshes; + + void processNode(aiNode *node, const aiScene *scene); + Mesh processMesh(aiMesh *mesh, const aiScene *scene); + std::vector loadMaterialTextures(aiMaterial *mat, const aiTextureType& type); + + glm::mat4 modelView; +}; + +// #ifndef MODEL_H +// #define MODEL_H +// +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// +// +// #include +// #include +// #include +// #include +// #include +// #include +// using namespace std; +// +// +// #include +// +// #include +// #include +// #include +// #include +// +// class Shader +// { +// public: +// unsigned int ID; +// // constructor generates the shader on the fly +// // ------------------------------------------------------------------------ +// Shader() = default; +// Shader(const char* vertexPath, const char* fragmentPath, const char* geometryPath = nullptr) +// { +// // 1. retrieve the vertex/fragment source code from filePath +// std::string vertexCode; +// std::string fragmentCode; +// std::string geometryCode; +// std::ifstream vShaderFile; +// std::ifstream fShaderFile; +// std::ifstream gShaderFile; +// // ensure ifstream objects can throw exceptions: +// vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); +// fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); +// gShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); +// try +// { +// // open files +// vShaderFile.open(vertexPath); +// fShaderFile.open(fragmentPath); +// std::stringstream vShaderStream, fShaderStream; +// // read file's buffer contents into streams +// vShaderStream << vShaderFile.rdbuf(); +// fShaderStream << fShaderFile.rdbuf(); +// // close file handlers +// vShaderFile.close(); +// fShaderFile.close(); +// // convert stream into string +// vertexCode = vShaderStream.str(); +// fragmentCode = fShaderStream.str(); +// // if geometry shader path is present, also load a geometry shader +// if(geometryPath != nullptr) +// { +// gShaderFile.open(geometryPath); +// std::stringstream gShaderStream; +// gShaderStream << gShaderFile.rdbuf(); +// gShaderFile.close(); +// geometryCode = gShaderStream.str(); +// } +// } +// catch (std::ifstream::failure& e) +// { +// std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl; +// } +// const char* vShaderCode = vertexCode.c_str(); +// const char * fShaderCode = fragmentCode.c_str(); +// // 2. compile shaders +// unsigned int vertex, fragment; +// // vertex shader +// vertex = glCreateShader(GL_VERTEX_SHADER); +// glShaderSource(vertex, 1, &vShaderCode, NULL); +// glCompileShader(vertex); +// checkCompileErrors(vertex, "VERTEX"); +// // fragment Shader +// fragment = glCreateShader(GL_FRAGMENT_SHADER); +// glShaderSource(fragment, 1, &fShaderCode, NULL); +// glCompileShader(fragment); +// checkCompileErrors(fragment, "FRAGMENT"); +// // if geometry shader is given, compile geometry shader +// unsigned int geometry; +// if(geometryPath != nullptr) +// { +// const char * gShaderCode = geometryCode.c_str(); +// geometry = glCreateShader(GL_GEOMETRY_SHADER); +// glShaderSource(geometry, 1, &gShaderCode, NULL); +// glCompileShader(geometry); +// checkCompileErrors(geometry, "GEOMETRY"); +// } +// // shader Program +// ID = glCreateProgram(); +// glAttachShader(ID, vertex); +// glAttachShader(ID, fragment); +// if(geometryPath != nullptr) +// glAttachShader(ID, geometry); +// glLinkProgram(ID); +// checkCompileErrors(ID, "PROGRAM"); +// // delete the shaders as they're linked into our program now and no longer necessary +// glDeleteShader(vertex); +// glDeleteShader(fragment); +// if(geometryPath != nullptr) +// glDeleteShader(geometry); +// +// } +// // activate the shader +// // ------------------------------------------------------------------------ +// void use() +// { +// glUseProgram(ID); +// } +// // utility uniform functions +// // ------------------------------------------------------------------------ +// void setBool(const std::string &name, bool value) const +// { +// glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); +// } +// // ------------------------------------------------------------------------ +// void setInt(const std::string &name, int value) const +// { +// glUniform1i(glGetUniformLocation(ID, name.c_str()), value); +// } +// // ------------------------------------------------------------------------ +// void setFloat(const std::string &name, float value) const +// { +// glUniform1f(glGetUniformLocation(ID, name.c_str()), value); +// } +// // ------------------------------------------------------------------------ +// void setVec2(const std::string &name, const glm::vec2 &value) const +// { +// glUniform2fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); +// } +// void setVec2(const std::string &name, float x, float y) const +// { +// glUniform2f(glGetUniformLocation(ID, name.c_str()), x, y); +// } +// // ------------------------------------------------------------------------ +// void setVec3(const std::string &name, const glm::vec3 &value) const +// { +// glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); +// } +// void setVec3(const std::string &name, float x, float y, float z) const +// { +// glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z); +// } +// // ------------------------------------------------------------------------ +// void setVec4(const std::string &name, const glm::vec4 &value) const +// { +// glUniform4fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); +// } +// void setVec4(const std::string &name, float x, float y, float z, float w) +// { +// glUniform4f(glGetUniformLocation(ID, name.c_str()), x, y, z, w); +// } +// // ------------------------------------------------------------------------ +// void setMat2(const std::string &name, const glm::mat2 &mat) const +// { +// glUniformMatrix2fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); +// } +// // ------------------------------------------------------------------------ +// void setMat3(const std::string &name, const glm::mat3 &mat) const +// { +// glUniformMatrix3fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); +// } +// // ------------------------------------------------------------------------ +// void setMat4(const std::string &name, const glm::mat4 &mat) const +// { +// glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); +// } +// +// private: +// // utility function for checking shader compilation/linking errors. +// // ------------------------------------------------------------------------ +// void checkCompileErrors(GLuint shader, std::string type) +// { +// GLint success; +// GLchar infoLog[1024]; +// if(type != "PROGRAM") +// { +// glGetShaderiv(shader, GL_COMPILE_STATUS, &success); +// if(!success) +// { +// glGetShaderInfoLog(shader, 1024, NULL, infoLog); +// std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; +// } +// } +// else +// { +// glGetProgramiv(shader, GL_LINK_STATUS, &success); +// if(!success) +// { +// glGetProgramInfoLog(shader, 1024, NULL, infoLog); +// std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; +// } +// } +// } +// }; +// +// #include +// #include +// +// +// #include +// #include +// using namespace std; +// +// #define MAX_BONE_INFLUENCE 4 +// +// struct Vertex { +// // position +// glm::vec3 Position; +// // normal +// glm::vec3 Normal; +// // texCoords +// glm::vec2 TexCoords; +// // tangent +// glm::vec3 Tangent; +// // bitangent +// glm::vec3 Bitangent; +// //bone indexes which will influence this vertex +// int m_BoneIDs[MAX_BONE_INFLUENCE]; +// //weights from each bone +// float m_Weights[MAX_BONE_INFLUENCE]; +// }; +// +// struct Texture { +// unsigned int id; +// string type; +// string path; +// }; +// +// class Mesh { +// public: +// // mesh Data +// vector vertices; +// vector indices; +// vector textures; +// unsigned int VAO; +// +// // constructor +// Mesh(vector vertices, vector indices, vector textures) +// { +// this->vertices = vertices; +// this->indices = indices; +// this->textures = textures; +// +// // now that we have all the required data, set the vertex buffers and its attribute pointers. +// setupMesh(); +// } +// +// // render the mesh +// void Draw(Shader &shader) +// { +// // bind appropriate textures +// unsigned int diffuseNr = 1; +// unsigned int specularNr = 1; +// unsigned int normalNr = 1; +// unsigned int heightNr = 1; +// for(unsigned int i = 0; i < textures.size(); i++) +// { +// glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding +// // retrieve texture number (the N in diffuse_textureN) +// string number; +// string name = textures[i].type; +// if(name == "texture_diffuse") +// number = std::to_string(diffuseNr++); +// else if(name == "texture_specular") +// number = std::to_string(specularNr++); // transfer unsigned int to string +// else if(name == "texture_normal") +// number = std::to_string(normalNr++); // transfer unsigned int to string +// else if(name == "texture_height") +// number = std::to_string(heightNr++); // transfer unsigned int to string +// +// // now set the sampler to the correct texture unit +// glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i); +// // and finally bind the texture +// glBindTexture(GL_TEXTURE_2D, textures[i].id); +// } +// +// // draw mesh +// glBindVertexArray(VAO); +// glDrawElements(GL_TRIANGLES, static_cast(indices.size()), GL_UNSIGNED_INT, 0); +// glBindVertexArray(0); +// +// // always good practice to set everything back to defaults once configured. +// glActiveTexture(GL_TEXTURE0); +// } +// +// private: +// // render data +// unsigned int VBO, EBO; +// +// // initializes all the buffer objects/arrays +// void setupMesh() +// { +// // create buffers/arrays +// glGenVertexArrays(1, &VAO); +// glGenBuffers(1, &VBO); +// glGenBuffers(1, &EBO); +// +// glBindVertexArray(VAO); +// // load data into vertex buffers +// glBindBuffer(GL_ARRAY_BUFFER, VBO); +// // A great thing about structs is that their memory layout is sequential for all its items. +// // The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which +// // again translates to 3/2 floats which translates to a byte array. +// glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW); +// +// glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); +// glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); +// +// // set the vertex attribute pointers +// // vertex Positions +// glEnableVertexAttribArray(0); +// glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); +// // vertex normals +// glEnableVertexAttribArray(1); +// glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal)); +// // vertex texture coords +// glEnableVertexAttribArray(2); +// glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords)); +// // vertex tangent +// glEnableVertexAttribArray(3); +// glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent)); +// // vertex bitangent +// glEnableVertexAttribArray(4); +// glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent)); +// // ids +// glEnableVertexAttribArray(5); +// glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs)); +// +// // weights +// glEnableVertexAttribArray(6); +// glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights)); +// glBindVertexArray(0); +// } +// }; +// +// class Model +// { +// public: +// // model data +// vector textures_loaded; // stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once. +// vector meshes; +// string directory; +// bool gammaCorrection; +// +// // constructor, expects a filepath to a 3D model. +// Model(string const &path, bool gamma = false) : gammaCorrection(gamma) +// { +// loadModel(path); +// } +// +// // draws the model, and thus all its meshes +// void Draw(Shader &shader) +// { +// for(unsigned int i = 0; i < meshes.size(); i++) +// meshes[i].Draw(shader); +// } +// +// private: +// // loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector. +// void loadModel(string const &path) +// { +// // read file via ASSIMP +// Assimp::Importer importer; +// const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace); +// // check for errors +// if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero +// { +// cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl; +// return; +// } +// // retrieve the directory path of the filepath +// directory = path.substr(0, path.find_last_of('/')); +// +// // process ASSIMP's root node recursively +// processNode(scene->mRootNode, scene); +// } +// +// // processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any). +// void processNode(aiNode *node, const aiScene *scene) +// { +// // process each mesh located at the current node +// for(unsigned int i = 0; i < node->mNumMeshes; i++) +// { +// // the node object only contains indices to index the actual objects in the scene. +// // the scene contains all the data, node is just to keep stuff organized (like relations between nodes). +// aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; +// meshes.push_back(processMesh(mesh, scene)); +// } +// // after we've processed all of the meshes (if any) we then recursively process each of the children nodes +// for(unsigned int i = 0; i < node->mNumChildren; i++) +// { +// processNode(node->mChildren[i], scene); +// } +// +// } +// +// Mesh processMesh(aiMesh *mesh, const aiScene *scene) +// { +// // data to fill +// vector vertices; +// vector indices; +// vector textures; +// +// // walk through each of the mesh's vertices +// for(unsigned int i = 0; i < mesh->mNumVertices; i++) +// { +// Vertex vertex; +// glm::vec3 vector; // we declare a placeholder vector since assimp uses its own vector class that doesn't directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first. +// // positions +// vector.x = mesh->mVertices[i].x; +// vector.y = mesh->mVertices[i].y; +// vector.z = mesh->mVertices[i].z; +// vertex.Position = vector; +// // normals +// if (mesh->HasNormals()) +// { +// vector.x = mesh->mNormals[i].x; +// vector.y = mesh->mNormals[i].y; +// vector.z = mesh->mNormals[i].z; +// vertex.Normal = vector; +// } +// // texture coordinates +// if(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates? +// { +// glm::vec2 vec; +// // a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't +// // use models where a vertex can have multiple texture coordinates so we always take the first set (0). +// vec.x = mesh->mTextureCoords[0][i].x; +// vec.y = mesh->mTextureCoords[0][i].y; +// vertex.TexCoords = vec; +// // tangent +// vector.x = mesh->mTangents[i].x; +// vector.y = mesh->mTangents[i].y; +// vector.z = mesh->mTangents[i].z; +// vertex.Tangent = vector; +// // bitangent +// vector.x = mesh->mBitangents[i].x; +// vector.y = mesh->mBitangents[i].y; +// vector.z = mesh->mBitangents[i].z; +// vertex.Bitangent = vector; +// } +// else +// vertex.TexCoords = glm::vec2(0.0f, 0.0f); +// +// vertices.push_back(vertex); +// } +// // now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices. +// for(unsigned int i = 0; i < mesh->mNumFaces; i++) +// { +// aiFace face = mesh->mFaces[i]; +// // retrieve all indices of the face and store them in the indices vector +// for(unsigned int j = 0; j < face.mNumIndices; j++) +// indices.push_back(face.mIndices[j]); +// } +// // process materials +// aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; +// // we assume a convention for sampler names in the shaders. Each diffuse texture should be named +// // as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER. +// // Same applies to other texture as the following list summarizes: +// // diffuse: texture_diffuseN +// // specular: texture_specularN +// // normal: texture_normalN +// +// // 1. diffuse maps +// vector diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); +// textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); +// // 2. specular maps +// vector specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular"); +// textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); +// // 3. normal maps +// std::vector normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal"); +// textures.insert(textures.end(), normalMaps.begin(), normalMaps.end()); +// // 4. height maps +// std::vector heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height"); +// textures.insert(textures.end(), heightMaps.begin(), heightMaps.end()); +// +// // return a mesh object created from the extracted mesh data +// return Mesh(vertices, indices, textures); +// } +// +// // checks all material textures of a given type and loads the textures if they're not loaded yet. +// // the required info is returned as a Texture struct. +// vector loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) +// { +// vector textures; +// for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) +// { +// aiString str; +// mat->GetTexture(type, i, &str); +// // check if texture was loaded before and if so, continue to next iteration: skip loading a new texture +// bool skip = false; +// for(unsigned int j = 0; j < textures_loaded.size(); j++) +// { +// if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0) +// { +// textures.push_back(textures_loaded[j]); +// skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization) +// break; +// } +// } +// if(!skip) +// { // if texture hasn't been loaded already, load it +// Texture texture; +// // texture.id = TextureFromFile(str.C_Str(), this->directory); +// texture.type = typeName; +// texture.path = str.C_Str(); +// textures.push_back(texture); +// textures_loaded.push_back(texture); // store it as texture loaded for entire model, to ensure we won't unnecessary load duplicate textures. +// } +// } +// return textures; +// } +// }; +// +// +// #endif diff --git a/include/client/shader.hpp b/include/client/shader.hpp new file mode 100644 index 00000000..e5a44e6c --- /dev/null +++ b/include/client/shader.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include +#include +#include + + +class Shader { + public: + // constructor reads and builds the shader + Shader(const std::string& vertexPath, const std::string& fragmentPath); + ~Shader(); + + unsigned int getID(); + // use/activate the shader + void use(); + // utility uniform functions + void setBool(const std::string &name, bool value) const; + void setInt(const std::string &name, int value) const; + void setFloat(const std::string &name, float value) const; + private: + // the shader program ID + unsigned int ID; +}; diff --git a/include/client/shaders.hpp b/include/client/shaders.hpp deleted file mode 100644 index 2c5d1d46..00000000 --- a/include/client/shaders.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - - - -GLuint loadCubeShaders(); diff --git a/include/client/util.hpp b/include/client/util.hpp index e9c4ffaa..3f59c932 100644 --- a/include/client/util.hpp +++ b/include/client/util.hpp @@ -1,19 +1,2 @@ #pragma once -#include - -#include -#include -#include -#include -#include - -// #include - -// #include -// #include - -#include "client/core.hpp" - - -GLuint LoadShaders(const std::string& vertex_file_path, const std::string& fragment_file_path); diff --git a/src/client/client.cpp b/src/client/client.cpp index 4362cdc7..7e00b4ec 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1,12 +1,16 @@ #include "client/client.hpp" + +#include +#include + #include #include #include #include -#include -#include +#include -#include "client/shaders.hpp" +#include "client/shader.hpp" +#include "client/model.hpp" #include "shared/game/event.hpp" #include "shared/network/constants.hpp" #include "shared/network/packet.hpp" @@ -25,8 +29,8 @@ Client::Client(boost::asio::io_context& io_context, GameConfig config): resolver(io_context), socket(io_context), config(config), - gameState(GamePhase::TITLE_SCREEN, config) -{ + gameState(GamePhase::TITLE_SCREEN, config) { + this->root_path = boost::dll::program_location().parent_path().parent_path().parent_path(); } void Client::connectAndListen(std::string ip_addr) { @@ -57,7 +61,7 @@ bool Client::init() { /* Create a windowed mode window and its OpenGL context */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL); + window = glfwCreateWindow(640, 480, "Arcana", NULL, NULL); if (!window) { glfwTerminate(); return false; @@ -75,17 +79,27 @@ bool Client::init() { std::cout << "shader version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl; std::cout << "shader version: " << glGetString(GL_VERSION) << std::endl; - this->cubeShaderProgram = loadCubeShaders(); - if (!this->cubeShaderProgram) { - std::cout << "Failed to load cube shader files" << std::endl; + + boost::filesystem::path vertFilepath = this->root_path / "src/client/shaders/shader.vert"; + boost::filesystem::path fragFilepath = this->root_path / "src/client/shaders/shader.frag"; + this->cubeShader = std::make_shared(vertFilepath.c_str(), fragFilepath.c_str()); + if (!this->cubeShader) { + std::cout << "Could not load cube shader" << std::endl; return false; } + boost::filesystem::path playerModelFilepath = this->root_path / "src/client/models/bear-sp22.obj"; + this->playerModel = std::make_unique(playerModelFilepath.string()); + if (!this->playerModel) { + std::cout << "Could not load player model" << std::endl; + return false; + } + this->playerModel->Scale(0.25); + return true; } bool Client::cleanup() { - glDeleteProgram(this->cubeShaderProgram); return true; } @@ -108,13 +122,13 @@ void Client::idleCallback(boost::asio::io_context& context) { std::optional movement = glm::vec3(0.0f); if(is_held_right) - movement.value() += glm::vec3(cubeMovementDelta, 0.0f, 0.0f); + movement.value() += glm::vec3(playerMovementDelta, 0.0f, 0.0f); if(is_held_left) - movement.value() += glm::vec3(-cubeMovementDelta, 0.0f, 0.0f); + movement.value() += glm::vec3(-playerMovementDelta, 0.0f, 0.0f); if(is_held_up) - movement.value() += glm::vec3(0.0f, cubeMovementDelta, 0.0f); + movement.value() += glm::vec3(0.0f, playerMovementDelta, 0.0f); if(is_held_down) - movement.value() += glm::vec3(0.0f, -cubeMovementDelta, 0.0f); + movement.value() += glm::vec3(0.0f, -playerMovementDelta, 0.0f); if (movement.has_value()) { auto eid = 0; @@ -143,14 +157,12 @@ void Client::draw() { for (int i = 0; i < this->gameState.objects.size(); i++) { std::shared_ptr sharedObject = this->gameState.objects.at(i); - if (sharedObject == nullptr) + if (sharedObject == nullptr) { continue; - - std::cout << "got an object" << std::endl; - // tmp: all objects are cubes - Cube* cube = new Cube(); - cube->update(sharedObject->physics.position); - cube->draw(this->cubeShaderProgram); + } + // all objects are players for now + this->playerModel->Update(sharedObject->physics.position); + this->playerModel->Draw(this->cubeShader); } } diff --git a/src/client/model.cpp b/src/client/model.cpp new file mode 100644 index 00000000..dcee5092 --- /dev/null +++ b/src/client/model.cpp @@ -0,0 +1,273 @@ +#include "client/model.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "assimp/material.h" +#include "glm/ext/matrix_transform.hpp" +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +#define GLM_ENABLE_EXPERIMENTAL +#include "glm/ext/matrix_clip_space.hpp" +#include "glm/fwd.hpp" +#include +#include +#include +#include + + +Mesh::Mesh(std::vector vertices, std::vector indices, std::vector textures) : + vertices(vertices), indices(indices), textures(textures) { + + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); + + // vertex positions + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); + + // vertex normals + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal)); + + // vertex texture coords + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, textureCoords)); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + std::cout << "Loaded mesh with " << vertices.size() << " vertices, and " << textures.size() << " textures" << std::endl; +} + +void Mesh::Draw(std::shared_ptr shader, glm::mat4 modelView) const { + // actiavte the shader program + shader->use(); + + // Currently 'hardcoding' camera logic in + float FOV = 45.0f; + float Aspect = 1.33f; + float NearClip = 0.1f; + float FarClip = 100.0f; + + float Distance = 10.0f; + float Azimuth = 0.0f; + float Incline = 20.0f; + + glm::mat4 world(1); + world[3][2] = Distance; + world = glm::eulerAngleY(glm::radians(-Azimuth)) * glm::eulerAngleX(glm::radians(-Incline)) * world; + + // Compute view matrix (inverse of world matrix) + glm::mat4 view = glm::inverse(world); + + // Compute perspective projection matrix + glm::mat4 project = glm::perspective(glm::radians(FOV), Aspect, NearClip, FarClip); + + // Compute final view-projection matrix + glm::mat4 viewProjMtx = project * view; + + auto color = glm::vec3(0.0f, 1.0f, 1.0f); + + // get the locations and send the uniforms to the shader + glUniformMatrix4fv(glGetUniformLocation(shader->getID(), "viewProj"), 1, false, reinterpret_cast(&viewProjMtx)); + glUniformMatrix4fv(glGetUniformLocation(shader->getID(), "model"), 1, GL_FALSE, reinterpret_cast(&modelView)); + glUniform3fv(glGetUniformLocation(shader->getID(), "DiffuseColor"), 1, &color[0]); + + unsigned int diffuseNr = 1; + unsigned int specularNr = 1; + for(unsigned int i = 0; i < textures.size(); i++) { + glActiveTexture(GL_TEXTURE0 + i); // activate proper texture unit before binding + // retrieve texture number (the N in diffuse_textureN) + std::string number; + std::string name = textures[i].getType(); + if(name == "texture_diffuse") + number = std::to_string(diffuseNr++); + else if(name == "texture_specular") + number = std::to_string(specularNr++); + + shader->setInt(("material." + name + number).c_str(), i); + glBindTexture(GL_TEXTURE_2D, textures[i].getID()); + } + glActiveTexture(GL_TEXTURE0); + + // draw mesh + glBindVertexArray(VAO); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + glUseProgram(0); +} + +Model::Model(const std::string& filepath) : + modelView(1.0f) { + + Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(filepath, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_SplitLargeMeshes | aiProcess_OptimizeMeshes); + if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { + throw std::invalid_argument(std::string("ERROR::ASSIMP::") + importer.GetErrorString()); + } + + processNode(scene->mRootNode, scene); +} + +void Model::Draw(std::shared_ptr shader) { + for(const Mesh& mesh : this->meshes) + mesh.Draw(shader, this->modelView); +} + +void Model::Update(const glm::vec3 &new_pos) { + modelView[3] = glm::vec4(new_pos, 1.0f); +} + +void Model::Scale(const float& new_factor) { + glm::vec3 scaleVector(new_factor, new_factor, new_factor); + this->modelView = glm::scale(this->modelView, scaleVector); +} + +void Model::processNode(aiNode *node, const aiScene *scene) { + // process all the node's meshes (if any) + for(unsigned int i = 0; i < node->mNumMeshes; i++) { + aiMesh *mesh = scene->mMeshes[node->mMeshes[i]]; + meshes.push_back(processMesh(mesh, scene)); + } + // then do the same for each of its children + for(unsigned int i = 0; i < node->mNumChildren; i++) { + processNode(node->mChildren[i], scene); + } +} + +Mesh Model::processMesh(aiMesh *mesh, const aiScene *scene) { + std::vector vertices; + std::vector indices; + std::vector textures; + + // process vertex positions, normals and texture coordinates + for(unsigned int i = 0; i < mesh->mNumVertices; i++) { + glm::vec3 position( + mesh->mVertices[i].x, + mesh->mVertices[i].y, + mesh->mVertices[i].z); + glm::vec3 normal( + mesh->mNormals[i].x, + mesh->mNormals[i].y, + mesh->mNormals[i].z); + + // check if the mesh contain texture coordinates + glm::vec2 texture(0.0f, 0.0f); + if(mesh->mTextureCoords[0]) { + texture.x = mesh->mTextureCoords[0][i].x; + texture.y = mesh->mTextureCoords[0][i].y; + } + + vertices.push_back(Vertex{ + position, + normal, + texture + }); + } + // process indices + for(unsigned int i = 0; i < mesh->mNumFaces; i++) { + aiFace face = mesh->mFaces[i]; + for(unsigned int j = 0; j < face.mNumIndices; j++) + indices.push_back(face.mIndices[j]); + } + + // process material + if(mesh->mMaterialIndex >= 0) { + aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; + std::vector diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE); + textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); + std::vector specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR); + textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); + } + + return Mesh(vertices, indices, textures); +} + + +std::vector Model::loadMaterialTextures(aiMaterial* mat, const aiTextureType& type) { + std::vector textures; + for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { + aiString str; + mat->GetTexture(type, i, &str); + Texture texture(std::string(str.C_Str()), type); + textures.push_back(texture); + } + return textures; +} + + +Texture::Texture(const std::string& filepath, const aiTextureType& type) { + switch (type) { + case aiTextureType_DIFFUSE: + this->type = "texture_diffuse"; + case aiTextureType_SPECULAR: + this->type = "texture_specular"; + default: + throw std::invalid_argument(std::string("Unimplemented texture type ") + aiTextureTypeToString(type)); + } + + unsigned int textureID; + glGenTextures(1, &textureID); + + int width, height, nrComponents; + std::cout << "attempting to load texture at " << filepath << std::endl; + unsigned char *data = stbi_load(filepath.c_str(), &width, &height, &nrComponents, 0); + if (!data) { + std::cout << "Texture failed to load at path: " << filepath << std::endl; + stbi_image_free(data); + } + GLenum format; + if (nrComponents == 1) + format = GL_RED; + else if (nrComponents == 3) + format = GL_RGB; + else if (nrComponents == 4) + format = GL_RGBA; + + glBindTexture(GL_TEXTURE_2D, textureID); + glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); + glGenerateMipmap(GL_TEXTURE_2D); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + stbi_image_free(data); + + this->ID = textureID; +} + +unsigned int Texture::getID() const { + return ID; +} + +std::string Texture::getType() const { + return type; +} + + diff --git a/src/client/shader.cpp b/src/client/shader.cpp new file mode 100644 index 00000000..2cca847e --- /dev/null +++ b/src/client/shader.cpp @@ -0,0 +1,110 @@ +#include +#include + +#include +#include +#include + +#include "client/shader.hpp" + +Shader::Shader(const std::string& vertexPath, const std::string& fragmentPath) { + std::string vertexCode; + std::string fragmentCode; + std::ifstream vShaderFile; + std::ifstream fShaderFile; + // ensure ifstream objects can throw exceptions: + vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); + fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); + try { + // open files + vShaderFile.open(vertexPath); + fShaderFile.open(fragmentPath); + std::stringstream vShaderStream, fShaderStream; + // read file's buffer contents into streams + vShaderStream << vShaderFile.rdbuf(); + fShaderStream << fShaderFile.rdbuf(); + // close file handlers + vShaderFile.close(); + fShaderFile.close(); + // convert stream into string + vertexCode = vShaderStream.str(); + fragmentCode = fShaderStream.str(); + } catch(std::ifstream::failure e) { + std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; + } + const char* vShaderCode = vertexCode.c_str(); + const char* fShaderCode = fragmentCode.c_str(); + + // 2. compile shaders + unsigned int vertex, fragment; + int success; + char infoLog[512]; + + // vertex Shader + vertex = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertex, 1, &vShaderCode, NULL); + glCompileShader(vertex); + // print compile errors if any + glGetShaderiv(vertex, GL_COMPILE_STATUS, &success); + if(!success) { + glGetShaderInfoLog(vertex, 512, NULL, infoLog); + std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; + }; + + // fragment shader + fragment = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragment, 1, &fShaderCode, NULL); + glCompileShader(fragment); + // print compile errors if any + glGetShaderiv(fragment, GL_COMPILE_STATUS, &success); + if(!success) { + glGetShaderInfoLog(vertex, 512, NULL, infoLog); + std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; + }; + + if (vertex == 0 && fragment == 0) { + throw new std::invalid_argument("both shaders failed to init"); + } + + // shader Program + ID = glCreateProgram(); + glAttachShader(ID, vertex); + glAttachShader(ID, fragment); + glLinkProgram(ID); + // print linking errors if any + glGetProgramiv(ID, GL_LINK_STATUS, &success); + if(!success) { + glGetProgramInfoLog(ID, 512, NULL, infoLog); + std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; + } + + // delete the shaders as they're linked into our program now and no longer necessary + glDetachShader(ID, vertex); + glDetachShader(ID, fragment); + glDeleteShader(vertex); + glDeleteShader(fragment); +} + +Shader::~Shader() { + glDeleteProgram(this->ID); +} + +void Shader::use() { + glUseProgram(ID); +} + +void Shader::setBool(const std::string &name, bool value) const { + glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); +} + +void Shader::setInt(const std::string &name, int value) const { + glUniform1i(glGetUniformLocation(ID, name.c_str()), value); +} + +void Shader::setFloat(const std::string &name, float value) const { + glUniform1f(glGetUniformLocation(ID, name.c_str()), value); +} + +unsigned int Shader::getID() { + return ID; +} diff --git a/src/client/shaders.cpp b/src/client/shaders.cpp deleted file mode 100644 index 2a7ea5f0..00000000 --- a/src/client/shaders.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include -#include - -#include - -#include "client/util.hpp" - -GLuint loadCubeShaders() { - boost::filesystem::path root_path = boost::dll::program_location().parent_path().parent_path().parent_path(); - boost::filesystem::path vertFilepath = root_path / "src/client/shaders/shader.vert"; - boost::filesystem::path fragFilepath = root_path / "src/client/shaders/shader.frag"; - - GLuint shaderProgram = LoadShaders(vertFilepath.string(), fragFilepath.string()); - // Check the shader program exists and is non-zero - if (!shaderProgram) { - return 0; - } - return shaderProgram; -} diff --git a/src/client/util.cpp b/src/client/util.cpp index b5357dd3..4abfa034 100644 --- a/src/client/util.cpp +++ b/src/client/util.cpp @@ -1,79 +1,2 @@ #include "client/util.hpp" -enum ShaderType { - vertex, - fragment -}; - -GLuint LoadSingleShader(const std::string& shaderFilePath, ShaderType type) { - // Create a shader id. - GLuint shaderID = 0; - - if (type == vertex) - shaderID = glCreateShader(GL_VERTEX_SHADER); - else if (type == fragment) - shaderID = glCreateShader(GL_FRAGMENT_SHADER); - - // Try to read shader codes from the shader file. - std::string shaderCode; - std::ifstream shaderStream(shaderFilePath, std::ios::in); - if (shaderStream.is_open()) { - std::string Line = ""; - while (getline(shaderStream, Line)) - shaderCode += "\n" + Line; - shaderStream.close(); - } else { - std::cerr << "Impossible to open " << shaderFilePath << ". " - << "Check to make sure the file exists and you passed in the " - << "right filepath!" - << std::endl; - return 0; - } - - GLint Result = GL_FALSE; - - // Compile Shader. - std::cerr << "Compiling shader: " << shaderFilePath << std::endl; - char const* sourcePointer = shaderCode.c_str(); - glShaderSource(shaderID, 1, &sourcePointer, NULL); - glCompileShader(shaderID); - - // Check Shader. - glGetShaderiv(shaderID, GL_COMPILE_STATUS, &Result); - if (type == vertex) - printf("Successfully compiled vertex shader!\n"); - else if (type == fragment) - printf("Successfully compiled fragment shader!\n"); - - return shaderID; -} - -GLuint LoadShaders(const std::string& vertexFilePath, const std::string& fragmentFilePath) { - // Create the vertex shader and fragment shader. - GLuint vertexShaderID = LoadSingleShader(vertexFilePath, vertex); - GLuint fragmentShaderID = LoadSingleShader(fragmentFilePath, fragment); - - // Check both shaders. - if (vertexShaderID == 0 || fragmentShaderID == 0) return 0; - - GLint Result = GL_FALSE; - - // Link the program. - printf("Linking program\n"); - GLuint programID = glCreateProgram(); - glAttachShader(programID, vertexShaderID); - glAttachShader(programID, fragmentShaderID); - glLinkProgram(programID); - - // Check the program. - glGetProgramiv(programID, GL_LINK_STATUS, &Result); - printf("Successfully linked program!\n"); - - // Detach and delete the shaders as they are no longer needed. - glDetachShader(programID, vertexShaderID); - glDetachShader(programID, fragmentShaderID); - glDeleteShader(vertexShaderID); - glDeleteShader(fragmentShaderID); - - return programID; -} From 744c7aa9f7cecf6901d2a4a23499b06bfe9cfbfe Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 27 Apr 2024 18:05:12 -0700 Subject: [PATCH 09/92] make target to automate model downloads --- CMakeLists.txt | 9 +++++++++ README.md | 4 ++++ shell.nix | 2 ++ src/client/models/.gitignore | 2 ++ 4 files changed, 17 insertions(+) create mode 100644 src/client/models/.gitignore diff --git a/CMakeLists.txt b/CMakeLists.txt index 15a4e224..f31e259a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,3 +73,12 @@ add_custom_target(lint -i${CMAKE_SOURCE_DIR}/src/server/tests -i${CMAKE_SOURCE_DIR}/src/shared/tests ) + +add_custom_target(pull_models + COMMAND + gdown 133bVNM4_27hg_VoZGo9EUfCn7n6VXzOU -O ${CMAKE_SOURCE_DIR}/src/client/models/Player1-fire.obj && + gdown 1v3XO_E1ularO5Ku2WaA8O9GqE402a8cx -O ${CMAKE_SOURCE_DIR}/src/client/models/bear-sp22.obj && + gdown 1hHK-0iKMT6uboFUl3DXC9JcbnfsNCNF4 -O ${CMAKE_SOURCE_DIR}/src/client/models/cube.obj && + gdown 1mEWRgBP7G-s3XOr6NO9yichD4_eKc3JH -O ${CMAKE_SOURCE_DIR}/src/client/models/teapot.obj +) + diff --git a/README.md b/README.md index 6ceff0e2..f918e256 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,10 @@ Depending on where you need to link the library (client, server, shared), you wi - [C++ Intellisense](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) - [General Productivity](https://marketplace.visualstudio.com/items?itemName=jirkavrba.subway-surfers) +## Models + +You can download models from our Google Drive folder [here](https://drive.google.com/drive/folders/1N7a5cDgMcXbPO0RtgznnEo-1XUfdMScM?usp=sharing) and place them in `src/client/models`. Alternatively, you can install [gdown](https://github.com/wkentaro/gdown) and run `make pull_models` to automatically pull them. + ## Documentation View deployed documentation [here](https://cse125.ucsd.edu/2024/cse125g3/site/docs/html/) diff --git a/shell.nix b/shell.nix index f398b3e7..56922eb6 100644 --- a/shell.nix +++ b/shell.nix @@ -30,6 +30,8 @@ mkShell { doxygen clang-tools_14 cppcheck + + python310Packages.gdown ]; nativeBuildInputs = with pkgs; [ pkg-config diff --git a/src/client/models/.gitignore b/src/client/models/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/src/client/models/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From 87e5ddb3e01f77d197f21a558565e55f20278d8c Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 28 Apr 2024 10:46:11 -0700 Subject: [PATCH 10/92] successfully running font loading code --- include/client/client.hpp | 3 +++ include/client/gui/font/loader.hpp | 10 ++++++++-- include/client/gui/gui.hpp | 17 ++++++++++++++++- include/client/gui/window.hpp | 14 -------------- src/client/CMakeLists.txt | 6 ++++++ src/client/client.cpp | 21 ++++----------------- src/client/gui/font/font.cpp | 4 ++-- src/client/gui/font/loader.cpp | 18 ++++++++++-------- src/client/gui/gui.cpp | 18 ++++++++++++++++++ src/shared/CMakeLists.txt | 1 + 10 files changed, 68 insertions(+), 44 deletions(-) delete mode 100644 include/client/gui/window.hpp create mode 100644 src/client/gui/gui.cpp diff --git a/include/client/client.hpp b/include/client/client.hpp index d8575f67..42f4c73a 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -13,6 +13,7 @@ #include "client/cube.hpp" #include "client/util.hpp" #include "client/lobbyfinder.hpp" +#include "client/gui/gui.hpp" //#include "shared/game/gamestate.hpp" #include "shared/game/sharedgamestate.hpp" @@ -54,6 +55,8 @@ class Client { GLFWwindow *window; GLuint shaderProgram; + gui::GUI gui; + // Flags static bool is_held_up; static bool is_held_down; diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp index 0fc9ec42..0136a3f1 100644 --- a/include/client/gui/font/loader.hpp +++ b/include/client/gui/font/loader.hpp @@ -3,6 +3,10 @@ #include "client/core.hpp" #include "client/gui/font/font.hpp" +// freetype needs this extra include for whatever unholy reason +#include +#include FT_FREETYPE_H + #include namespace gui::font { @@ -18,7 +22,7 @@ struct Character { struct font_pair_hash { std::size_t operator()(const std::pair& p) const; -} +}; class Loader { public: @@ -27,7 +31,9 @@ class Loader { bool init(); private: - void _loadFont(Font font); + FT_Library ft; + + bool _loadFont(Font font); std::unordered_map< std::pair, diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index df19c21b..5fc9f9f7 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -6,4 +6,19 @@ #include "client/gui/widget/options.hpp" #include "client/gui/widget/type.hpp" #include "client/gui/widget/widget.hpp" -#include "client/gui/window.hpp" \ No newline at end of file +#include "client/gui/font/font.hpp" +#include "client/gui/font/loader.hpp" + +namespace gui { + +class GUI { +public: + GUI() = default; + + bool init(); + +private: + font::Loader fonts; +}; + +} \ No newline at end of file diff --git a/include/client/gui/window.hpp b/include/client/gui/window.hpp deleted file mode 100644 index 82df2ca2..00000000 --- a/include/client/gui/window.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "client/gui/gui.hpp" - -namespace gui { - -class Window { -public: - Window(); - - void render(); - -private: -}; - -} \ No newline at end of file diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 54ebceb9..9528f734 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -15,6 +15,12 @@ set(FILES cube.cpp util.cpp lobbyfinder.cpp + + gui/font/font.cpp + gui/font/loader.cpp + gui/widget/widget.cpp + gui/gui.cpp + ${IMGUI_SOURCES} ) diff --git a/src/client/client.cpp b/src/client/client.cpp index b44339fd..e0bac81d 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -86,24 +86,18 @@ int Client::init() { // Check the shader program. if (!shaderProgram) { std::cerr << "Failed to initialize shader program" << std::endl; - return false; + return -1; } - FT_Library ft; - if (FT_Init_FreeType(&ft)) { - std::cerr << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; - return false; + if (!this->gui.init()) { + std::cerr << "GUI failed to init" << std::endl; + return -1; } - return 0; } int Client::cleanup() { - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - glDeleteProgram(shaderProgram); return 0; } @@ -113,19 +107,12 @@ void Client::displayCallback() { /* Render here */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - if (this->gameState.phase == GamePhase::TITLE_SCREEN) { } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); } - ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - /* Poll for and process events */ glfwPollEvents(); glfwSwapBuffers(window); diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index 255f3603..f11ce9b0 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -7,9 +7,9 @@ namespace gui::font { std::string getFilepath(Font font) { auto dir = getRepoRoot() / "fonts"; switch (font) { - case Font::MENU: return (dir / "Lato-Regular.tff").string(); + case Font::MENU: return (dir / "Lato-Regular.ttf").string(); default: - case Font::TEXT: return (dir / "Lato-Regular.tff").string(); + case Font::TEXT: return (dir / "Lato-Regular.ttf").string(); } } diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 50bf55c7..616e613d 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -10,21 +10,20 @@ namespace gui::font { -std::size_t font_pair_hash::operator()(const std::pair& p) { +std::size_t font_pair_hash::operator()(const std::pair& p) const { // idk if this is actually doing what I think it is doing - return std::hash(static_cast(p.first) << 32 ^ p.second); + return (static_cast(p.first) << 32 ^ p.second); } bool Loader::init() { - FT_Library ft; - if (FT_Init_FreeType(&ft)) { + if (FT_Init_FreeType(&this->ft)) { std::cerr << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; return false; } // we mess with some alignment when creating the textures, // so this is supposed to prevent seg faults related to that - glPixelStorei(GL_UNPACK_ALIGNMVkxENT, 1); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); if (!this->_loadFont(Font::MENU)) { return false; @@ -33,16 +32,20 @@ bool Loader::init() { return false; } + FT_Done_FreeType(this->ft); // done loading fonts, so can release these resources + return true; } bool Loader::_loadFont(Font font) { auto path = font::getFilepath(font); + std::cout << "Loading font: " << path << "\n"; + FT_Face face; - if (FT_New_Face(ft, path, 0, &face)) { + if (FT_New_Face(this->ft, path.c_str(), 0, &face)) { std::cout << "ERROR::FREETYPE: Failed to load font at " << path << std::endl; - return -1; + return false; } for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE}) { @@ -89,7 +92,6 @@ bool Loader::_loadFont(Font font) { } FT_Done_Face(face); - FT_Done_FreeType(ft); return true; } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp new file mode 100644 index 00000000..cc671f09 --- /dev/null +++ b/src/client/gui/gui.cpp @@ -0,0 +1,18 @@ +#include "client/gui/gui.hpp" + +#include + +namespace gui { + + bool GUI::init() { + std::cout << "Initializing GUI...\n"; + + if (!this->fonts.init()) { + return false; + } + + std::cout << "Initialized GUI\n"; + return true; + } + +} \ No newline at end of file diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index c09b74c7..1ebd306c 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -8,6 +8,7 @@ set(FILES utilities/config.cpp utilities/rng.cpp + utilities/root_path.cpp ) add_library(${LIB_NAME} STATIC ${FILES}) From a4dc0be29565822727db313d8d696ef9a5dd2976 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 28 Apr 2024 11:55:24 -0700 Subject: [PATCH 11/92] theoretically this should be rendering fonts --- include/client/core.hpp | 3 +- include/client/gui/font/font.hpp | 4 +- include/client/gui/font/loader.hpp | 2 + include/client/gui/gui.hpp | 14 ++++- include/client/gui/widget/dyntext.hpp | 15 ++++- include/client/gui/widget/widget.hpp | 4 +- src/client/CMakeLists.txt | 1 + src/client/client.cpp | 19 +++++-- src/client/gui/font/font.cpp | 13 +++++ src/client/gui/font/loader.cpp | 12 +++- src/client/gui/gui.cpp | 37 +++++++++--- src/client/gui/widget/dyntext.cpp | 81 +++++++++++++++++++++++++++ src/client/gui/widget/widget.cpp | 4 +- src/client/shaders/text.frag | 15 +++++ src/client/shaders/text.vert | 14 +++++ 15 files changed, 213 insertions(+), 25 deletions(-) create mode 100644 src/client/gui/widget/dyntext.cpp create mode 100644 src/client/shaders/text.frag create mode 100644 src/client/shaders/text.vert diff --git a/include/client/core.hpp b/include/client/core.hpp index 5d1d1ba0..6ce73f45 100644 --- a/include/client/core.hpp +++ b/include/client/core.hpp @@ -1,3 +1,4 @@ #include #include -#include \ No newline at end of file +#include +#include \ No newline at end of file diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index ef0c5d5c..6695c2d8 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -1,6 +1,6 @@ #pragma once -#include "shared/utilities/root_path.hpp" +#include "client/core.hpp" #include #include @@ -26,4 +26,6 @@ enum class FontColor { std::string getFilepath(Font font); +glm::vec3 getRGB(FontColor color); + } diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp index 0136a3f1..5f59755f 100644 --- a/include/client/gui/font/loader.hpp +++ b/include/client/gui/font/loader.hpp @@ -30,6 +30,8 @@ class Loader { bool init(); + [[nodiscard]] const Character& loadChar(char c, Font font, FontSizePx size) const; + private: FT_Library ft; diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 5fc9f9f7..dabe6eea 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -6,19 +6,27 @@ #include "client/gui/widget/options.hpp" #include "client/gui/widget/type.hpp" #include "client/gui/widget/widget.hpp" +#include "client/gui/widget/dyntext.hpp" #include "client/gui/font/font.hpp" #include "client/gui/font/loader.hpp" +#include + namespace gui { class GUI { public: - GUI() = default; + GUI(); + + bool init(GLuint text_shader); - bool init(); + void render(); private: - font::Loader fonts; + std::vector text; + GLuint text_shader; + + std::shared_ptr fonts; }; } \ No newline at end of file diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index ae5daf2e..49141efc 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -1,9 +1,12 @@ #pragma once #include +#include +#include "client/core.hpp" #include "client/gui/widget/widget.hpp" #include "client/gui/font/font.hpp" +#include "client/gui/font/loader.hpp" namespace gui::widget { @@ -12,15 +15,23 @@ class DynText : public Widget { struct Options { font::Font font {font::Font::TEXT}; font::FontSizePx font_size {font::FontSizePx::MEDIUM}; + glm::vec3 color {font::getRGB(font::FontColor::BLACK)}; + float scale {1.0}; }; + // TODO: way to make certain words within the dyntext different colors? - DynText(std::string text, Options options = {}); + DynText(std::string text, std::shared_ptr loader, Options options); + DynText(std::string text, std::shared_ptr loader); - void render() override; + void render(GLuint shader, float x, float y) override; private: Options options; + std::string text; + std::shared_ptr fonts; + unsigned int VAO; + unsigned int VBO; }; } diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index 4818822a..9f073367 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -1,6 +1,6 @@ #pragma once -// #include "client/core.hpp" +#include "client/core.hpp" #include "client/gui/widget/type.hpp" #include "client/gui/widget/options.hpp" @@ -29,7 +29,7 @@ class Widget { void removeOnClick(CallbackHandle handle); void removeOnHover(CallbackHandle handle); - virtual void render() = 0; + virtual void render(GLuint shader, float x, float y) = 0; [[nodiscard]] Type getType() const; diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 9528f734..f5995740 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -19,6 +19,7 @@ set(FILES gui/font/font.cpp gui/font/loader.cpp gui/widget/widget.cpp + gui/widget/dyntext.cpp gui/gui.cpp ${IMGUI_SOURCES} diff --git a/src/client/client.cpp b/src/client/client.cpp index e0bac81d..f57d2fc6 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -14,6 +14,7 @@ #include "shared/network/constants.hpp" #include "shared/network/packet.hpp" #include "shared/utilities/config.hpp" +#include "shared/utilities/root_path.hpp" using namespace boost::asio::ip; using namespace std::chrono_literals; @@ -29,7 +30,8 @@ Client::Client(boost::asio::io_context& io_context, GameConfig config): socket(io_context), config(config), gameState(GamePhase::TITLE_SCREEN, config), - session(nullptr) + session(nullptr), + gui() { } @@ -81,15 +83,22 @@ int Client::init() { /* Load shader programs */ std::cout << "loading shader" << std::endl; - shaderProgram = LoadShaders("../src/client/shaders/shader.vert", "../src/client/shaders/shader.frag"); + auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; + shaderProgram = LoadShaders((shader_path / "shader.vert").c_str(), (shader_path / "shader.frag").c_str()); + auto textShaderProgram = LoadShaders((shader_path / "text.vert").c_str(), (shader_path / "text.frag").c_str()); - // Check the shader program. if (!shaderProgram) { std::cerr << "Failed to initialize shader program" << std::endl; return -1; } - if (!this->gui.init()) { + if (!textShaderProgram) { + std::cerr << "Failed to initialize text shader program" << std::endl; + return -1; + } + + // Init GUI (e.g. load in all fonts) + if (!this->gui.init(textShaderProgram)) { std::cerr << "GUI failed to init" << std::endl; return -1; } @@ -107,6 +116,8 @@ void Client::displayCallback() { /* Render here */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + this->gui.render(); + if (this->gameState.phase == GamePhase::TITLE_SCREEN) { } else if (this->gameState.phase == GamePhase::GAME) { diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index f11ce9b0..c211ac56 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -1,5 +1,6 @@ #include "client/gui/font/font.hpp" +#include "client/core.hpp" #include "shared/utilities/root_path.hpp" namespace gui::font { @@ -13,4 +14,16 @@ std::string getFilepath(Font font) { } } +glm::vec3 getRGB(FontColor color) { + switch (color) { + case FontColor::RED: + return {1.0f, 0.0f, 0.0f}; + case FontColor::BLUE: + return {0.0f, 0.0f, 1.0f}; + default: + case FontColor::BLACK: + return {1.0f, 1.0f, 1.0f}; + } +} + } diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 616e613d..8ef6de18 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -12,7 +12,7 @@ namespace gui::font { std::size_t font_pair_hash::operator()(const std::pair& p) const { // idk if this is actually doing what I think it is doing - return (static_cast(p.first) << 32 ^ p.second); + return (static_cast(p.first) << 32 ^ p.second); } bool Loader::init() { @@ -37,6 +37,14 @@ bool Loader::init() { return true; } +const Character& Loader::loadChar(char c, Font font, FontSizePx size) const { + if (!this->font_map.contains({font, size}) || !this->font_map.at({font, size}).contains(c)) { + return this->font_map.at({Font::TEXT, FontSizePx::MEDIUM}).at('?'); + } + + return this->font_map.at({font, size}).at(c); +} + bool Loader::_loadFont(Font font) { auto path = font::getFilepath(font); @@ -83,7 +91,7 @@ bool Loader::_loadFont(Font font) { texture, glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), - face->glyph->advance.x + static_cast(face->glyph->advance.x) }; characters.insert({c, character}); } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index cc671f09..8f077d1b 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -4,15 +4,38 @@ namespace gui { - bool GUI::init() { - std::cout << "Initializing GUI...\n"; +GUI::GUI() { - if (!this->fonts.init()) { - return false; - } +} - std::cout << "Initialized GUI\n"; - return true; +bool GUI::init(GLuint text_shader) +{ + std::cout << "Initializing GUI...\n"; + + this->fonts = std::make_shared(); + + if (!this->fonts->init()) { + return false; } + this->text_shader = text_shader; + + this->text.push_back(widget::DynText("Arcana", this->fonts)); + + std::cout << "Initialized GUI\n"; + return true; +} + +void GUI::render() { + // for text rendering + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + for (auto& t : this->text) { + t.render(this->text_shader, 0, 0); + } + + glDisable(GL_BLEND); +} + } \ No newline at end of file diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp new file mode 100644 index 00000000..0aca4ad4 --- /dev/null +++ b/src/client/gui/widget/dyntext.cpp @@ -0,0 +1,81 @@ +#include "client/gui/widget/dyntext.hpp" +#include "client/gui/font/font.hpp" +#include "client/gui/font/loader.hpp" +#include "client/core.hpp" + +#include +#include +#include + +namespace gui::widget { + +DynText::DynText(std::string text, std::shared_ptr fonts, + DynText::Options options): + text(text), options(options), fonts(fonts), Widget(Type::DynText) +{ + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); +} + +DynText::DynText(std::string text, std::shared_ptr fonts): + text(text), fonts(fonts), Widget(Type::DynText) +{ + // let the default values take over for options +} + +void DynText::render(GLuint shader, float x, float y) { + glUseProgram(shader); + + // todo move to gui + glm::mat4 projection = glm::ortho(0.0f, 800.0f, 0.0f, 600.0f); + glUniformMatrix4fv(glGetUniformLocation(shader, "projection"), 1, false, reinterpret_cast(&projection)); + glUniform3f(glGetUniformLocation(shader, "textColor"), + this->options.color.x, this->options.color.y, this->options.color.z); + glActiveTexture(GL_TEXTURE0); + glBindVertexArray(VAO); + + // iterate through all characters + for (const char& c : this->text) + { + font::Character ch = this->fonts->loadChar(c, this->options.font, this->options.font_size); + + float xpos = x + ch.bearing.x * this->options.scale; + float ypos = y - (ch.size.y - ch.bearing.y) * this->options.scale; + + float w = ch.size.x * this->options.scale; + float h = ch.size.y * this->options.scale; + // update VBO for each character + float vertices[6][4] = { + { xpos, ypos + h, 0.0f, 0.0f }, + { xpos, ypos, 0.0f, 1.0f }, + { xpos + w, ypos, 1.0f, 1.0f }, + + { xpos, ypos + h, 0.0f, 0.0f }, + { xpos + w, ypos, 1.0f, 1.0f }, + { xpos + w, ypos + h, 1.0f, 0.0f } + }; + // render glyph texture over quad + glBindTexture(GL_TEXTURE_2D, ch.texture_id); + // update content of VBO memory + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glBindBuffer(GL_ARRAY_BUFFER, 0); + // render quad + glDrawArrays(GL_TRIANGLES, 0, 6); + // now advance cursors for next glyph (note that advance is number of 1/64 pixels) + x += (ch.advance >> 6) * this->options.scale; // bitshift by 6 to get value in pixels (2^6 = 64) + } + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + + glUseProgram(0); +} + +} \ No newline at end of file diff --git a/src/client/gui/widget/widget.cpp b/src/client/gui/widget/widget.cpp index 73979706..28eab67c 100644 --- a/src/client/gui/widget/widget.cpp +++ b/src/client/gui/widget/widget.cpp @@ -1,7 +1,6 @@ #include "client/gui/widget/widget.hpp" -namespace gui { -namespace widget { +namespace gui::widget { Widget::Widget(Type type): type(type) @@ -61,4 +60,3 @@ Type Widget::getType() const { } } -} diff --git a/src/client/shaders/text.frag b/src/client/shaders/text.frag new file mode 100644 index 00000000..e4ea6554 --- /dev/null +++ b/src/client/shaders/text.frag @@ -0,0 +1,15 @@ +#version 330 core +in vec2 TexCoords; +out vec4 color; + +uniform sampler2D text; +uniform vec3 textColor; + +// Fragment shader for rendering text, copied from +// https://learnopengl.com/In-Practice/Text-Rendering + +void main() +{ + vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); + color = vec4(textColor, 1.0) * sampled; +} diff --git a/src/client/shaders/text.vert b/src/client/shaders/text.vert new file mode 100644 index 00000000..e3360d36 --- /dev/null +++ b/src/client/shaders/text.vert @@ -0,0 +1,14 @@ +#version 330 core +layout (location = 0) in vec4 vertex; // +out vec2 TexCoords; + +uniform mat4 projection; + +// Vertex shader for rendering text, copied from +// https://learnopengl.com/In-Practice/Text-Rendering + +void main() +{ + gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); + TexCoords = vertex.zw; +} From 777fa9ad44104f537115c01e9f929c22a5fae3d5 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 28 Apr 2024 12:01:18 -0700 Subject: [PATCH 12/92] random location rendering for debuggin --- src/client/gui/gui.cpp | 8 +++++++- src/client/gui/widget/dyntext.cpp | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 8f077d1b..8468c48e 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -1,6 +1,7 @@ #include "client/gui/gui.hpp" #include +#include "shared/utilities/rng.hpp" namespace gui { @@ -32,7 +33,12 @@ void GUI::render() { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); for (auto& t : this->text) { - t.render(this->text_shader, 0, 0); + // originally tried 0,0 which should be bottom left corner i think + // now trying to render it to random coordinates to see if + // it flickers maybe and im using the wrong coords at 0,0? + t.render(this->text_shader, + static_cast(randomInt(-640, 640)), + static_cast(randomInt(-480, 480))); } glDisable(GL_BLEND); diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 0aca4ad4..36262fd2 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -34,7 +34,7 @@ void DynText::render(GLuint shader, float x, float y) { glUseProgram(shader); // todo move to gui - glm::mat4 projection = glm::ortho(0.0f, 800.0f, 0.0f, 600.0f); + glm::mat4 projection = glm::ortho(0.0f, 640.0f, 0.0f, 480.0f); glUniformMatrix4fv(glGetUniformLocation(shader, "projection"), 1, false, reinterpret_cast(&projection)); glUniform3f(glGetUniformLocation(shader, "textColor"), this->options.color.x, this->options.color.y, this->options.color.z); From 675bcefd641b1e90a8a0f1a0c720f69dd4b528ab Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sun, 28 Apr 2024 23:28:16 -0700 Subject: [PATCH 13/92] docs and cleanup --- include/client/model.hpp | 612 ++++---------------------------------- include/client/shader.hpp | 41 ++- src/client/client.cpp | 10 +- src/client/model.cpp | 10 +- src/client/shader.cpp | 21 +- 5 files changed, 118 insertions(+), 576 deletions(-) diff --git a/include/client/model.hpp b/include/client/model.hpp index d178abad..7505631f 100644 --- a/include/client/model.hpp +++ b/include/client/model.hpp @@ -14,6 +14,10 @@ #include "assimp/material.h" #include "client/shader.hpp" +/** + * Stores position, normal vector, and coordinates + * in texture map for every vertex in a mesh. + */ struct Vertex { glm::vec3 position; glm::vec3 normal; @@ -22,31 +26,60 @@ struct Vertex { class Texture { public: + /** + * Load a texture from a filepath. + * + * @param filepath is the file path to the texture + * @param type specifies the type of texture to load. + * Currently only aiTextureType_SPECULAR and aiTextureType_DIFFUSE + * are implemented. + */ Texture(const std::string& filepath, const aiTextureType& type); - unsigned int getID() const; + + /** + * @return the texture's ID to be passed into OpenGL functions + */ + GLuint getID() const; + + /* + * Get the type of texture. Either "texture_diffuse" or "texture_specular". + */ std::string getType() const; private: - unsigned int ID; + GLuint ID; std::string type; }; +/** + * Mesh holds the data needed to render a mesh (collection of triangles). + * + * A Mesh differs from a Model since a Model is typically made up of multiple, + * smaller meshes. This is useful for animating parts of model individual (ex: legs, + * arms, head) + */ class Mesh { public: - std::vector vertices; - // std::vector indices; - // std::vector textures; + /** + * Creates a new mesh from a collection of vertices, indices and textures + */ + Mesh(const std::vector& vertices, const std::vector& indices, const std::vector& textures); - std::vector positions; - std::vector normals; + /** + * Render the Mesh to the viewport using the provided shader program. + * + * @param shader to use when rendering the program. Determines position of + * vertices and their color/texture. + * @param modelView determines the scaling/rotation/translation of the + * mesh + */ + void Draw(std::shared_ptr shader, glm::mat4 modelView) const; + private: + std::vector vertices; std::vector indices; std::vector textures; - Mesh(std::vector vertices, std::vector indices, std::vector textures); - void Draw(std::shared_ptr shader, glm::mat4 modelView) const; - private: // render data opengl needs GLuint VAO, VBO, EBO; - // GLuint VAO, VBO_positions, VBO_normals, EBO; }; @@ -59,7 +92,7 @@ class Model { * * @param Filepath to model file. */ - Model(const std::string& filepath); + explicit Model(const std::string& filepath); /** * Draws all the meshes of a given model @@ -69,546 +102,29 @@ class Model { */ void Draw(std::shared_ptr shader); - void Update(const glm::vec3& new_pos); + /** + * Sets the position of the Model to the given x,y,z + * values + * + * @param vector of x, y, z of the model's new position + */ + void TranslateTo(const glm::vec3& new_pos); + + /** + * Scale the Model across all axes (x,y,z) + * by a factor + * + * @param new_factor describes how much to scale the model by. + * Ex: setting it to 0.5 will cut the model's rendered size + * in half. + */ void Scale(const float& new_factor); private: - // model data std::vector meshes; - void processNode(aiNode *node, const aiScene *scene); - Mesh processMesh(aiMesh *mesh, const aiScene *scene); - std::vector loadMaterialTextures(aiMaterial *mat, const aiTextureType& type); + void processNode(aiNode* node, const aiScene* scene); + Mesh processMesh(aiMesh* mesh, const aiScene* scene); + std::vector loadMaterialTextures(aiMaterial* mat, const aiTextureType& type); glm::mat4 modelView; }; - -// #ifndef MODEL_H -// #define MODEL_H -// -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// -// -// #include -// #include -// #include -// #include -// #include -// #include -// using namespace std; -// -// -// #include -// -// #include -// #include -// #include -// #include -// -// class Shader -// { -// public: -// unsigned int ID; -// // constructor generates the shader on the fly -// // ------------------------------------------------------------------------ -// Shader() = default; -// Shader(const char* vertexPath, const char* fragmentPath, const char* geometryPath = nullptr) -// { -// // 1. retrieve the vertex/fragment source code from filePath -// std::string vertexCode; -// std::string fragmentCode; -// std::string geometryCode; -// std::ifstream vShaderFile; -// std::ifstream fShaderFile; -// std::ifstream gShaderFile; -// // ensure ifstream objects can throw exceptions: -// vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); -// fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); -// gShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); -// try -// { -// // open files -// vShaderFile.open(vertexPath); -// fShaderFile.open(fragmentPath); -// std::stringstream vShaderStream, fShaderStream; -// // read file's buffer contents into streams -// vShaderStream << vShaderFile.rdbuf(); -// fShaderStream << fShaderFile.rdbuf(); -// // close file handlers -// vShaderFile.close(); -// fShaderFile.close(); -// // convert stream into string -// vertexCode = vShaderStream.str(); -// fragmentCode = fShaderStream.str(); -// // if geometry shader path is present, also load a geometry shader -// if(geometryPath != nullptr) -// { -// gShaderFile.open(geometryPath); -// std::stringstream gShaderStream; -// gShaderStream << gShaderFile.rdbuf(); -// gShaderFile.close(); -// geometryCode = gShaderStream.str(); -// } -// } -// catch (std::ifstream::failure& e) -// { -// std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl; -// } -// const char* vShaderCode = vertexCode.c_str(); -// const char * fShaderCode = fragmentCode.c_str(); -// // 2. compile shaders -// unsigned int vertex, fragment; -// // vertex shader -// vertex = glCreateShader(GL_VERTEX_SHADER); -// glShaderSource(vertex, 1, &vShaderCode, NULL); -// glCompileShader(vertex); -// checkCompileErrors(vertex, "VERTEX"); -// // fragment Shader -// fragment = glCreateShader(GL_FRAGMENT_SHADER); -// glShaderSource(fragment, 1, &fShaderCode, NULL); -// glCompileShader(fragment); -// checkCompileErrors(fragment, "FRAGMENT"); -// // if geometry shader is given, compile geometry shader -// unsigned int geometry; -// if(geometryPath != nullptr) -// { -// const char * gShaderCode = geometryCode.c_str(); -// geometry = glCreateShader(GL_GEOMETRY_SHADER); -// glShaderSource(geometry, 1, &gShaderCode, NULL); -// glCompileShader(geometry); -// checkCompileErrors(geometry, "GEOMETRY"); -// } -// // shader Program -// ID = glCreateProgram(); -// glAttachShader(ID, vertex); -// glAttachShader(ID, fragment); -// if(geometryPath != nullptr) -// glAttachShader(ID, geometry); -// glLinkProgram(ID); -// checkCompileErrors(ID, "PROGRAM"); -// // delete the shaders as they're linked into our program now and no longer necessary -// glDeleteShader(vertex); -// glDeleteShader(fragment); -// if(geometryPath != nullptr) -// glDeleteShader(geometry); -// -// } -// // activate the shader -// // ------------------------------------------------------------------------ -// void use() -// { -// glUseProgram(ID); -// } -// // utility uniform functions -// // ------------------------------------------------------------------------ -// void setBool(const std::string &name, bool value) const -// { -// glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); -// } -// // ------------------------------------------------------------------------ -// void setInt(const std::string &name, int value) const -// { -// glUniform1i(glGetUniformLocation(ID, name.c_str()), value); -// } -// // ------------------------------------------------------------------------ -// void setFloat(const std::string &name, float value) const -// { -// glUniform1f(glGetUniformLocation(ID, name.c_str()), value); -// } -// // ------------------------------------------------------------------------ -// void setVec2(const std::string &name, const glm::vec2 &value) const -// { -// glUniform2fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); -// } -// void setVec2(const std::string &name, float x, float y) const -// { -// glUniform2f(glGetUniformLocation(ID, name.c_str()), x, y); -// } -// // ------------------------------------------------------------------------ -// void setVec3(const std::string &name, const glm::vec3 &value) const -// { -// glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); -// } -// void setVec3(const std::string &name, float x, float y, float z) const -// { -// glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z); -// } -// // ------------------------------------------------------------------------ -// void setVec4(const std::string &name, const glm::vec4 &value) const -// { -// glUniform4fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); -// } -// void setVec4(const std::string &name, float x, float y, float z, float w) -// { -// glUniform4f(glGetUniformLocation(ID, name.c_str()), x, y, z, w); -// } -// // ------------------------------------------------------------------------ -// void setMat2(const std::string &name, const glm::mat2 &mat) const -// { -// glUniformMatrix2fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); -// } -// // ------------------------------------------------------------------------ -// void setMat3(const std::string &name, const glm::mat3 &mat) const -// { -// glUniformMatrix3fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); -// } -// // ------------------------------------------------------------------------ -// void setMat4(const std::string &name, const glm::mat4 &mat) const -// { -// glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); -// } -// -// private: -// // utility function for checking shader compilation/linking errors. -// // ------------------------------------------------------------------------ -// void checkCompileErrors(GLuint shader, std::string type) -// { -// GLint success; -// GLchar infoLog[1024]; -// if(type != "PROGRAM") -// { -// glGetShaderiv(shader, GL_COMPILE_STATUS, &success); -// if(!success) -// { -// glGetShaderInfoLog(shader, 1024, NULL, infoLog); -// std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; -// } -// } -// else -// { -// glGetProgramiv(shader, GL_LINK_STATUS, &success); -// if(!success) -// { -// glGetProgramInfoLog(shader, 1024, NULL, infoLog); -// std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; -// } -// } -// } -// }; -// -// #include -// #include -// -// -// #include -// #include -// using namespace std; -// -// #define MAX_BONE_INFLUENCE 4 -// -// struct Vertex { -// // position -// glm::vec3 Position; -// // normal -// glm::vec3 Normal; -// // texCoords -// glm::vec2 TexCoords; -// // tangent -// glm::vec3 Tangent; -// // bitangent -// glm::vec3 Bitangent; -// //bone indexes which will influence this vertex -// int m_BoneIDs[MAX_BONE_INFLUENCE]; -// //weights from each bone -// float m_Weights[MAX_BONE_INFLUENCE]; -// }; -// -// struct Texture { -// unsigned int id; -// string type; -// string path; -// }; -// -// class Mesh { -// public: -// // mesh Data -// vector vertices; -// vector indices; -// vector textures; -// unsigned int VAO; -// -// // constructor -// Mesh(vector vertices, vector indices, vector textures) -// { -// this->vertices = vertices; -// this->indices = indices; -// this->textures = textures; -// -// // now that we have all the required data, set the vertex buffers and its attribute pointers. -// setupMesh(); -// } -// -// // render the mesh -// void Draw(Shader &shader) -// { -// // bind appropriate textures -// unsigned int diffuseNr = 1; -// unsigned int specularNr = 1; -// unsigned int normalNr = 1; -// unsigned int heightNr = 1; -// for(unsigned int i = 0; i < textures.size(); i++) -// { -// glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding -// // retrieve texture number (the N in diffuse_textureN) -// string number; -// string name = textures[i].type; -// if(name == "texture_diffuse") -// number = std::to_string(diffuseNr++); -// else if(name == "texture_specular") -// number = std::to_string(specularNr++); // transfer unsigned int to string -// else if(name == "texture_normal") -// number = std::to_string(normalNr++); // transfer unsigned int to string -// else if(name == "texture_height") -// number = std::to_string(heightNr++); // transfer unsigned int to string -// -// // now set the sampler to the correct texture unit -// glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i); -// // and finally bind the texture -// glBindTexture(GL_TEXTURE_2D, textures[i].id); -// } -// -// // draw mesh -// glBindVertexArray(VAO); -// glDrawElements(GL_TRIANGLES, static_cast(indices.size()), GL_UNSIGNED_INT, 0); -// glBindVertexArray(0); -// -// // always good practice to set everything back to defaults once configured. -// glActiveTexture(GL_TEXTURE0); -// } -// -// private: -// // render data -// unsigned int VBO, EBO; -// -// // initializes all the buffer objects/arrays -// void setupMesh() -// { -// // create buffers/arrays -// glGenVertexArrays(1, &VAO); -// glGenBuffers(1, &VBO); -// glGenBuffers(1, &EBO); -// -// glBindVertexArray(VAO); -// // load data into vertex buffers -// glBindBuffer(GL_ARRAY_BUFFER, VBO); -// // A great thing about structs is that their memory layout is sequential for all its items. -// // The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which -// // again translates to 3/2 floats which translates to a byte array. -// glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW); -// -// glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); -// glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); -// -// // set the vertex attribute pointers -// // vertex Positions -// glEnableVertexAttribArray(0); -// glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); -// // vertex normals -// glEnableVertexAttribArray(1); -// glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal)); -// // vertex texture coords -// glEnableVertexAttribArray(2); -// glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords)); -// // vertex tangent -// glEnableVertexAttribArray(3); -// glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent)); -// // vertex bitangent -// glEnableVertexAttribArray(4); -// glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent)); -// // ids -// glEnableVertexAttribArray(5); -// glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs)); -// -// // weights -// glEnableVertexAttribArray(6); -// glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights)); -// glBindVertexArray(0); -// } -// }; -// -// class Model -// { -// public: -// // model data -// vector textures_loaded; // stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once. -// vector meshes; -// string directory; -// bool gammaCorrection; -// -// // constructor, expects a filepath to a 3D model. -// Model(string const &path, bool gamma = false) : gammaCorrection(gamma) -// { -// loadModel(path); -// } -// -// // draws the model, and thus all its meshes -// void Draw(Shader &shader) -// { -// for(unsigned int i = 0; i < meshes.size(); i++) -// meshes[i].Draw(shader); -// } -// -// private: -// // loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector. -// void loadModel(string const &path) -// { -// // read file via ASSIMP -// Assimp::Importer importer; -// const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace); -// // check for errors -// if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero -// { -// cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl; -// return; -// } -// // retrieve the directory path of the filepath -// directory = path.substr(0, path.find_last_of('/')); -// -// // process ASSIMP's root node recursively -// processNode(scene->mRootNode, scene); -// } -// -// // processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any). -// void processNode(aiNode *node, const aiScene *scene) -// { -// // process each mesh located at the current node -// for(unsigned int i = 0; i < node->mNumMeshes; i++) -// { -// // the node object only contains indices to index the actual objects in the scene. -// // the scene contains all the data, node is just to keep stuff organized (like relations between nodes). -// aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; -// meshes.push_back(processMesh(mesh, scene)); -// } -// // after we've processed all of the meshes (if any) we then recursively process each of the children nodes -// for(unsigned int i = 0; i < node->mNumChildren; i++) -// { -// processNode(node->mChildren[i], scene); -// } -// -// } -// -// Mesh processMesh(aiMesh *mesh, const aiScene *scene) -// { -// // data to fill -// vector vertices; -// vector indices; -// vector textures; -// -// // walk through each of the mesh's vertices -// for(unsigned int i = 0; i < mesh->mNumVertices; i++) -// { -// Vertex vertex; -// glm::vec3 vector; // we declare a placeholder vector since assimp uses its own vector class that doesn't directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first. -// // positions -// vector.x = mesh->mVertices[i].x; -// vector.y = mesh->mVertices[i].y; -// vector.z = mesh->mVertices[i].z; -// vertex.Position = vector; -// // normals -// if (mesh->HasNormals()) -// { -// vector.x = mesh->mNormals[i].x; -// vector.y = mesh->mNormals[i].y; -// vector.z = mesh->mNormals[i].z; -// vertex.Normal = vector; -// } -// // texture coordinates -// if(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates? -// { -// glm::vec2 vec; -// // a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't -// // use models where a vertex can have multiple texture coordinates so we always take the first set (0). -// vec.x = mesh->mTextureCoords[0][i].x; -// vec.y = mesh->mTextureCoords[0][i].y; -// vertex.TexCoords = vec; -// // tangent -// vector.x = mesh->mTangents[i].x; -// vector.y = mesh->mTangents[i].y; -// vector.z = mesh->mTangents[i].z; -// vertex.Tangent = vector; -// // bitangent -// vector.x = mesh->mBitangents[i].x; -// vector.y = mesh->mBitangents[i].y; -// vector.z = mesh->mBitangents[i].z; -// vertex.Bitangent = vector; -// } -// else -// vertex.TexCoords = glm::vec2(0.0f, 0.0f); -// -// vertices.push_back(vertex); -// } -// // now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices. -// for(unsigned int i = 0; i < mesh->mNumFaces; i++) -// { -// aiFace face = mesh->mFaces[i]; -// // retrieve all indices of the face and store them in the indices vector -// for(unsigned int j = 0; j < face.mNumIndices; j++) -// indices.push_back(face.mIndices[j]); -// } -// // process materials -// aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; -// // we assume a convention for sampler names in the shaders. Each diffuse texture should be named -// // as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER. -// // Same applies to other texture as the following list summarizes: -// // diffuse: texture_diffuseN -// // specular: texture_specularN -// // normal: texture_normalN -// -// // 1. diffuse maps -// vector diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); -// textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); -// // 2. specular maps -// vector specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular"); -// textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); -// // 3. normal maps -// std::vector normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal"); -// textures.insert(textures.end(), normalMaps.begin(), normalMaps.end()); -// // 4. height maps -// std::vector heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height"); -// textures.insert(textures.end(), heightMaps.begin(), heightMaps.end()); -// -// // return a mesh object created from the extracted mesh data -// return Mesh(vertices, indices, textures); -// } -// -// // checks all material textures of a given type and loads the textures if they're not loaded yet. -// // the required info is returned as a Texture struct. -// vector loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) -// { -// vector textures; -// for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) -// { -// aiString str; -// mat->GetTexture(type, i, &str); -// // check if texture was loaded before and if so, continue to next iteration: skip loading a new texture -// bool skip = false; -// for(unsigned int j = 0; j < textures_loaded.size(); j++) -// { -// if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0) -// { -// textures.push_back(textures_loaded[j]); -// skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization) -// break; -// } -// } -// if(!skip) -// { // if texture hasn't been loaded already, load it -// Texture texture; -// // texture.id = TextureFromFile(str.C_Str(), this->directory); -// texture.type = typeName; -// texture.path = str.C_Str(); -// textures.push_back(texture); -// textures_loaded.push_back(texture); // store it as texture loaded for entire model, to ensure we won't unnecessary load duplicate textures. -// } -// } -// return textures; -// } -// }; -// -// -// #endif diff --git a/include/client/shader.hpp b/include/client/shader.hpp index e5a44e6c..89c48b4a 100644 --- a/include/client/shader.hpp +++ b/include/client/shader.hpp @@ -10,16 +10,49 @@ class Shader { public: - // constructor reads and builds the shader + /** + * Create a shader program from filepaths to a vertex and fragment shader. + * Use getID() to access the shader program's ID. + */ Shader(const std::string& vertexPath, const std::string& fragmentPath); ~Shader(); - unsigned int getID(); - // use/activate the shader + /* + * @return the shader program's ID which can be passed into OpenGL functions + */ + GLuint getID(); + + /* + * Activate the shader program. Must be called before drawing. + * Calls glUseProgram under the hood. + */ void use(); - // utility uniform functions + + /* + * Sets a boolean unform variable of the shader program + * with the specified value + * @param name is the name of the uniform variable as written + * in the shader program + * @param value is the boolean value to write to that variable + */ void setBool(const std::string &name, bool value) const; + + /* + * Sets an integer unform variable of the shader program + * with the specified value + * @param name is the name of the uniform variable as written + * in the shader program + * @param value is the integer value to write to that variable + */ void setInt(const std::string &name, int value) const; + + /* + * Sets a float unform variable of the shader program + * with the specified value + * @param name is the name of the uniform variable as written + * in the shader program + * @param value is the float value to write to that variable + */ void setFloat(const std::string &name, float value) const; private: // the shader program ID diff --git a/src/client/client.cpp b/src/client/client.cpp index 7e00b4ec..b8e834e6 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -83,17 +83,9 @@ bool Client::init() { boost::filesystem::path vertFilepath = this->root_path / "src/client/shaders/shader.vert"; boost::filesystem::path fragFilepath = this->root_path / "src/client/shaders/shader.frag"; this->cubeShader = std::make_shared(vertFilepath.c_str(), fragFilepath.c_str()); - if (!this->cubeShader) { - std::cout << "Could not load cube shader" << std::endl; - return false; - } boost::filesystem::path playerModelFilepath = this->root_path / "src/client/models/bear-sp22.obj"; this->playerModel = std::make_unique(playerModelFilepath.string()); - if (!this->playerModel) { - std::cout << "Could not load player model" << std::endl; - return false; - } this->playerModel->Scale(0.25); return true; @@ -161,7 +153,7 @@ void Client::draw() { continue; } // all objects are players for now - this->playerModel->Update(sharedObject->physics.position); + this->playerModel->TranslateTo(sharedObject->physics.position); this->playerModel->Draw(this->cubeShader); } } diff --git a/src/client/model.cpp b/src/client/model.cpp index dcee5092..1d06a09a 100644 --- a/src/client/model.cpp +++ b/src/client/model.cpp @@ -30,7 +30,7 @@ #include -Mesh::Mesh(std::vector vertices, std::vector indices, std::vector textures) : +Mesh::Mesh(const std::vector& vertices, const std::vector& indices, const std::vector& textures) : vertices(vertices), indices(indices), textures(textures) { glGenVertexArrays(1, &VAO); @@ -46,15 +46,15 @@ Mesh::Mesh(std::vector vertices, std::vector indices, std: // vertex positions glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(0)); // vertex normals glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal)); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, normal))); // vertex texture coords glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, textureCoords)); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, textureCoords))); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); @@ -138,7 +138,7 @@ void Model::Draw(std::shared_ptr shader) { mesh.Draw(shader, this->modelView); } -void Model::Update(const glm::vec3 &new_pos) { +void Model::TranslateTo(const glm::vec3 &new_pos) { modelView[3] = glm::vec4(new_pos, 1.0f); } diff --git a/src/client/shader.cpp b/src/client/shader.cpp index 2cca847e..5433a8aa 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -12,31 +12,32 @@ Shader::Shader(const std::string& vertexPath, const std::string& fragmentPath) { std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; - // ensure ifstream objects can throw exceptions: + + // ensure ifstream objects can throw exceptions vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { - // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); + std::stringstream vShaderStream, fShaderStream; - // read file's buffer contents into streams + vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); - // close file handlers + vShaderFile.close(); fShaderFile.close(); - // convert stream into string + vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); - } catch(std::ifstream::failure e) { - std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; + } catch(std::ifstream::failure& e) { + throw std::invalid_argument("Error: could not read shader file"); } + const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str(); - // 2. compile shaders - unsigned int vertex, fragment; + GLuint vertex, fragment; int success; char infoLog[512]; @@ -105,6 +106,6 @@ void Shader::setFloat(const std::string &name, float value) const { glUniform1f(glGetUniformLocation(ID, name.c_str()), value); } -unsigned int Shader::getID() { +GLuint Shader::getID() { return ID; } From 758ef8e2d3941afa8ee676e60bc90d20911c3f74 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Mon, 29 Apr 2024 17:27:20 -0700 Subject: [PATCH 14/92] pass std::string to shader constructor might be causing Windows client not to build. for some reason I was converting it to c_str before --- src/client/client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index b8e834e6..2b0cf4a0 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -82,7 +82,7 @@ bool Client::init() { boost::filesystem::path vertFilepath = this->root_path / "src/client/shaders/shader.vert"; boost::filesystem::path fragFilepath = this->root_path / "src/client/shaders/shader.frag"; - this->cubeShader = std::make_shared(vertFilepath.c_str(), fragFilepath.c_str()); + this->cubeShader = std::make_shared(vertFilepath.string(), fragFilepath.string()); boost::filesystem::path playerModelFilepath = this->root_path / "src/client/models/bear-sp22.obj"; this->playerModel = std::make_unique(playerModelFilepath.string()); From 42c1888367e8e0114ad4156e75a6af5386038870 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Mon, 29 Apr 2024 18:49:19 -0700 Subject: [PATCH 15/92] rendering... something --- include/client/gui/font/font.hpp | 6 +++--- src/client/gui/font/font.cpp | 2 +- src/client/gui/gui.cpp | 20 +++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index 6695c2d8..f04b9465 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -13,9 +13,9 @@ enum class Font { }; enum FontSizePx { - SMALL = 12, - MEDIUM = 24, - LARGE = 36 + SMALL = 64, + MEDIUM = 128, + LARGE = 256 }; enum class FontColor { diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index c211ac56..62103d91 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -22,7 +22,7 @@ glm::vec3 getRGB(FontColor color) { return {0.0f, 0.0f, 1.0f}; default: case FontColor::BLACK: - return {1.0f, 1.0f, 1.0f}; + return {0.0f, 0.0f, 0.0f}; } } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 8468c48e..7a641d64 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -21,7 +21,15 @@ bool GUI::init(GLuint text_shader) this->text_shader = text_shader; - this->text.push_back(widget::DynText("Arcana", this->fonts)); + this->text.push_back( + widget::DynText("Arcana", + this->fonts, + widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::LARGE, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f, + })); std::cout << "Initialized GUI\n"; return true; @@ -32,15 +40,13 @@ void GUI::render() { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_CULL_FACE); + for (auto& t : this->text) { - // originally tried 0,0 which should be bottom left corner i think - // now trying to render it to random coordinates to see if - // it flickers maybe and im using the wrong coords at 0,0? - t.render(this->text_shader, - static_cast(randomInt(-640, 640)), - static_cast(randomInt(-480, 480))); + t.render(this->text_shader, 0.0f, 0.0f); } + glDisable(GL_CULL_FACE); glDisable(GL_BLEND); } From 476cfbd4f9c838a129ddc5b713ed09134926f043 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Mon, 29 Apr 2024 19:28:34 -0700 Subject: [PATCH 16/92] fix incorrect defaults for dyntext options --- include/client/gui/font/loader.hpp | 2 +- src/client/gui/font/loader.cpp | 2 +- src/client/gui/gui.cpp | 11 +---------- src/client/gui/widget/dyntext.cpp | 7 ++++++- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp index 5f59755f..e17e99a0 100644 --- a/include/client/gui/font/loader.hpp +++ b/include/client/gui/font/loader.hpp @@ -39,7 +39,7 @@ class Loader { std::unordered_map< std::pair, - std::unordered_map, + std::unordered_map, font_pair_hash > font_map; }; diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 8ef6de18..e0158195 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -58,7 +58,7 @@ bool Loader::_loadFont(Font font) { for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE}) { FT_Set_Pixel_Sizes(face, 0, font_size); - std::unordered_map characters; + std::unordered_map characters; for (unsigned char c = 0; c < 128; c++) { // load character glyph if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 7a641d64..bcb6e3db 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -21,15 +21,7 @@ bool GUI::init(GLuint text_shader) this->text_shader = text_shader; - this->text.push_back( - widget::DynText("Arcana", - this->fonts, - widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::LARGE, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f, - })); + this->text.push_back(widget::DynText("abcdefg", this->fonts)); std::cout << "Initialized GUI\n"; return true; @@ -39,7 +31,6 @@ void GUI::render() { // for text rendering glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_CULL_FACE); for (auto& t : this->text) { diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 36262fd2..1e3431ad 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -25,7 +25,12 @@ DynText::DynText(std::string text, std::shared_ptr fonts, } DynText::DynText(std::string text, std::shared_ptr fonts): - text(text), fonts(fonts), Widget(Type::DynText) + DynText(text, fonts, DynText::Options { + .font {font::Font::TEXT}, + .font_size {font::FontSizePx::MEDIUM}, + .color {font::getRGB(font::FontColor::BLACK)}, + .scale {1.0}, + }) { // let the default values take over for options } From 3337619e7399b4d7d9f06d0fd00b59ccc5df4478 Mon Sep 17 00:00:00 2001 From: David Min Date: Mon, 29 Apr 2024 20:04:42 -0700 Subject: [PATCH 17/92] Enabled polygon fill for text drawing, removed imgui from CMakeLists --- src/client/CMakeLists.txt | 6 ++---- src/client/client.cpp | 6 +++--- src/client/gui/gui.cpp | 4 +++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index f5995740..cafbc969 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -5,7 +5,6 @@ set(LIB_NAME game_client_lib) # potential review later add_subdirectory(../../dependencies/glfw ${CMAKE_BINARY_DIR}/glfw) add_subdirectory(../../dependencies/glm ${CMAKE_BINARY_DIR}/glm) -add_subdirectory(../../dependencies/imgui ${CMAKE_BINARY_DIR}/imgui) add_subdirectory(../../dependencies/glew ${CMAKE_BINARY_DIR}/glew) add_subdirectory(../../dependencies/freetype ${CMAKE_BINARY_DIR}/freetype) @@ -22,7 +21,6 @@ set(FILES gui/widget/dyntext.cpp gui/gui.cpp - ${IMGUI_SOURCES} ) # OpenGL @@ -42,13 +40,13 @@ target_link_libraries(${LIB_NAME} Boost::serialization nlohmann_json::nlohmann_json ) -target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${IMGUI_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static freetype) add_executable(${TARGET_NAME} main.cpp) target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${IMGUI_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") target_link_libraries(${TARGET_NAME} PRIVATE game_shared_lib ${LIB_NAME}) target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static freetype) diff --git a/src/client/client.cpp b/src/client/client.cpp index f57d2fc6..6686c596 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -3,9 +3,9 @@ #include #include #include -#include "imgui.h" -#include "imgui_impl_glfw.h" -#include "imgui_impl_opengl3.h" +// #include "imgui.h" +// #include "imgui_impl_glfw.h" +// #include "imgui_impl_opengl3.h" #include #include diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index bcb6e3db..95a5d73d 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -21,7 +21,7 @@ bool GUI::init(GLuint text_shader) this->text_shader = text_shader; - this->text.push_back(widget::DynText("abcdefg", this->fonts)); + this->text.push_back(widget::DynText("Arcana", this->fonts)); std::cout << "Initialized GUI\n"; return true; @@ -32,6 +32,8 @@ void GUI::render() { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_CULL_FACE); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + for (auto& t : this->text) { t.render(this->text_shader, 0.0f, 0.0f); From 192c8e72f0e1597191c1e2b52d03a7207eb846d2 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Mon, 29 Apr 2024 20:14:30 -0700 Subject: [PATCH 18/92] click handling logic --- include/client/client.hpp | 1 - include/client/gui/gui.hpp | 14 ++++++++- include/client/gui/widget/widget.hpp | 5 ++++ src/client/gui/gui.cpp | 45 +++++++++++++++++++++++++--- src/client/gui/widget/widget.cpp | 23 ++++++++++++++ 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index 42f4c73a..49e92d74 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -33,7 +33,6 @@ class Client { // Bound window callbacks static void keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods); - // Getter / Setters GLFWwindow* getWindow() { return window; } diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index dabe6eea..31887b72 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -11,19 +11,31 @@ #include "client/gui/font/loader.hpp" #include +#include namespace gui { +using WidgetHandle = std::size_t; + class GUI { public: + GUI(); bool init(GLuint text_shader); void render(); + WidgetHandle addWidget(std::unique_ptr widget, float x, float y); + std::unique_ptr removeWidget(WidgetHandle handle); + + void handleClick(float x, float y); + void handleHover(float x, float y); + private: - std::vector text; + WidgetHandle next_handle {0}; + std::unordered_map> widgets; + std::unordered_map> bboxes; GLuint text_shader; std::shared_ptr fonts; diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index 9f073367..fa80d97c 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -31,8 +31,13 @@ class Widget { virtual void render(GLuint shader, float x, float y) = 0; + void doClick(); + void doHover(); + [[nodiscard]] Type getType() const; + [[nodiscard]] std::pair getSize() const; + protected: Type type; std::size_t width {0}; diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index bcb6e3db..5ed76ec0 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -1,8 +1,10 @@ #include "client/gui/gui.hpp" +#include #include #include "shared/utilities/rng.hpp" + namespace gui { GUI::GUI() { @@ -21,8 +23,6 @@ bool GUI::init(GLuint text_shader) this->text_shader = text_shader; - this->text.push_back(widget::DynText("abcdefg", this->fonts)); - std::cout << "Initialized GUI\n"; return true; } @@ -33,12 +33,49 @@ void GUI::render() { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_CULL_FACE); - for (auto& t : this->text) { - t.render(this->text_shader, 0.0f, 0.0f); + for (auto& [_handle, widget] : this->widgets) { + widget->render(this->text_shader, 0.0f, 0.0f); } glDisable(GL_CULL_FACE); glDisable(GL_BLEND); } +WidgetHandle GUI::addWidget(std::unique_ptr widget, float x, float y) { + WidgetHandle handle = this->next_handle++; + glm::vec2 bottom_left(x, y); + const auto& [width, height] = widget->getSize(); + glm::vec2 top_right(x + width, y + height); + this->widgets.insert({handle, std::move(widget)}); + this->bboxes.insert({handle, {bottom_left, top_right}}); + return handle; +} + +std::unique_ptr GUI::removeWidget(WidgetHandle handle) { + auto widget = std::move(this->widgets.at(handle)); + this->widgets.erase(handle); + this->bboxes.erase(handle); + return widget; +} + +// TODO: reduce copied code between these two functions + +void GUI::handleClick(float x, float y) { + for (const auto& [handle, bbox] : this->bboxes) { + const auto& [bottom_left, top_right] = bbox; + if (x > bottom_left.x && x < top_right.x && y > bottom_left.y && y < top_right.y) { + this->widgets.at(handle)->doClick(); + } + } +} + +void GUI::handleHover(float x, float y) { + for (const auto& [handle, bbox] : this->bboxes) { + const auto& [bottom_left, top_right] = bbox; + if (x > bottom_left.x && x < top_right.x && y > bottom_left.y && y < top_right.y) { + this->widgets.at(handle)->doHover(); + } + } +} + } \ No newline at end of file diff --git a/src/client/gui/widget/widget.cpp b/src/client/gui/widget/widget.cpp index 28eab67c..e59114c9 100644 --- a/src/client/gui/widget/widget.cpp +++ b/src/client/gui/widget/widget.cpp @@ -17,34 +17,41 @@ Widget& Widget::setSize(std::size_t width, std::size_t height) { Widget& Widget::setAlign(VAlign valign, HAlign halign) { this->valign = valign; this->halign = halign; + return *this; } Widget& Widget::setAlign(HAlign halign) { this->setAlign(VAlign::NONE, halign); + return *this; } Widget& Widget::setAlign(VAlign valign) { this->setAlign(valign, HAlign::NONE); + return *this; } Widget& Widget::addOnClick(Callback callback, CallbackHandle& handle) { handle = this->next_click_handle++; this->on_clicks.insert({handle, callback}); + return *this; } Widget& Widget::addOnClick(Callback callback) { CallbackHandle handle = 0; this->addOnClick(callback, handle); + return *this; } Widget& Widget::addOnHover(Callback callback, CallbackHandle& handle) { handle = this->next_hover_handle++; this->on_hovers.insert({handle, callback}); + return *this; } Widget& Widget::addOnHover(Callback callback) { CallbackHandle handle = 0; this->addOnHover(callback, handle); + return *this; } void Widget::removeOnClick(CallbackHandle handle) { @@ -55,8 +62,24 @@ void Widget::removeOnHover(CallbackHandle handle) { this->on_hovers.erase(handle); } +void Widget::doClick() { + for (const auto& [_handle, callback] : this->on_clicks) { + callback(); + } +} + +void Widget::doHover() { + for (const auto& [_handle, callback] : this->on_hovers) { + callback(); + } +} + Type Widget::getType() const { return this->type; } +std::pair Widget::getSize() const { + return {this->width, this->height}; +} + } From ddce9b70fd109213b613357e7a0f15fd21d54564 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 00:47:24 -0700 Subject: [PATCH 19/92] first click handling attmept --- include/client/client.hpp | 5 ++++ include/client/gui/widget/widget.hpp | 10 ++----- include/client/util.hpp | 1 - src/client/client.cpp | 23 +++++++++++++-- src/client/gui/gui.cpp | 18 +++++++++--- src/client/gui/widget/dyntext.cpp | 19 ++++++++++-- src/client/gui/widget/widget.cpp | 44 ++++------------------------ src/client/main.cpp | 4 +-- 8 files changed, 66 insertions(+), 58 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index c0409766..45efb794 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -22,6 +22,9 @@ #include "shared/network/session.hpp" #include "shared/utilities/config.hpp" +#define WINDOW_WIDTH 1280 +#define WINDOW_HEIGHT 960 + using namespace boost::asio::ip; class Client { @@ -34,6 +37,7 @@ class Client { // Bound window callbacks static void keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods); static void mouseCallback(GLFWwindow* window, double xposIn, double yposIn); + static void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods); // Getter / Setters GLFWwindow* getWindow() { return window; } @@ -68,6 +72,7 @@ class Client { static bool cam_is_held_right; static bool cam_is_held_left; + static bool is_left_mouse_down; static float mouse_xpos; static float mouse_ypos; diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index fa80d97c..ab345e40 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -17,14 +17,8 @@ class Widget { public: explicit Widget(Type type); - Widget& setSize(std::size_t width, std::size_t height); - Widget& setAlign(VAlign valign, HAlign halign); - Widget& setAlign(VAlign valign); - Widget& setAlign(HAlign halign); - Widget& addOnClick(Callback callback, CallbackHandle& handle); - Widget& addOnClick(Callback callback); - Widget& addOnHover(Callback callback, CallbackHandle& handle); - Widget& addOnHover(Callback callback); + CallbackHandle addOnClick(Callback callback); + CallbackHandle addOnHover(Callback callback); void removeOnClick(CallbackHandle handle); void removeOnHover(CallbackHandle handle); diff --git a/include/client/util.hpp b/include/client/util.hpp index 8014f60d..5ae6653d 100644 --- a/include/client/util.hpp +++ b/include/client/util.hpp @@ -15,5 +15,4 @@ #include "client/core.hpp" - GLuint LoadShaders(const char* vertex_file_path, const char* fragment_file_path); diff --git a/src/client/client.cpp b/src/client/client.cpp index fae8a72c..f043bbbe 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -29,6 +29,7 @@ bool Client::cam_is_held_up = false; bool Client::cam_is_held_down = false; bool Client::cam_is_held_right = false; bool Client::cam_is_held_left = false; +bool Client::is_left_mouse_down = false; float Client::mouse_xpos = 0.0f; float Client::mouse_ypos = 0.0f; @@ -72,18 +73,19 @@ int Client::init() { /* Create a windowed mode window and its OpenGL context */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL); + window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Arcana", NULL, NULL); if (!window) { glfwTerminate(); return -1; } + glfwSetWindowSizeLimits(window, WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT); /* Make the window's context current */ glfwMakeContextCurrent(window); // tell GLFW to capture our mouse - glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + // glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); GLenum err = glewInit() ; if (GLEW_OK != err) { @@ -131,7 +133,7 @@ void Client::displayCallback() { this->gui.render(); if (this->gameState.phase == GamePhase::TITLE_SCREEN) { - + } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); } @@ -143,6 +145,10 @@ void Client::displayCallback() { // Handle any updates void Client::idleCallback(boost::asio::io_context& context) { + if (is_left_mouse_down) { + this->gui.handleClick(mouse_xpos, mouse_ypos); + } + std::optional movement = glm::vec3(0.0f); if(is_held_right) @@ -298,3 +304,14 @@ void Client::mouseCallback(GLFWwindow* window, double xposIn, double yposIn) { mouse_xpos = static_cast(xposIn); mouse_ypos = static_cast(yposIn); } + +void Client::mouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { + if (button == GLFW_MOUSE_BUTTON_LEFT) { + if (action == GLFW_PRESS) { + std::cout << mouse_xpos << ", " << mouse_ypos << "\n"; + is_left_mouse_down = true; + } else if (action == GLFW_RELEASE) { + is_left_mouse_down = false; + } + } +} \ No newline at end of file diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 2fb40ac4..c9b11c44 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -3,6 +3,7 @@ #include #include #include "shared/utilities/rng.hpp" +#include "client/client.hpp" namespace gui { @@ -23,7 +24,10 @@ bool GUI::init(GLuint text_shader) this->text_shader = text_shader; - this->addWidget(std::make_unique("Arcana", this->fonts), 0.0f, 0.0f); + auto title = std::make_unique("Arcana", this->fonts); + title->addOnClick([](){std::cout << "Clickie click\n";}); + + this->addWidget(std::move(title), 0.0f, 0.0f); std::cout << "Initialized GUI\n"; return true; @@ -36,9 +40,9 @@ void GUI::render() { glEnable(GL_CULL_FACE); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - - for (auto& [_handle, widget] : this->widgets) { - widget->render(this->text_shader, 0.0f, 0.0f); + for (auto& [handle, widget] : this->widgets) { + const auto& [bottom_left, _] = this->bboxes.at(handle); + widget->render(this->text_shader, bottom_left.x, bottom_left.y); } glDisable(GL_CULL_FACE); @@ -65,6 +69,9 @@ std::unique_ptr GUI::removeWidget(WidgetHandle handle) { // TODO: reduce copied code between these two functions void GUI::handleClick(float x, float y) { + // convert to gui coords, where (0,0) is bottome left + y = WINDOW_HEIGHT - y; + for (const auto& [handle, bbox] : this->bboxes) { const auto& [bottom_left, top_right] = bbox; if (x > bottom_left.x && x < top_right.x && y > bottom_left.y && y < top_right.y) { @@ -74,6 +81,9 @@ void GUI::handleClick(float x, float y) { } void GUI::handleHover(float x, float y) { + // convert to gui coords, where (0,0) is bottome left + y = WINDOW_HEIGHT - y; + for (const auto& [handle, bbox] : this->bboxes) { const auto& [bottom_left, top_right] = bbox; if (x > bottom_left.x && x < top_right.x && y > bottom_left.y && y < top_right.y) { diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 1e3431ad..5cc2df53 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -2,8 +2,10 @@ #include "client/gui/font/font.hpp" #include "client/gui/font/loader.hpp" #include "client/core.hpp" +#include "client/client.hpp" #include +#include #include #include @@ -21,7 +23,20 @@ DynText::DynText(std::string text, std::shared_ptr fonts, glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); + glBindVertexArray(0); + + // Calculate size of string of text + this->width = 0; + this->height = 0; + for (int i = 0; i < text.size(); i++) { + font::Character ch = this->fonts->loadChar(this->text[i], this->options.font, this->options.font_size); + this->height = std::max(this->height, static_cast(ch.size.y)); + if (i != text.size() - 1 && i != 0) { + this->width += ch.advance * 64; + } else { + this->width += ch.size.x; + } + } } DynText::DynText(std::string text, std::shared_ptr fonts): @@ -39,7 +54,7 @@ void DynText::render(GLuint shader, float x, float y) { glUseProgram(shader); // todo move to gui - glm::mat4 projection = glm::ortho(0.0f, 640.0f, 0.0f, 480.0f); + glm::mat4 projection = glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); glUniformMatrix4fv(glGetUniformLocation(shader, "projection"), 1, false, reinterpret_cast(&projection)); glUniform3f(glGetUniformLocation(shader, "textColor"), this->options.color.x, this->options.color.y, this->options.color.z); diff --git a/src/client/gui/widget/widget.cpp b/src/client/gui/widget/widget.cpp index e59114c9..13884248 100644 --- a/src/client/gui/widget/widget.cpp +++ b/src/client/gui/widget/widget.cpp @@ -8,50 +8,18 @@ Widget::Widget(Type type): } -Widget& Widget::setSize(std::size_t width, std::size_t height) { - this->width = width; - this->height = height; - return *this; -} - -Widget& Widget::setAlign(VAlign valign, HAlign halign) { - this->valign = valign; - this->halign = halign; - return *this; -} - -Widget& Widget::setAlign(HAlign halign) { - this->setAlign(VAlign::NONE, halign); - return *this; -} -Widget& Widget::setAlign(VAlign valign) { - this->setAlign(valign, HAlign::NONE); - return *this; -} - -Widget& Widget::addOnClick(Callback callback, CallbackHandle& handle) { - handle = this->next_click_handle++; +CallbackHandle Widget::addOnClick(Callback callback) { + CallbackHandle handle = this->next_click_handle++; this->on_clicks.insert({handle, callback}); - return *this; + return handle; } -Widget& Widget::addOnClick(Callback callback) { - CallbackHandle handle = 0; - this->addOnClick(callback, handle); - return *this; -} -Widget& Widget::addOnHover(Callback callback, CallbackHandle& handle) { - handle = this->next_hover_handle++; +CallbackHandle Widget::addOnHover(Callback callback) { + CallbackHandle handle = this->next_hover_handle++; this->on_hovers.insert({handle, callback}); - return *this; -} - -Widget& Widget::addOnHover(Callback callback) { - CallbackHandle handle = 0; - this->addOnHover(callback, handle); - return *this; + return handle; } void Widget::removeOnClick(CallbackHandle handle) { diff --git a/src/client/main.cpp b/src/client/main.cpp index b490ab4f..18680471 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -26,7 +26,7 @@ void set_callbacks(GLFWwindow* window) { glfwSetKeyCallback(window, Client::keyCallback); // Set the mouse and cursor callbacks - // glfwSetMouseButtonCallback(window, Client::mouseCallback); + glfwSetMouseButtonCallback(window, Client::mouseButtonCallback); glfwSetCursorPosCallback(window, Client::mouseCallback); } @@ -57,7 +57,7 @@ int main(int argc, char* argv[]) // lobby_finder.startSearching(); // } // } else { - // client.connectAndListen(config.network.server_ip); + // client.connectAndListen(config.network.server_ip); // } if (client.init() == -1) { From 0bc2cfda7e923c8e75ce809b7296d78ec4d09c2c Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 09:50:42 -0700 Subject: [PATCH 20/92] fix click hitbox for gui --- src/client/gui/widget/dyntext.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 5cc2df53..65961954 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -32,7 +32,7 @@ DynText::DynText(std::string text, std::shared_ptr fonts, font::Character ch = this->fonts->loadChar(this->text[i], this->options.font, this->options.font_size); this->height = std::max(this->height, static_cast(ch.size.y)); if (i != text.size() - 1 && i != 0) { - this->width += ch.advance * 64; + this->width += ch.advance / 64.0; } else { this->width += ch.size.x; } From 48b6390d557231a9525d0d50f94bf0dac0d48404 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 12:10:00 -0700 Subject: [PATCH 21/92] in progress: flexbox horrors --- include/client/client.hpp | 1 + include/client/gui/gui.hpp | 1 + include/client/gui/widget/dyntext.hpp | 6 ++++- include/client/gui/widget/flexbox.hpp | 35 +++++++++++++++++++++++++++ include/client/gui/widget/options.hpp | 16 ------------ include/client/gui/widget/type.hpp | 2 +- include/client/gui/widget/widget.hpp | 3 --- src/client/CMakeLists.txt | 1 + src/client/client.cpp | 1 + src/client/gui/gui.cpp | 11 ++++++--- src/client/gui/widget/flexbox.cpp | 24 ++++++++++++++++++ src/client/main.cpp | 4 +-- 12 files changed, 78 insertions(+), 27 deletions(-) create mode 100644 include/client/gui/widget/flexbox.hpp create mode 100644 src/client/gui/widget/flexbox.cpp diff --git a/include/client/client.hpp b/include/client/client.hpp index 45efb794..db379cd6 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -73,6 +73,7 @@ class Client { static bool cam_is_held_left; static bool is_left_mouse_down; + static bool is_click_available; static float mouse_xpos; static float mouse_ypos; diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 31887b72..72322231 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -7,6 +7,7 @@ #include "client/gui/widget/type.hpp" #include "client/gui/widget/widget.hpp" #include "client/gui/widget/dyntext.hpp" +#include "client/gui/widget/flexbox.hpp" #include "client/gui/font/font.hpp" #include "client/gui/font/loader.hpp" diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index 49141efc..15e46f8b 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -18,7 +18,11 @@ class DynText : public Widget { glm::vec3 color {font::getRGB(font::FontColor::BLACK)}; float scale {1.0}; }; - // TODO: way to make certain words within the dyntext different colors? + + template + static std::unique_ptr make(Params&&... params) { + return std::make_unique(std::forward(params)...); + } DynText(std::string text, std::shared_ptr loader, Options options); DynText(std::string text, std::shared_ptr loader); diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp new file mode 100644 index 00000000..649a5d57 --- /dev/null +++ b/include/client/gui/widget/flexbox.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "client/gui/widget/widget.hpp" + +#include +#include +#include + +namespace gui::widget { + +class Flexbox : public Widget { +public: + struct Options { + JustifyContent direction; + }; + + static std::unique_ptr make(Options options) { + return std::make_unique(options); + } + + Flexbox(Options options): + Widget(Type::Flexbox), options(options) + { + } + + void addItem(std::unique_ptr widget); + + void render(GLuint shader, float x, float y) override; + +private: + Options options; + std::vector> widgets; +}; + +} diff --git a/include/client/gui/widget/options.hpp b/include/client/gui/widget/options.hpp index ff84d19e..4498fbaa 100644 --- a/include/client/gui/widget/options.hpp +++ b/include/client/gui/widget/options.hpp @@ -2,22 +2,6 @@ namespace gui::widget { -enum class HAlign { - LEFT, - CENTER, - RIGHT, - - NONE -}; - -enum class VAlign { - TOP, - CENTER, - BOTTOM, - - NONE -}; - enum class JustifyContent { VERTICAL, HORIZONTAL diff --git a/include/client/gui/widget/type.hpp b/include/client/gui/widget/type.hpp index 774bf49e..6dc210b8 100644 --- a/include/client/gui/widget/type.hpp +++ b/include/client/gui/widget/type.hpp @@ -3,7 +3,7 @@ namespace gui::widget { enum class Type { - DynText + DynText, Flexbox }; } diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index ab345e40..10106563 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -37,9 +37,6 @@ class Widget { std::size_t width {0}; std::size_t height {0}; - VAlign valign {VAlign::NONE}; - HAlign halign {HAlign::NONE}; - std::unordered_map on_clicks; std::unordered_map on_hovers; diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index cafbc969..4cd3decb 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -19,6 +19,7 @@ set(FILES gui/font/loader.cpp gui/widget/widget.cpp gui/widget/dyntext.cpp + gui/widget/flexbox.cpp gui/gui.cpp ) diff --git a/src/client/client.cpp b/src/client/client.cpp index f043bbbe..9e5b3db1 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -147,6 +147,7 @@ void Client::displayCallback() { void Client::idleCallback(boost::asio::io_context& context) { if (is_left_mouse_down) { this->gui.handleClick(mouse_xpos, mouse_ypos); + is_left_mouse_down = false; } std::optional movement = glm::vec3(0.0f); diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index c9b11c44..814e5dfc 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "shared/utilities/rng.hpp" #include "client/client.hpp" @@ -24,10 +25,14 @@ bool GUI::init(GLuint text_shader) this->text_shader = text_shader; - auto title = std::make_unique("Arcana", this->fonts); - title->addOnClick([](){std::cout << "Clickie click\n";}); + auto title = widget::DynText::make("Arcana", this->fonts); + title->addOnClick([](){std::cout << "Clickie click on title\n";}); + auto option = widget::DynText::make("Start Game", this->fonts); + option->addOnClick([](){std::cout << "click on option\n";}); - this->addWidget(std::move(title), 0.0f, 0.0f); + auto flexbox = widget::Flexbox::make(std::move(title), std::move(option)); + + this->addWidget(std::move(flexbox), 0.0f, 0.0f); std::cout << "Initialized GUI\n"; return true; diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp new file mode 100644 index 00000000..c96360b4 --- /dev/null +++ b/src/client/gui/widget/flexbox.cpp @@ -0,0 +1,24 @@ +#include "client/gui/widget/flexbox.hpp" + +#include +#include +#include + +#include "client/core.hpp" + +namespace gui::widget { + +void Flexbox::render(GLuint shader, float x, float y) { + // use x and y as origin coordinates, and render everything else based off of it + + float curr_y = y; + for (const auto& widget : this->widgets) { + widget->render(shader, x, curr_y); + const auto& [_, curr_height] = widget->getSize(); + curr_y += curr_height; + } +} + + +} + diff --git a/src/client/main.cpp b/src/client/main.cpp index 18680471..fb56ea4c 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -15,7 +15,7 @@ void error_callback(int error, const char* description) { std::cerr << description << std::endl; } -void set_callbacks(GLFWwindow* window) { +void set_callbacks(GLFWwindow* window, Client* client) { // Set the error callback. glfwSetErrorCallback(error_callback); @@ -63,8 +63,6 @@ int main(int argc, char* argv[]) if (client.init() == -1) { exit(EXIT_FAILURE); } - - GLFWwindow* window = client.getWindow(); if (!window) exit(EXIT_FAILURE); From 2286c73839174bf27315787da1cbe33b293907d1 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 16:23:38 -0700 Subject: [PATCH 22/92] in progress --- include/client/gui/gui.hpp | 5 +- include/client/gui/widget/dyntext.hpp | 9 +++- include/client/gui/widget/flexbox.hpp | 22 +++++---- include/client/gui/widget/widget.hpp | 18 +++++-- src/client/gui/gui.cpp | 27 ++++------- src/client/gui/widget/dyntext.cpp | 26 ++++++---- src/client/gui/widget/flexbox.cpp | 69 ++++++++++++++++++++++++--- src/client/gui/widget/widget.cpp | 36 ++++++++++---- src/client/main.cpp | 2 +- 9 files changed, 152 insertions(+), 62 deletions(-) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 72322231..b8a56361 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -27,7 +27,7 @@ class GUI { void render(); - WidgetHandle addWidget(std::unique_ptr widget, float x, float y); + WidgetHandle addWidget(widget::Widget::Ptr&& widget, float x, float y); std::unique_ptr removeWidget(WidgetHandle handle); void handleClick(float x, float y); @@ -35,8 +35,7 @@ class GUI { private: WidgetHandle next_handle {0}; - std::unordered_map> widgets; - std::unordered_map> bboxes; + std::unordered_map widgets; GLuint text_shader; std::shared_ptr fonts; diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index 15e46f8b..ed462a64 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -12,6 +12,8 @@ namespace gui::widget { class DynText : public Widget { public: + using Ptr = std::unique_ptr; + struct Options { font::Font font {font::Font::TEXT}; font::FontSizePx font_size {font::FontSizePx::MEDIUM}; @@ -20,14 +22,17 @@ class DynText : public Widget { }; template - static std::unique_ptr make(Params&&... params) { + static Ptr make(Params&&... params) { return std::make_unique(std::forward(params)...); } + DynText(glm::vec2 origin, std::string text, std::shared_ptr loader, Options options); + DynText(glm::vec2 origin, std::string text, std::shared_ptr loader); DynText(std::string text, std::shared_ptr loader, Options options); DynText(std::string text, std::shared_ptr loader); - void render(GLuint shader, float x, float y) override; + void render(GLuint shader) override; + private: Options options; diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index 649a5d57..cd4358d1 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -2,7 +2,6 @@ #include "client/gui/widget/widget.hpp" -#include #include #include @@ -10,26 +9,29 @@ namespace gui::widget { class Flexbox : public Widget { public: + using Ptr = std::unique_ptr; + struct Options { JustifyContent direction; }; - static std::unique_ptr make(Options options) { - return std::make_unique(options); - } + static Ptr make(glm::vec2 origin, Options options); + static Ptr make(glm::vec2 origin); + + Flexbox(glm::vec2 origin, Options options); + explicit Flexbox(glm::vec2 origin); - Flexbox(Options options): - Widget(Type::Flexbox), options(options) - { - } + void doClick(float x, float y) override; + void doHover(float x, float y) override; - void addItem(std::unique_ptr widget); + void push(Widget::Ptr&& widget); - void render(GLuint shader, float x, float y) override; + void render(GLuint shader) override; private: Options options; std::vector> widgets; }; + } diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index 10106563..0a6af975 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -4,6 +4,7 @@ #include "client/gui/widget/type.hpp" #include "client/gui/widget/options.hpp" +#include #include #include #include @@ -15,7 +16,12 @@ using CallbackHandle = std::size_t; class Widget { public: - explicit Widget(Type type); + using Ptr = std::unique_ptr; + + explicit Widget(Type type, glm::vec2 origin); + + void setOrigin(glm::vec2 origin); + const glm::vec2& getOrigin() const; CallbackHandle addOnClick(Callback callback); CallbackHandle addOnHover(Callback callback); @@ -23,10 +29,10 @@ class Widget { void removeOnClick(CallbackHandle handle); void removeOnHover(CallbackHandle handle); - virtual void render(GLuint shader, float x, float y) = 0; + virtual void render(GLuint shader) = 0; - void doClick(); - void doHover(); + virtual void doClick(float x, float y); + virtual void doHover(float x, float y); [[nodiscard]] Type getType() const; @@ -34,6 +40,7 @@ class Widget { protected: Type type; + glm::vec2 origin; std::size_t width {0}; std::size_t height {0}; @@ -43,6 +50,9 @@ class Widget { private: CallbackHandle next_click_handle {0}; CallbackHandle next_hover_handle {0}; + + bool _doesIntersect(float x, float y) const; }; + } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 814e5dfc..c7f56632 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -30,7 +30,9 @@ bool GUI::init(GLuint text_shader) auto option = widget::DynText::make("Start Game", this->fonts); option->addOnClick([](){std::cout << "click on option\n";}); - auto flexbox = widget::Flexbox::make(std::move(title), std::move(option)); + auto flexbox = widget::Flexbox::make(glm::vec2(0.0f, 0.0f)); + flexbox->push(std::move(title)); + flexbox->push(std::move(option)); this->addWidget(std::move(flexbox), 0.0f, 0.0f); @@ -46,28 +48,23 @@ void GUI::render() { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); for (auto& [handle, widget] : this->widgets) { - const auto& [bottom_left, _] = this->bboxes.at(handle); - widget->render(this->text_shader, bottom_left.x, bottom_left.y); + widget->render(this->text_shader); } glDisable(GL_CULL_FACE); glDisable(GL_BLEND); } -WidgetHandle GUI::addWidget(std::unique_ptr widget, float x, float y) { +WidgetHandle GUI::addWidget(widget::Widget::Ptr&& widget, float x, float y) { WidgetHandle handle = this->next_handle++; - glm::vec2 bottom_left(x, y); const auto& [width, height] = widget->getSize(); - glm::vec2 top_right(x + width, y + height); this->widgets.insert({handle, std::move(widget)}); - this->bboxes.insert({handle, {bottom_left, top_right}}); return handle; } std::unique_ptr GUI::removeWidget(WidgetHandle handle) { auto widget = std::move(this->widgets.at(handle)); this->widgets.erase(handle); - this->bboxes.erase(handle); return widget; } @@ -77,11 +74,8 @@ void GUI::handleClick(float x, float y) { // convert to gui coords, where (0,0) is bottome left y = WINDOW_HEIGHT - y; - for (const auto& [handle, bbox] : this->bboxes) { - const auto& [bottom_left, top_right] = bbox; - if (x > bottom_left.x && x < top_right.x && y > bottom_left.y && y < top_right.y) { - this->widgets.at(handle)->doClick(); - } + for (const auto& [_, widget] : this->widgets) { + widget->doClick(x, y); } } @@ -89,11 +83,8 @@ void GUI::handleHover(float x, float y) { // convert to gui coords, where (0,0) is bottome left y = WINDOW_HEIGHT - y; - for (const auto& [handle, bbox] : this->bboxes) { - const auto& [bottom_left, top_right] = bbox; - if (x > bottom_left.x && x < top_right.x && y > bottom_left.y && y < top_right.y) { - this->widgets.at(handle)->doHover(); - } + for (const auto& [_, widget] : this->widgets) { + widget->doHover(x, y); } } diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 65961954..66ef1c9e 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -11,9 +11,9 @@ namespace gui::widget { -DynText::DynText(std::string text, std::shared_ptr fonts, - DynText::Options options): - text(text), options(options), fonts(fonts), Widget(Type::DynText) +DynText::DynText(glm::vec2 origin, std::string text, + std::shared_ptr fonts, DynText::Options options): + text(text), options(options), fonts(fonts), Widget(Type::DynText, origin) { glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); @@ -39,18 +39,21 @@ DynText::DynText(std::string text, std::shared_ptr fonts, } } -DynText::DynText(std::string text, std::shared_ptr fonts): - DynText(text, fonts, DynText::Options { +DynText::DynText(glm::vec2 origin, std::string text, std::shared_ptr fonts): + DynText(origin, text, fonts, DynText::Options { .font {font::Font::TEXT}, .font_size {font::FontSizePx::MEDIUM}, .color {font::getRGB(font::FontColor::BLACK)}, .scale {1.0}, - }) -{ - // let the default values take over for options -} + }) {} -void DynText::render(GLuint shader, float x, float y) { +DynText::DynText(std::string text, std::shared_ptr fonts): + DynText({0.0f, 0.0f}, text, fonts) {} + +DynText::DynText(std::string text, std::shared_ptr fonts, DynText::Options options): + DynText({0.0f, 0.0f}, text, fonts, options) {} + +void DynText::render(GLuint shader) { glUseProgram(shader); // todo move to gui @@ -61,6 +64,9 @@ void DynText::render(GLuint shader, float x, float y) { glActiveTexture(GL_TEXTURE0); glBindVertexArray(VAO); + float x = this->origin.x; + float y = this->origin.y; + // iterate through all characters for (const char& c : this->text) { diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index c96360b4..f11a0812 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -1,21 +1,78 @@ #include "client/gui/widget/flexbox.hpp" #include -#include #include #include "client/core.hpp" namespace gui::widget { -void Flexbox::render(GLuint shader, float x, float y) { +Flexbox::Ptr Flexbox::make(glm::vec2 origin, Flexbox::Options options) { + return std::make_unique(options, origin); +} + +Flexbox::Ptr Flexbox::make(glm::vec2 origin) { + return std::make_unique(origin); +} + +Flexbox::Flexbox(glm::vec2 origin, Flexbox::Options options): + Widget(Type::Flexbox, origin), + options(options) {} + +Flexbox::Flexbox(glm::vec2 origin): + Flexbox(origin, Flexbox::Options { .direction=JustifyContent::HORIZONTAL }) {} + +void Flexbox::doClick(float x, float y) { + Widget::doClick(x, y); + for (auto& widget : this->widgets) { + widget->doClick(x, y); + } +} + +void Flexbox::doHover(float x, float y) { + Widget::doHover(x, y); + for (auto& widget : this->widgets) { + widget->doHover(x, y); + } +} + +void Flexbox::push(Widget::Ptr&& widget) { + const auto& [new_width, new_height] = widget->getSize(); + + glm::vec2 prev_origin; + std::size_t prev_width; + std::size_t prev_height; + if (this->widgets.empty()) { + prev_origin = this->origin; + prev_width = this->width; + prev_height = this->height; + } else { + Widget::Ptr& prev_widget = this->widgets[this->widgets.size() - 1]; + prev_origin = prev_widget->getOrigin(); + prev_width = prev_widget->getSize().first; + prev_width = prev_widget->getSize().second; + } + + if (this->options.direction == JustifyContent::HORIZONTAL) { + this->width += new_width; + this->height = std::max(this->height, new_height); + glm::vec2 new_origin(prev_origin.x + prev_width, prev_origin.y); + widget->setOrigin(new_origin); + } else if (this->options.direction == JustifyContent::VERTICAL) { + this->height += new_height; + this->width = std::max(this->width, new_width); + glm::vec2 new_origin(prev_origin.x, prev_origin.y + prev_height); + widget->setOrigin(new_origin); + } + + this->widgets.push_back(std::move(widget)); +} + +void Flexbox::render(GLuint shader) { // use x and y as origin coordinates, and render everything else based off of it - float curr_y = y; for (const auto& widget : this->widgets) { - widget->render(shader, x, curr_y); - const auto& [_, curr_height] = widget->getSize(); - curr_y += curr_height; + widget->render(shader); } } diff --git a/src/client/gui/widget/widget.cpp b/src/client/gui/widget/widget.cpp index 13884248..fa3e3a92 100644 --- a/src/client/gui/widget/widget.cpp +++ b/src/client/gui/widget/widget.cpp @@ -2,12 +2,19 @@ namespace gui::widget { -Widget::Widget(Type type): - type(type) +Widget::Widget(Type type, glm::vec2 origin): + type(type), origin(origin) { } +void Widget::setOrigin(glm::vec2 origin) { + this->origin = origin; +} + +const glm::vec2& Widget::getOrigin() const { + return this->origin; +} CallbackHandle Widget::addOnClick(Callback callback) { CallbackHandle handle = this->next_click_handle++; @@ -30,15 +37,19 @@ void Widget::removeOnHover(CallbackHandle handle) { this->on_hovers.erase(handle); } -void Widget::doClick() { - for (const auto& [_handle, callback] : this->on_clicks) { - callback(); +void Widget::doClick(float x, float y) { + if (this->_doesIntersect(x, y)) { + for (const auto& [_handle, callback] : this->on_clicks) { + callback(); + } } } -void Widget::doHover() { - for (const auto& [_handle, callback] : this->on_hovers) { - callback(); +void Widget::doHover(float x, float y) { + if (this->_doesIntersect(x, y)) { + for (const auto& [_handle, callback] : this->on_hovers) { + callback(); + } } } @@ -50,4 +61,13 @@ std::pair Widget::getSize() const { return {this->width, this->height}; } +bool Widget::_doesIntersect(float x, float y) const { + return ( + x > this->origin.x && + y > this->origin.y && + x < this->origin.x + this->width && + y < this->origin.y + this->height + ); +} + } diff --git a/src/client/main.cpp b/src/client/main.cpp index fb56ea4c..e9597a5e 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -68,7 +68,7 @@ int main(int argc, char* argv[]) if (!window) exit(EXIT_FAILURE); // Setup callbacks. - set_callbacks(window); + set_callbacks(window, &client); // Setup OpenGL settings. set_opengl_settings(window); From 7f9d9c18170767778ccc5546405beb7990db4b1d Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 16:51:57 -0700 Subject: [PATCH 23/92] fix compile issue --- src/client/gui/widget/flexbox.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index f11a0812..c457240d 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -8,7 +8,7 @@ namespace gui::widget { Flexbox::Ptr Flexbox::make(glm::vec2 origin, Flexbox::Options options) { - return std::make_unique(options, origin); + return std::make_unique(origin, options); } Flexbox::Ptr Flexbox::make(glm::vec2 origin) { From 591e33b6a17360c295735a50936a558254dddfe4 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 17:28:07 -0700 Subject: [PATCH 24/92] working flexbox! --- include/client/gui/gui.hpp | 4 +++- src/client/gui/gui.cpp | 6 +++++- src/client/gui/widget/flexbox.cpp | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index b8a56361..5c49d47b 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -2,6 +2,7 @@ // #include "client/core.hpp" + // Include all gui headers so everyone else just needs to include this file #include "client/gui/widget/options.hpp" #include "client/gui/widget/type.hpp" @@ -20,7 +21,6 @@ using WidgetHandle = std::size_t; class GUI { public: - GUI(); bool init(GLuint text_shader); @@ -41,4 +41,6 @@ class GUI { std::shared_ptr fonts; }; +using namespace gui; + } \ No newline at end of file diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index c7f56632..c927b6f8 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -30,7 +30,11 @@ bool GUI::init(GLuint text_shader) auto option = widget::DynText::make("Start Game", this->fonts); option->addOnClick([](){std::cout << "click on option\n";}); - auto flexbox = widget::Flexbox::make(glm::vec2(0.0f, 0.0f)); + auto flexbox = widget::Flexbox::make( + glm::vec2(0.0f, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL + }); flexbox->push(std::move(title)); flexbox->push(std::move(option)); diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index c457240d..72d88674 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -50,7 +50,7 @@ void Flexbox::push(Widget::Ptr&& widget) { Widget::Ptr& prev_widget = this->widgets[this->widgets.size() - 1]; prev_origin = prev_widget->getOrigin(); prev_width = prev_widget->getSize().first; - prev_width = prev_widget->getSize().second; + prev_height = prev_widget->getSize().second; } if (this->options.direction == JustifyContent::HORIZONTAL) { From 1328df8516fdf23cbab157b86311006ac56c10b0 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 17:53:44 -0700 Subject: [PATCH 25/92] add center alignment to flexbox --- include/client/gui/widget/flexbox.hpp | 1 + include/client/gui/widget/options.hpp | 6 ++++++ src/client/gui/gui.cpp | 3 ++- src/client/gui/widget/flexbox.cpp | 22 +++++++++++++++++++++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index cd4358d1..aad51abb 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -13,6 +13,7 @@ class Flexbox : public Widget { struct Options { JustifyContent direction; + AlignItems alignment; }; static Ptr make(glm::vec2 origin, Options options); diff --git a/include/client/gui/widget/options.hpp b/include/client/gui/widget/options.hpp index 4498fbaa..244efe94 100644 --- a/include/client/gui/widget/options.hpp +++ b/include/client/gui/widget/options.hpp @@ -7,4 +7,10 @@ enum class JustifyContent { HORIZONTAL }; +enum class AlignItems { + CENTER, + LEFT, + RIGHT +}; + } \ No newline at end of file diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index c927b6f8..dd17d3d0 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -33,7 +33,8 @@ bool GUI::init(GLuint text_shader) auto flexbox = widget::Flexbox::make( glm::vec2(0.0f, 0.0f), widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL + .direction { widget::JustifyContent::VERTICAL }, + .alignment { widget::AlignItems::CENTER }, }); flexbox->push(std::move(title)); flexbox->push(std::move(option)); diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index 72d88674..d236cacb 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "client/core.hpp" @@ -20,7 +21,10 @@ Flexbox::Flexbox(glm::vec2 origin, Flexbox::Options options): options(options) {} Flexbox::Flexbox(glm::vec2 origin): - Flexbox(origin, Flexbox::Options { .direction=JustifyContent::HORIZONTAL }) {} + Flexbox(origin, Flexbox::Options { + .direction = JustifyContent::HORIZONTAL, + .alignment = AlignItems::LEFT, + }) {} void Flexbox::doClick(float x, float y) { Widget::doClick(x, y); @@ -39,6 +43,8 @@ void Flexbox::doHover(float x, float y) { void Flexbox::push(Widget::Ptr&& widget) { const auto& [new_width, new_height] = widget->getSize(); + // Bless this mess! + glm::vec2 prev_origin; std::size_t prev_width; std::size_t prev_height; @@ -65,6 +71,20 @@ void Flexbox::push(Widget::Ptr&& widget) { widget->setOrigin(new_origin); } + if (this->options.alignment == AlignItems::CENTER) { + if (this->options.direction == JustifyContent::HORIZONTAL) { + std::cerr << "Note: center alignment not yet implemented for horizontal justify. Doing nothing\n"; + } else if (this->options.direction == JustifyContent::VERTICAL) { + for (auto& widget : this->widgets) { + const auto [curr_width, _] = widget->getSize(); + glm::vec2 new_origin(this->origin.x + ((this->width - curr_width) / 2.0f), widget->getOrigin().y); + widget->setOrigin(new_origin); + } + } + } else if (this->options.alignment == AlignItems::RIGHT) { + std::cerr << "Note: right alignment not yet implemented. Doing nothing\n"; + } // else it is align left, which is default behavior and requires no more messing + this->widgets.push_back(std::move(widget)); } From cebcc2ae5dbfe4dfb7e795415a7bc0122f15acd2 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 19:01:39 -0700 Subject: [PATCH 26/92] start image loading --- dependencies/stb/stb_image.cpp | 2 + dependencies/stb/stb_image.h | 7985 +++++++++++++++++++++++ fonts/AncientModernTales-a7Po.ttf | Bin 0 -> 31228 bytes imgs/Yoshi.png | Bin 0 -> 240242 bytes include/client/gui/gui.hpp | 1 + include/client/gui/img/img.hpp | 21 + include/client/gui/img/loader.hpp | 21 + include/client/gui/widget/staticimg.hpp | 29 + include/client/gui/widget/type.hpp | 2 +- src/client/CMakeLists.txt | 6 + src/client/gui/font/font.cpp | 4 +- src/client/gui/imgs/img.cpp | 15 + src/client/gui/imgs/loader.cpp | 9 + src/client/gui/widget/staticimg.cpp | 9 + 14 files changed, 8101 insertions(+), 3 deletions(-) create mode 100644 dependencies/stb/stb_image.cpp create mode 100644 dependencies/stb/stb_image.h create mode 100644 fonts/AncientModernTales-a7Po.ttf create mode 100644 imgs/Yoshi.png create mode 100644 include/client/gui/img/img.hpp create mode 100644 include/client/gui/img/loader.hpp create mode 100644 include/client/gui/widget/staticimg.hpp create mode 100644 src/client/gui/imgs/img.cpp create mode 100644 src/client/gui/imgs/loader.cpp create mode 100644 src/client/gui/widget/staticimg.cpp diff --git a/dependencies/stb/stb_image.cpp b/dependencies/stb/stb_image.cpp new file mode 100644 index 00000000..badb3ef4 --- /dev/null +++ b/dependencies/stb/stb_image.cpp @@ -0,0 +1,2 @@ +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" \ No newline at end of file diff --git a/dependencies/stb/stb_image.h b/dependencies/stb/stb_image.h new file mode 100644 index 00000000..a632d543 --- /dev/null +++ b/dependencies/stb/stb_image.h @@ -0,0 +1,7985 @@ +/* stb_image - v2.29 - public domain image loader - http://nothings.org/stb + no warranty implied; use at your own risk + + Do this: + #define STB_IMAGE_IMPLEMENTATION + before you include this file in *one* C or C++ file to create the implementation. + + // i.e. it should look like this: + #include ... + #include ... + #include ... + #define STB_IMAGE_IMPLEMENTATION + #include "stb_image.h" + + You can #define STBI_ASSERT(x) before the #include to avoid using assert.h. + And #define STBI_MALLOC, STBI_REALLOC, and STBI_FREE to avoid using malloc,realloc,free + + + QUICK NOTES: + Primarily of interest to game developers and other people who can + avoid problematic images and only need the trivial interface + + JPEG baseline & progressive (12 bpc/arithmetic not supported, same as stock IJG lib) + PNG 1/2/4/8/16-bit-per-channel + + TGA (not sure what subset, if a subset) + BMP non-1bpp, non-RLE + PSD (composited view only, no extra channels, 8/16 bit-per-channel) + + GIF (*comp always reports as 4-channel) + HDR (radiance rgbE format) + PIC (Softimage PIC) + PNM (PPM and PGM binary only) + + Animated GIF still needs a proper API, but here's one way to do it: + http://gist.github.com/urraka/685d9a6340b26b830d49 + + - decode from memory or through FILE (define STBI_NO_STDIO to remove code) + - decode from arbitrary I/O callbacks + - SIMD acceleration on x86/x64 (SSE2) and ARM (NEON) + + Full documentation under "DOCUMENTATION" below. + + +LICENSE + + See end of file for license information. + +RECENT REVISION HISTORY: + + 2.29 (2023-05-xx) optimizations + 2.28 (2023-01-29) many error fixes, security errors, just tons of stuff + 2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes + 2.26 (2020-07-13) many minor fixes + 2.25 (2020-02-02) fix warnings + 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically + 2.23 (2019-08-11) fix clang static analysis warning + 2.22 (2019-03-04) gif fixes, fix warnings + 2.21 (2019-02-25) fix typo in comment + 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs + 2.19 (2018-02-11) fix warning + 2.18 (2018-01-30) fix warnings + 2.17 (2018-01-29) bugfix, 1-bit BMP, 16-bitness query, fix warnings + 2.16 (2017-07-23) all functions have 16-bit variants; optimizations; bugfixes + 2.15 (2017-03-18) fix png-1,2,4; all Imagenet JPGs; no runtime SSE detection on GCC + 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs + 2.13 (2016-12-04) experimental 16-bit API, only for PNG so far; fixes + 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes + 2.11 (2016-04-02) 16-bit PNGS; enable SSE2 in non-gcc x64 + RGB-format JPEG; remove white matting in PSD; + allocate large structures on the stack; + correct channel count for PNG & BMP + 2.10 (2016-01-22) avoid warning introduced in 2.09 + 2.09 (2016-01-16) 16-bit TGA; comments in PNM files; STBI_REALLOC_SIZED + + See end of file for full revision history. + + + ============================ Contributors ========================= + + Image formats Extensions, features + Sean Barrett (jpeg, png, bmp) Jetro Lauha (stbi_info) + Nicolas Schulz (hdr, psd) Martin "SpartanJ" Golini (stbi_info) + Jonathan Dummer (tga) James "moose2000" Brown (iPhone PNG) + Jean-Marc Lienher (gif) Ben "Disch" Wenger (io callbacks) + Tom Seddon (pic) Omar Cornut (1/2/4-bit PNG) + Thatcher Ulrich (psd) Nicolas Guillemot (vertical flip) + Ken Miller (pgm, ppm) Richard Mitton (16-bit PSD) + github:urraka (animated gif) Junggon Kim (PNM comments) + Christopher Forseth (animated gif) Daniel Gibson (16-bit TGA) + socks-the-fox (16-bit PNG) + Jeremy Sawicki (handle all ImageNet JPGs) + Optimizations & bugfixes Mikhail Morozov (1-bit BMP) + Fabian "ryg" Giesen Anael Seghezzi (is-16-bit query) + Arseny Kapoulkine Simon Breuss (16-bit PNM) + John-Mark Allen + Carmelo J Fdez-Aguera + + Bug & warning fixes + Marc LeBlanc David Woo Guillaume George Martins Mozeiko + Christpher Lloyd Jerry Jansson Joseph Thomson Blazej Dariusz Roszkowski + Phil Jordan Dave Moore Roy Eltham + Hayaki Saito Nathan Reed Won Chun + Luke Graham Johan Duparc Nick Verigakis the Horde3D community + Thomas Ruf Ronny Chevalier github:rlyeh + Janez Zemva John Bartholomew Michal Cichon github:romigrou + Jonathan Blow Ken Hamada Tero Hanninen github:svdijk + Eugene Golushkov Laurent Gomila Cort Stratton github:snagar + Aruelien Pocheville Sergio Gonzalez Thibault Reuille github:Zelex + Cass Everitt Ryamond Barbiero github:grim210 + Paul Du Bois Engin Manap Aldo Culquicondor github:sammyhw + Philipp Wiesemann Dale Weiler Oriol Ferrer Mesia github:phprus + Josh Tobin Neil Bickford Matthew Gregan github:poppolopoppo + Julian Raschke Gregory Mullen Christian Floisand github:darealshinji + Baldur Karlsson Kevin Schmidt JR Smith github:Michaelangel007 + Brad Weinberger Matvey Cherevko github:mosra + Luca Sas Alexander Veselov Zack Middleton [reserved] + Ryan C. Gordon [reserved] [reserved] + DO NOT ADD YOUR NAME HERE + + Jacko Dirks + + To add your name to the credits, pick a random blank space in the middle and fill it. + 80% of merge conflicts on stb PRs are due to people adding their name at the end + of the credits. +*/ + +#ifndef STBI_INCLUDE_STB_IMAGE_H +#define STBI_INCLUDE_STB_IMAGE_H + +// DOCUMENTATION +// +// Limitations: +// - no 12-bit-per-channel JPEG +// - no JPEGs with arithmetic coding +// - GIF always returns *comp=4 +// +// Basic usage (see HDR discussion below for HDR usage): +// int x,y,n; +// unsigned char *data = stbi_load(filename, &x, &y, &n, 0); +// // ... process data if not NULL ... +// // ... x = width, y = height, n = # 8-bit components per pixel ... +// // ... replace '0' with '1'..'4' to force that many components per pixel +// // ... but 'n' will always be the number that it would have been if you said 0 +// stbi_image_free(data); +// +// Standard parameters: +// int *x -- outputs image width in pixels +// int *y -- outputs image height in pixels +// int *channels_in_file -- outputs # of image components in image file +// int desired_channels -- if non-zero, # of image components requested in result +// +// The return value from an image loader is an 'unsigned char *' which points +// to the pixel data, or NULL on an allocation failure or if the image is +// corrupt or invalid. The pixel data consists of *y scanlines of *x pixels, +// with each pixel consisting of N interleaved 8-bit components; the first +// pixel pointed to is top-left-most in the image. There is no padding between +// image scanlines or between pixels, regardless of format. The number of +// components N is 'desired_channels' if desired_channels is non-zero, or +// *channels_in_file otherwise. If desired_channels is non-zero, +// *channels_in_file has the number of components that _would_ have been +// output otherwise. E.g. if you set desired_channels to 4, you will always +// get RGBA output, but you can check *channels_in_file to see if it's trivially +// opaque because e.g. there were only 3 channels in the source image. +// +// An output image with N components has the following components interleaved +// in this order in each pixel: +// +// N=#comp components +// 1 grey +// 2 grey, alpha +// 3 red, green, blue +// 4 red, green, blue, alpha +// +// If image loading fails for any reason, the return value will be NULL, +// and *x, *y, *channels_in_file will be unchanged. The function +// stbi_failure_reason() can be queried for an extremely brief, end-user +// unfriendly explanation of why the load failed. Define STBI_NO_FAILURE_STRINGS +// to avoid compiling these strings at all, and STBI_FAILURE_USERMSG to get slightly +// more user-friendly ones. +// +// Paletted PNG, BMP, GIF, and PIC images are automatically depalettized. +// +// To query the width, height and component count of an image without having to +// decode the full file, you can use the stbi_info family of functions: +// +// int x,y,n,ok; +// ok = stbi_info(filename, &x, &y, &n); +// // returns ok=1 and sets x, y, n if image is a supported format, +// // 0 otherwise. +// +// Note that stb_image pervasively uses ints in its public API for sizes, +// including sizes of memory buffers. This is now part of the API and thus +// hard to change without causing breakage. As a result, the various image +// loaders all have certain limits on image size; these differ somewhat +// by format but generally boil down to either just under 2GB or just under +// 1GB. When the decoded image would be larger than this, stb_image decoding +// will fail. +// +// Additionally, stb_image will reject image files that have any of their +// dimensions set to a larger value than the configurable STBI_MAX_DIMENSIONS, +// which defaults to 2**24 = 16777216 pixels. Due to the above memory limit, +// the only way to have an image with such dimensions load correctly +// is for it to have a rather extreme aspect ratio. Either way, the +// assumption here is that such larger images are likely to be malformed +// or malicious. If you do need to load an image with individual dimensions +// larger than that, and it still fits in the overall size limit, you can +// #define STBI_MAX_DIMENSIONS on your own to be something larger. +// +// =========================================================================== +// +// UNICODE: +// +// If compiling for Windows and you wish to use Unicode filenames, compile +// with +// #define STBI_WINDOWS_UTF8 +// and pass utf8-encoded filenames. Call stbi_convert_wchar_to_utf8 to convert +// Windows wchar_t filenames to utf8. +// +// =========================================================================== +// +// Philosophy +// +// stb libraries are designed with the following priorities: +// +// 1. easy to use +// 2. easy to maintain +// 3. good performance +// +// Sometimes I let "good performance" creep up in priority over "easy to maintain", +// and for best performance I may provide less-easy-to-use APIs that give higher +// performance, in addition to the easy-to-use ones. Nevertheless, it's important +// to keep in mind that from the standpoint of you, a client of this library, +// all you care about is #1 and #3, and stb libraries DO NOT emphasize #3 above all. +// +// Some secondary priorities arise directly from the first two, some of which +// provide more explicit reasons why performance can't be emphasized. +// +// - Portable ("ease of use") +// - Small source code footprint ("easy to maintain") +// - No dependencies ("ease of use") +// +// =========================================================================== +// +// I/O callbacks +// +// I/O callbacks allow you to read from arbitrary sources, like packaged +// files or some other source. Data read from callbacks are processed +// through a small internal buffer (currently 128 bytes) to try to reduce +// overhead. +// +// The three functions you must define are "read" (reads some bytes of data), +// "skip" (skips some bytes of data), "eof" (reports if the stream is at the end). +// +// =========================================================================== +// +// SIMD support +// +// The JPEG decoder will try to automatically use SIMD kernels on x86 when +// supported by the compiler. For ARM Neon support, you must explicitly +// request it. +// +// (The old do-it-yourself SIMD API is no longer supported in the current +// code.) +// +// On x86, SSE2 will automatically be used when available based on a run-time +// test; if not, the generic C versions are used as a fall-back. On ARM targets, +// the typical path is to have separate builds for NEON and non-NEON devices +// (at least this is true for iOS and Android). Therefore, the NEON support is +// toggled by a build flag: define STBI_NEON to get NEON loops. +// +// If for some reason you do not want to use any of SIMD code, or if +// you have issues compiling it, you can disable it entirely by +// defining STBI_NO_SIMD. +// +// =========================================================================== +// +// HDR image support (disable by defining STBI_NO_HDR) +// +// stb_image supports loading HDR images in general, and currently the Radiance +// .HDR file format specifically. You can still load any file through the existing +// interface; if you attempt to load an HDR file, it will be automatically remapped +// to LDR, assuming gamma 2.2 and an arbitrary scale factor defaulting to 1; +// both of these constants can be reconfigured through this interface: +// +// stbi_hdr_to_ldr_gamma(2.2f); +// stbi_hdr_to_ldr_scale(1.0f); +// +// (note, do not use _inverse_ constants; stbi_image will invert them +// appropriately). +// +// Additionally, there is a new, parallel interface for loading files as +// (linear) floats to preserve the full dynamic range: +// +// float *data = stbi_loadf(filename, &x, &y, &n, 0); +// +// If you load LDR images through this interface, those images will +// be promoted to floating point values, run through the inverse of +// constants corresponding to the above: +// +// stbi_ldr_to_hdr_scale(1.0f); +// stbi_ldr_to_hdr_gamma(2.2f); +// +// Finally, given a filename (or an open file or memory block--see header +// file for details) containing image data, you can query for the "most +// appropriate" interface to use (that is, whether the image is HDR or +// not), using: +// +// stbi_is_hdr(char *filename); +// +// =========================================================================== +// +// iPhone PNG support: +// +// We optionally support converting iPhone-formatted PNGs (which store +// premultiplied BGRA) back to RGB, even though they're internally encoded +// differently. To enable this conversion, call +// stbi_convert_iphone_png_to_rgb(1). +// +// Call stbi_set_unpremultiply_on_load(1) as well to force a divide per +// pixel to remove any premultiplied alpha *only* if the image file explicitly +// says there's premultiplied data (currently only happens in iPhone images, +// and only if iPhone convert-to-rgb processing is on). +// +// =========================================================================== +// +// ADDITIONAL CONFIGURATION +// +// - You can suppress implementation of any of the decoders to reduce +// your code footprint by #defining one or more of the following +// symbols before creating the implementation. +// +// STBI_NO_JPEG +// STBI_NO_PNG +// STBI_NO_BMP +// STBI_NO_PSD +// STBI_NO_TGA +// STBI_NO_GIF +// STBI_NO_HDR +// STBI_NO_PIC +// STBI_NO_PNM (.ppm and .pgm) +// +// - You can request *only* certain decoders and suppress all other ones +// (this will be more forward-compatible, as addition of new decoders +// doesn't require you to disable them explicitly): +// +// STBI_ONLY_JPEG +// STBI_ONLY_PNG +// STBI_ONLY_BMP +// STBI_ONLY_PSD +// STBI_ONLY_TGA +// STBI_ONLY_GIF +// STBI_ONLY_HDR +// STBI_ONLY_PIC +// STBI_ONLY_PNM (.ppm and .pgm) +// +// - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still +// want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB +// +// - If you define STBI_MAX_DIMENSIONS, stb_image will reject images greater +// than that size (in either width or height) without further processing. +// This is to let programs in the wild set an upper bound to prevent +// denial-of-service attacks on untrusted data, as one could generate a +// valid image of gigantic dimensions and force stb_image to allocate a +// huge block of memory and spend disproportionate time decoding it. By +// default this is set to (1 << 24), which is 16777216, but that's still +// very big. + +#ifndef STBI_NO_STDIO +#include +#endif // STBI_NO_STDIO + +#define STBI_VERSION 1 + +enum +{ + STBI_default = 0, // only used for desired_channels + + STBI_grey = 1, + STBI_grey_alpha = 2, + STBI_rgb = 3, + STBI_rgb_alpha = 4 +}; + +#include +typedef unsigned char stbi_uc; +typedef unsigned short stbi_us; + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef STBIDEF +#ifdef STB_IMAGE_STATIC +#define STBIDEF static +#else +#define STBIDEF extern +#endif +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// PRIMARY API - works on images of any type +// + +// +// load image by filename, open file, or memory buffer +// + +typedef struct +{ + int (*read) (void *user,char *data,int size); // fill 'data' with 'size' bytes. return number of bytes actually read + void (*skip) (void *user,int n); // skip the next 'n' bytes, or 'unget' the last -n bytes if negative + int (*eof) (void *user); // returns nonzero if we are at end of file/data +} stbi_io_callbacks; + +//////////////////////////////////// +// +// 8-bits-per-channel interface +// + +STBIDEF stbi_uc *stbi_load_from_memory (stbi_uc const *buffer, int len , int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk , void *user, int *x, int *y, int *channels_in_file, int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF stbi_uc *stbi_load (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); +// for stbi_load_from_file, file pointer is left pointing immediately after image +#endif + +#ifndef STBI_NO_GIF +STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp); +#endif + +#ifdef STBI_WINDOWS_UTF8 +STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif + +//////////////////////////////////// +// +// 16-bits-per-channel interface +// + +STBIDEF stbi_us *stbi_load_16_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF stbi_us *stbi_load_16 (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_us *stbi_load_from_file_16(FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); +#endif + +//////////////////////////////////// +// +// float-per-channel interface +// +#ifndef STBI_NO_LINEAR + STBIDEF float *stbi_loadf_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); + STBIDEF float *stbi_loadf_from_callbacks (stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); + + #ifndef STBI_NO_STDIO + STBIDEF float *stbi_loadf (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); + STBIDEF float *stbi_loadf_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); + #endif +#endif + +#ifndef STBI_NO_HDR + STBIDEF void stbi_hdr_to_ldr_gamma(float gamma); + STBIDEF void stbi_hdr_to_ldr_scale(float scale); +#endif // STBI_NO_HDR + +#ifndef STBI_NO_LINEAR + STBIDEF void stbi_ldr_to_hdr_gamma(float gamma); + STBIDEF void stbi_ldr_to_hdr_scale(float scale); +#endif // STBI_NO_LINEAR + +// stbi_is_hdr is always defined, but always returns false if STBI_NO_HDR +STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user); +STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len); +#ifndef STBI_NO_STDIO +STBIDEF int stbi_is_hdr (char const *filename); +STBIDEF int stbi_is_hdr_from_file(FILE *f); +#endif // STBI_NO_STDIO + + +// get a VERY brief reason for failure +// on most compilers (and ALL modern mainstream compilers) this is threadsafe +STBIDEF const char *stbi_failure_reason (void); + +// free the loaded image -- this is just free() +STBIDEF void stbi_image_free (void *retval_from_stbi_load); + +// get image dimensions & components without fully decoding +STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp); +STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp); +STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len); +STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *clbk, void *user); + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_info (char const *filename, int *x, int *y, int *comp); +STBIDEF int stbi_info_from_file (FILE *f, int *x, int *y, int *comp); +STBIDEF int stbi_is_16_bit (char const *filename); +STBIDEF int stbi_is_16_bit_from_file(FILE *f); +#endif + + + +// for image formats that explicitly notate that they have premultiplied alpha, +// we just return the colors as stored in the file. set this flag to force +// unpremultiplication. results are undefined if the unpremultiply overflow. +STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply); + +// indicate whether we should process iphone images back to canonical format, +// or just pass them through "as-is" +STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert); + +// flip the image vertically, so the first pixel in the output array is the bottom left +STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip); + +// as above, but only applies to images loaded on the thread that calls the function +// this function is only available if your compiler supports thread-local variables; +// calling it will fail to link if your compiler doesn't +STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply); +STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert); +STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip); + +// ZLIB client - used by PNG, available for other purposes + +STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen); +STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header); +STBIDEF char *stbi_zlib_decode_malloc(const char *buffer, int len, int *outlen); +STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); + +STBIDEF char *stbi_zlib_decode_noheader_malloc(const char *buffer, int len, int *outlen); +STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); + + +#ifdef __cplusplus +} +#endif + +// +// +//// end header file ///////////////////////////////////////////////////// +#endif // STBI_INCLUDE_STB_IMAGE_H + +#ifdef STB_IMAGE_IMPLEMENTATION + +#if defined(STBI_ONLY_JPEG) || defined(STBI_ONLY_PNG) || defined(STBI_ONLY_BMP) \ + || defined(STBI_ONLY_TGA) || defined(STBI_ONLY_GIF) || defined(STBI_ONLY_PSD) \ + || defined(STBI_ONLY_HDR) || defined(STBI_ONLY_PIC) || defined(STBI_ONLY_PNM) \ + || defined(STBI_ONLY_ZLIB) + #ifndef STBI_ONLY_JPEG + #define STBI_NO_JPEG + #endif + #ifndef STBI_ONLY_PNG + #define STBI_NO_PNG + #endif + #ifndef STBI_ONLY_BMP + #define STBI_NO_BMP + #endif + #ifndef STBI_ONLY_PSD + #define STBI_NO_PSD + #endif + #ifndef STBI_ONLY_TGA + #define STBI_NO_TGA + #endif + #ifndef STBI_ONLY_GIF + #define STBI_NO_GIF + #endif + #ifndef STBI_ONLY_HDR + #define STBI_NO_HDR + #endif + #ifndef STBI_ONLY_PIC + #define STBI_NO_PIC + #endif + #ifndef STBI_ONLY_PNM + #define STBI_NO_PNM + #endif +#endif + +#if defined(STBI_NO_PNG) && !defined(STBI_SUPPORT_ZLIB) && !defined(STBI_NO_ZLIB) +#define STBI_NO_ZLIB +#endif + + +#include +#include // ptrdiff_t on osx +#include +#include +#include + +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) +#include // ldexp, pow +#endif + +#ifndef STBI_NO_STDIO +#include +#endif + +#ifndef STBI_ASSERT +#include +#define STBI_ASSERT(x) assert(x) +#endif + +#ifdef __cplusplus +#define STBI_EXTERN extern "C" +#else +#define STBI_EXTERN extern +#endif + + +#ifndef _MSC_VER + #ifdef __cplusplus + #define stbi_inline inline + #else + #define stbi_inline + #endif +#else + #define stbi_inline __forceinline +#endif + +#ifndef STBI_NO_THREAD_LOCALS + #if defined(__cplusplus) && __cplusplus >= 201103L + #define STBI_THREAD_LOCAL thread_local + #elif defined(__GNUC__) && __GNUC__ < 5 + #define STBI_THREAD_LOCAL __thread + #elif defined(_MSC_VER) + #define STBI_THREAD_LOCAL __declspec(thread) + #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) + #define STBI_THREAD_LOCAL _Thread_local + #endif + + #ifndef STBI_THREAD_LOCAL + #if defined(__GNUC__) + #define STBI_THREAD_LOCAL __thread + #endif + #endif +#endif + +#if defined(_MSC_VER) || defined(__SYMBIAN32__) +typedef unsigned short stbi__uint16; +typedef signed short stbi__int16; +typedef unsigned int stbi__uint32; +typedef signed int stbi__int32; +#else +#include +typedef uint16_t stbi__uint16; +typedef int16_t stbi__int16; +typedef uint32_t stbi__uint32; +typedef int32_t stbi__int32; +#endif + +// should produce compiler error if size is wrong +typedef unsigned char validate_uint32[sizeof(stbi__uint32)==4 ? 1 : -1]; + +#ifdef _MSC_VER +#define STBI_NOTUSED(v) (void)(v) +#else +#define STBI_NOTUSED(v) (void)sizeof(v) +#endif + +#ifdef _MSC_VER +#define STBI_HAS_LROTL +#endif + +#ifdef STBI_HAS_LROTL + #define stbi_lrot(x,y) _lrotl(x,y) +#else + #define stbi_lrot(x,y) (((x) << (y)) | ((x) >> (-(y) & 31))) +#endif + +#if defined(STBI_MALLOC) && defined(STBI_FREE) && (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED)) +// ok +#elif !defined(STBI_MALLOC) && !defined(STBI_FREE) && !defined(STBI_REALLOC) && !defined(STBI_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBI_MALLOC, STBI_FREE, and STBI_REALLOC (or STBI_REALLOC_SIZED)." +#endif + +#ifndef STBI_MALLOC +#define STBI_MALLOC(sz) malloc(sz) +#define STBI_REALLOC(p,newsz) realloc(p,newsz) +#define STBI_FREE(p) free(p) +#endif + +#ifndef STBI_REALLOC_SIZED +#define STBI_REALLOC_SIZED(p,oldsz,newsz) STBI_REALLOC(p,newsz) +#endif + +// x86/x64 detection +#if defined(__x86_64__) || defined(_M_X64) +#define STBI__X64_TARGET +#elif defined(__i386) || defined(_M_IX86) +#define STBI__X86_TARGET +#endif + +#if defined(__GNUC__) && defined(STBI__X86_TARGET) && !defined(__SSE2__) && !defined(STBI_NO_SIMD) +// gcc doesn't support sse2 intrinsics unless you compile with -msse2, +// which in turn means it gets to use SSE2 everywhere. This is unfortunate, +// but previous attempts to provide the SSE2 functions with runtime +// detection caused numerous issues. The way architecture extensions are +// exposed in GCC/Clang is, sadly, not really suited for one-file libs. +// New behavior: if compiled with -msse2, we use SSE2 without any +// detection; if not, we don't use it at all. +#define STBI_NO_SIMD +#endif + +#if defined(__MINGW32__) && defined(STBI__X86_TARGET) && !defined(STBI_MINGW_ENABLE_SSE2) && !defined(STBI_NO_SIMD) +// Note that __MINGW32__ doesn't actually mean 32-bit, so we have to avoid STBI__X64_TARGET +// +// 32-bit MinGW wants ESP to be 16-byte aligned, but this is not in the +// Windows ABI and VC++ as well as Windows DLLs don't maintain that invariant. +// As a result, enabling SSE2 on 32-bit MinGW is dangerous when not +// simultaneously enabling "-mstackrealign". +// +// See https://github.com/nothings/stb/issues/81 for more information. +// +// So default to no SSE2 on 32-bit MinGW. If you've read this far and added +// -mstackrealign to your build settings, feel free to #define STBI_MINGW_ENABLE_SSE2. +#define STBI_NO_SIMD +#endif + +#if !defined(STBI_NO_SIMD) && (defined(STBI__X86_TARGET) || defined(STBI__X64_TARGET)) +#define STBI_SSE2 +#include + +#ifdef _MSC_VER + +#if _MSC_VER >= 1400 // not VC6 +#include // __cpuid +static int stbi__cpuid3(void) +{ + int info[4]; + __cpuid(info,1); + return info[3]; +} +#else +static int stbi__cpuid3(void) +{ + int res; + __asm { + mov eax,1 + cpuid + mov res,edx + } + return res; +} +#endif + +#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name + +#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) +static int stbi__sse2_available(void) +{ + int info3 = stbi__cpuid3(); + return ((info3 >> 26) & 1) != 0; +} +#endif + +#else // assume GCC-style if not VC++ +#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) + +#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) +static int stbi__sse2_available(void) +{ + // If we're even attempting to compile this on GCC/Clang, that means + // -msse2 is on, which means the compiler is allowed to use SSE2 + // instructions at will, and so are we. + return 1; +} +#endif + +#endif +#endif + +// ARM NEON +#if defined(STBI_NO_SIMD) && defined(STBI_NEON) +#undef STBI_NEON +#endif + +#ifdef STBI_NEON +#include +#ifdef _MSC_VER +#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name +#else +#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) +#endif +#endif + +#ifndef STBI_SIMD_ALIGN +#define STBI_SIMD_ALIGN(type, name) type name +#endif + +#ifndef STBI_MAX_DIMENSIONS +#define STBI_MAX_DIMENSIONS (1 << 24) +#endif + +/////////////////////////////////////////////// +// +// stbi__context struct and start_xxx functions + +// stbi__context structure is our basic context used by all images, so it +// contains all the IO context, plus some basic image information +typedef struct +{ + stbi__uint32 img_x, img_y; + int img_n, img_out_n; + + stbi_io_callbacks io; + void *io_user_data; + + int read_from_callbacks; + int buflen; + stbi_uc buffer_start[128]; + int callback_already_read; + + stbi_uc *img_buffer, *img_buffer_end; + stbi_uc *img_buffer_original, *img_buffer_original_end; +} stbi__context; + + +static void stbi__refill_buffer(stbi__context *s); + +// initialize a memory-decode context +static void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len) +{ + s->io.read = NULL; + s->read_from_callbacks = 0; + s->callback_already_read = 0; + s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer; + s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len; +} + +// initialize a callback-based context +static void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void *user) +{ + s->io = *c; + s->io_user_data = user; + s->buflen = sizeof(s->buffer_start); + s->read_from_callbacks = 1; + s->callback_already_read = 0; + s->img_buffer = s->img_buffer_original = s->buffer_start; + stbi__refill_buffer(s); + s->img_buffer_original_end = s->img_buffer_end; +} + +#ifndef STBI_NO_STDIO + +static int stbi__stdio_read(void *user, char *data, int size) +{ + return (int) fread(data,1,size,(FILE*) user); +} + +static void stbi__stdio_skip(void *user, int n) +{ + int ch; + fseek((FILE*) user, n, SEEK_CUR); + ch = fgetc((FILE*) user); /* have to read a byte to reset feof()'s flag */ + if (ch != EOF) { + ungetc(ch, (FILE *) user); /* push byte back onto stream if valid. */ + } +} + +static int stbi__stdio_eof(void *user) +{ + return feof((FILE*) user) || ferror((FILE *) user); +} + +static stbi_io_callbacks stbi__stdio_callbacks = +{ + stbi__stdio_read, + stbi__stdio_skip, + stbi__stdio_eof, +}; + +static void stbi__start_file(stbi__context *s, FILE *f) +{ + stbi__start_callbacks(s, &stbi__stdio_callbacks, (void *) f); +} + +//static void stop_file(stbi__context *s) { } + +#endif // !STBI_NO_STDIO + +static void stbi__rewind(stbi__context *s) +{ + // conceptually rewind SHOULD rewind to the beginning of the stream, + // but we just rewind to the beginning of the initial buffer, because + // we only use it after doing 'test', which only ever looks at at most 92 bytes + s->img_buffer = s->img_buffer_original; + s->img_buffer_end = s->img_buffer_original_end; +} + +enum +{ + STBI_ORDER_RGB, + STBI_ORDER_BGR +}; + +typedef struct +{ + int bits_per_channel; + int num_channels; + int channel_order; +} stbi__result_info; + +#ifndef STBI_NO_JPEG +static int stbi__jpeg_test(stbi__context *s); +static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PNG +static int stbi__png_test(stbi__context *s); +static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__png_is16(stbi__context *s); +#endif + +#ifndef STBI_NO_BMP +static int stbi__bmp_test(stbi__context *s); +static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_TGA +static int stbi__tga_test(stbi__context *s); +static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PSD +static int stbi__psd_test(stbi__context *s); +static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc); +static int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__psd_is16(stbi__context *s); +#endif + +#ifndef STBI_NO_HDR +static int stbi__hdr_test(stbi__context *s); +static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PIC +static int stbi__pic_test(stbi__context *s); +static void *stbi__pic_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_GIF +static int stbi__gif_test(stbi__context *s); +static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp); +static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PNM +static int stbi__pnm_test(stbi__context *s); +static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__pnm_is16(stbi__context *s); +#endif + +static +#ifdef STBI_THREAD_LOCAL +STBI_THREAD_LOCAL +#endif +const char *stbi__g_failure_reason; + +STBIDEF const char *stbi_failure_reason(void) +{ + return stbi__g_failure_reason; +} + +#ifndef STBI_NO_FAILURE_STRINGS +static int stbi__err(const char *str) +{ + stbi__g_failure_reason = str; + return 0; +} +#endif + +static void *stbi__malloc(size_t size) +{ + return STBI_MALLOC(size); +} + +// stb_image uses ints pervasively, including for offset calculations. +// therefore the largest decoded image size we can support with the +// current code, even on 64-bit targets, is INT_MAX. this is not a +// significant limitation for the intended use case. +// +// we do, however, need to make sure our size calculations don't +// overflow. hence a few helper functions for size calculations that +// multiply integers together, making sure that they're non-negative +// and no overflow occurs. + +// return 1 if the sum is valid, 0 on overflow. +// negative terms are considered invalid. +static int stbi__addsizes_valid(int a, int b) +{ + if (b < 0) return 0; + // now 0 <= b <= INT_MAX, hence also + // 0 <= INT_MAX - b <= INTMAX. + // And "a + b <= INT_MAX" (which might overflow) is the + // same as a <= INT_MAX - b (no overflow) + return a <= INT_MAX - b; +} + +// returns 1 if the product is valid, 0 on overflow. +// negative factors are considered invalid. +static int stbi__mul2sizes_valid(int a, int b) +{ + if (a < 0 || b < 0) return 0; + if (b == 0) return 1; // mul-by-0 is always safe + // portable way to check for no overflows in a*b + return a <= INT_MAX/b; +} + +#if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR) +// returns 1 if "a*b + add" has no negative terms/factors and doesn't overflow +static int stbi__mad2sizes_valid(int a, int b, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a*b, add); +} +#endif + +// returns 1 if "a*b*c + add" has no negative terms/factors and doesn't overflow +static int stbi__mad3sizes_valid(int a, int b, int c, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && + stbi__addsizes_valid(a*b*c, add); +} + +// returns 1 if "a*b*c*d + add" has no negative terms/factors and doesn't overflow +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) +static int stbi__mad4sizes_valid(int a, int b, int c, int d, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && + stbi__mul2sizes_valid(a*b*c, d) && stbi__addsizes_valid(a*b*c*d, add); +} +#endif + +#if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR) +// mallocs with size overflow checking +static void *stbi__malloc_mad2(int a, int b, int add) +{ + if (!stbi__mad2sizes_valid(a, b, add)) return NULL; + return stbi__malloc(a*b + add); +} +#endif + +static void *stbi__malloc_mad3(int a, int b, int c, int add) +{ + if (!stbi__mad3sizes_valid(a, b, c, add)) return NULL; + return stbi__malloc(a*b*c + add); +} + +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) +static void *stbi__malloc_mad4(int a, int b, int c, int d, int add) +{ + if (!stbi__mad4sizes_valid(a, b, c, d, add)) return NULL; + return stbi__malloc(a*b*c*d + add); +} +#endif + +// returns 1 if the sum of two signed ints is valid (between -2^31 and 2^31-1 inclusive), 0 on overflow. +static int stbi__addints_valid(int a, int b) +{ + if ((a >= 0) != (b >= 0)) return 1; // a and b have different signs, so no overflow + if (a < 0 && b < 0) return a >= INT_MIN - b; // same as a + b >= INT_MIN; INT_MIN - b cannot overflow since b < 0. + return a <= INT_MAX - b; +} + +// returns 1 if the product of two ints fits in a signed short, 0 on overflow. +static int stbi__mul2shorts_valid(int a, int b) +{ + if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow + if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid + if (b < 0) return a <= SHRT_MIN / b; // same as a * b >= SHRT_MIN + return a >= SHRT_MIN / b; +} + +// stbi__err - error +// stbi__errpf - error returning pointer to float +// stbi__errpuc - error returning pointer to unsigned char + +#ifdef STBI_NO_FAILURE_STRINGS + #define stbi__err(x,y) 0 +#elif defined(STBI_FAILURE_USERMSG) + #define stbi__err(x,y) stbi__err(y) +#else + #define stbi__err(x,y) stbi__err(x) +#endif + +#define stbi__errpf(x,y) ((float *)(size_t) (stbi__err(x,y)?NULL:NULL)) +#define stbi__errpuc(x,y) ((unsigned char *)(size_t) (stbi__err(x,y)?NULL:NULL)) + +STBIDEF void stbi_image_free(void *retval_from_stbi_load) +{ + STBI_FREE(retval_from_stbi_load); +} + +#ifndef STBI_NO_LINEAR +static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp); +#endif + +#ifndef STBI_NO_HDR +static stbi_uc *stbi__hdr_to_ldr(float *data, int x, int y, int comp); +#endif + +static int stbi__vertically_flip_on_load_global = 0; + +STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip) +{ + stbi__vertically_flip_on_load_global = flag_true_if_should_flip; +} + +#ifndef STBI_THREAD_LOCAL +#define stbi__vertically_flip_on_load stbi__vertically_flip_on_load_global +#else +static STBI_THREAD_LOCAL int stbi__vertically_flip_on_load_local, stbi__vertically_flip_on_load_set; + +STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip) +{ + stbi__vertically_flip_on_load_local = flag_true_if_should_flip; + stbi__vertically_flip_on_load_set = 1; +} + +#define stbi__vertically_flip_on_load (stbi__vertically_flip_on_load_set \ + ? stbi__vertically_flip_on_load_local \ + : stbi__vertically_flip_on_load_global) +#endif // STBI_THREAD_LOCAL + +static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) +{ + memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields + ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed + ri->channel_order = STBI_ORDER_RGB; // all current input & output are this, but this is here so we can add BGR order + ri->num_channels = 0; + + // test the formats with a very explicit header first (at least a FOURCC + // or distinctive magic number first) + #ifndef STBI_NO_PNG + if (stbi__png_test(s)) return stbi__png_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_BMP + if (stbi__bmp_test(s)) return stbi__bmp_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_GIF + if (stbi__gif_test(s)) return stbi__gif_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_PSD + if (stbi__psd_test(s)) return stbi__psd_load(s,x,y,comp,req_comp, ri, bpc); + #else + STBI_NOTUSED(bpc); + #endif + #ifndef STBI_NO_PIC + if (stbi__pic_test(s)) return stbi__pic_load(s,x,y,comp,req_comp, ri); + #endif + + // then the formats that can end up attempting to load with just 1 or 2 + // bytes matching expectations; these are prone to false positives, so + // try them later + #ifndef STBI_NO_JPEG + if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_PNM + if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri); + #endif + + #ifndef STBI_NO_HDR + if (stbi__hdr_test(s)) { + float *hdr = stbi__hdr_load(s, x,y,comp,req_comp, ri); + return stbi__hdr_to_ldr(hdr, *x, *y, req_comp ? req_comp : *comp); + } + #endif + + #ifndef STBI_NO_TGA + // test tga last because it's a crappy test! + if (stbi__tga_test(s)) + return stbi__tga_load(s,x,y,comp,req_comp, ri); + #endif + + return stbi__errpuc("unknown image type", "Image not of any known type, or corrupt"); +} + +static stbi_uc *stbi__convert_16_to_8(stbi__uint16 *orig, int w, int h, int channels) +{ + int i; + int img_len = w * h * channels; + stbi_uc *reduced; + + reduced = (stbi_uc *) stbi__malloc(img_len); + if (reduced == NULL) return stbi__errpuc("outofmem", "Out of memory"); + + for (i = 0; i < img_len; ++i) + reduced[i] = (stbi_uc)((orig[i] >> 8) & 0xFF); // top half of each byte is sufficient approx of 16->8 bit scaling + + STBI_FREE(orig); + return reduced; +} + +static stbi__uint16 *stbi__convert_8_to_16(stbi_uc *orig, int w, int h, int channels) +{ + int i; + int img_len = w * h * channels; + stbi__uint16 *enlarged; + + enlarged = (stbi__uint16 *) stbi__malloc(img_len*2); + if (enlarged == NULL) return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); + + for (i = 0; i < img_len; ++i) + enlarged[i] = (stbi__uint16)((orig[i] << 8) + orig[i]); // replicate to high and low byte, maps 0->0, 255->0xffff + + STBI_FREE(orig); + return enlarged; +} + +static void stbi__vertical_flip(void *image, int w, int h, int bytes_per_pixel) +{ + int row; + size_t bytes_per_row = (size_t)w * bytes_per_pixel; + stbi_uc temp[2048]; + stbi_uc *bytes = (stbi_uc *)image; + + for (row = 0; row < (h>>1); row++) { + stbi_uc *row0 = bytes + row*bytes_per_row; + stbi_uc *row1 = bytes + (h - row - 1)*bytes_per_row; + // swap row0 with row1 + size_t bytes_left = bytes_per_row; + while (bytes_left) { + size_t bytes_copy = (bytes_left < sizeof(temp)) ? bytes_left : sizeof(temp); + memcpy(temp, row0, bytes_copy); + memcpy(row0, row1, bytes_copy); + memcpy(row1, temp, bytes_copy); + row0 += bytes_copy; + row1 += bytes_copy; + bytes_left -= bytes_copy; + } + } +} + +#ifndef STBI_NO_GIF +static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int bytes_per_pixel) +{ + int slice; + int slice_size = w * h * bytes_per_pixel; + + stbi_uc *bytes = (stbi_uc *)image; + for (slice = 0; slice < z; ++slice) { + stbi__vertical_flip(bytes, w, h, bytes_per_pixel); + bytes += slice_size; + } +} +#endif + +static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + stbi__result_info ri; + void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8); + + if (result == NULL) + return NULL; + + // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. + STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); + + if (ri.bits_per_channel != 8) { + result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp); + ri.bits_per_channel = 8; + } + + // @TODO: move stbi__convert_format to here + + if (stbi__vertically_flip_on_load) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi_uc)); + } + + return (unsigned char *) result; +} + +static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + stbi__result_info ri; + void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16); + + if (result == NULL) + return NULL; + + // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. + STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); + + if (ri.bits_per_channel != 16) { + result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp); + ri.bits_per_channel = 16; + } + + // @TODO: move stbi__convert_format16 to here + // @TODO: special case RGB-to-Y (and RGBA-to-YA) for 8-bit-to-16-bit case to keep more precision + + if (stbi__vertically_flip_on_load) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi__uint16)); + } + + return (stbi__uint16 *) result; +} + +#if !defined(STBI_NO_HDR) && !defined(STBI_NO_LINEAR) +static void stbi__float_postprocess(float *result, int *x, int *y, int *comp, int req_comp) +{ + if (stbi__vertically_flip_on_load && result != NULL) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(float)); + } +} +#endif + +#ifndef STBI_NO_STDIO + +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) +STBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); +#endif + +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) +STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbi__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + + +STBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + unsigned char *result; + if (!f) return stbi__errpuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file(f,x,y,comp,req_comp); + fclose(f); + return result; +} + +STBIDEF stbi_uc *stbi_load_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + unsigned char *result; + stbi__context s; + stbi__start_file(&s,f); + result = stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); + if (result) { + // need to 'unget' all the characters in the IO buffer + fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); + } + return result; +} + +STBIDEF stbi__uint16 *stbi_load_from_file_16(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + stbi__uint16 *result; + stbi__context s; + stbi__start_file(&s,f); + result = stbi__load_and_postprocess_16bit(&s,x,y,comp,req_comp); + if (result) { + // need to 'unget' all the characters in the IO buffer + fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); + } + return result; +} + +STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + stbi__uint16 *result; + if (!f) return (stbi_us *) stbi__errpuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file_16(f,x,y,comp,req_comp); + fclose(f); + return result; +} + + +#endif //!STBI_NO_STDIO + +STBIDEF stbi_us *stbi_load_16_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); +} + +STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *)clbk, user); + return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); +} + +STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); +} + +STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); +} + +#ifndef STBI_NO_GIF +STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp) +{ + unsigned char *result; + stbi__context s; + stbi__start_mem(&s,buffer,len); + + result = (unsigned char*) stbi__load_gif_main(&s, delays, x, y, z, comp, req_comp); + if (stbi__vertically_flip_on_load) { + stbi__vertical_flip_slices( result, *x, *y, *z, *comp ); + } + + return result; +} +#endif + +#ifndef STBI_NO_LINEAR +static float *stbi__loadf_main(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + unsigned char *data; + #ifndef STBI_NO_HDR + if (stbi__hdr_test(s)) { + stbi__result_info ri; + float *hdr_data = stbi__hdr_load(s,x,y,comp,req_comp, &ri); + if (hdr_data) + stbi__float_postprocess(hdr_data,x,y,comp,req_comp); + return hdr_data; + } + #endif + data = stbi__load_and_postprocess_8bit(s, x, y, comp, req_comp); + if (data) + return stbi__ldr_to_hdr(data, *x, *y, req_comp ? req_comp : *comp); + return stbi__errpf("unknown image type", "Image not of any known type, or corrupt"); +} + +STBIDEF float *stbi_loadf_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} + +STBIDEF float *stbi_loadf_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} + +#ifndef STBI_NO_STDIO +STBIDEF float *stbi_loadf(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + float *result; + FILE *f = stbi__fopen(filename, "rb"); + if (!f) return stbi__errpf("can't fopen", "Unable to open file"); + result = stbi_loadf_from_file(f,x,y,comp,req_comp); + fclose(f); + return result; +} + +STBIDEF float *stbi_loadf_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_file(&s,f); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} +#endif // !STBI_NO_STDIO + +#endif // !STBI_NO_LINEAR + +// these is-hdr-or-not is defined independent of whether STBI_NO_LINEAR is +// defined, for API simplicity; if STBI_NO_LINEAR is defined, it always +// reports false! + +STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len) +{ + #ifndef STBI_NO_HDR + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__hdr_test(&s); + #else + STBI_NOTUSED(buffer); + STBI_NOTUSED(len); + return 0; + #endif +} + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_is_hdr (char const *filename) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result=0; + if (f) { + result = stbi_is_hdr_from_file(f); + fclose(f); + } + return result; +} + +STBIDEF int stbi_is_hdr_from_file(FILE *f) +{ + #ifndef STBI_NO_HDR + long pos = ftell(f); + int res; + stbi__context s; + stbi__start_file(&s,f); + res = stbi__hdr_test(&s); + fseek(f, pos, SEEK_SET); + return res; + #else + STBI_NOTUSED(f); + return 0; + #endif +} +#endif // !STBI_NO_STDIO + +STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user) +{ + #ifndef STBI_NO_HDR + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__hdr_test(&s); + #else + STBI_NOTUSED(clbk); + STBI_NOTUSED(user); + return 0; + #endif +} + +#ifndef STBI_NO_LINEAR +static float stbi__l2h_gamma=2.2f, stbi__l2h_scale=1.0f; + +STBIDEF void stbi_ldr_to_hdr_gamma(float gamma) { stbi__l2h_gamma = gamma; } +STBIDEF void stbi_ldr_to_hdr_scale(float scale) { stbi__l2h_scale = scale; } +#endif + +static float stbi__h2l_gamma_i=1.0f/2.2f, stbi__h2l_scale_i=1.0f; + +STBIDEF void stbi_hdr_to_ldr_gamma(float gamma) { stbi__h2l_gamma_i = 1/gamma; } +STBIDEF void stbi_hdr_to_ldr_scale(float scale) { stbi__h2l_scale_i = 1/scale; } + + +////////////////////////////////////////////////////////////////////////////// +// +// Common code used by all image loaders +// + +enum +{ + STBI__SCAN_load=0, + STBI__SCAN_type, + STBI__SCAN_header +}; + +static void stbi__refill_buffer(stbi__context *s) +{ + int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); + s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original); + if (n == 0) { + // at end of file, treat same as if from memory, but need to handle case + // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file + s->read_from_callbacks = 0; + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start+1; + *s->img_buffer = 0; + } else { + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start + n; + } +} + +stbi_inline static stbi_uc stbi__get8(stbi__context *s) +{ + if (s->img_buffer < s->img_buffer_end) + return *s->img_buffer++; + if (s->read_from_callbacks) { + stbi__refill_buffer(s); + return *s->img_buffer++; + } + return 0; +} + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_HDR) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +stbi_inline static int stbi__at_eof(stbi__context *s) +{ + if (s->io.read) { + if (!(s->io.eof)(s->io_user_data)) return 0; + // if feof() is true, check if buffer = end + // special case: we've only got the special 0 character at the end + if (s->read_from_callbacks == 0) return 1; + } + + return s->img_buffer >= s->img_buffer_end; +} +#endif + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) +// nothing +#else +static void stbi__skip(stbi__context *s, int n) +{ + if (n == 0) return; // already there! + if (n < 0) { + s->img_buffer = s->img_buffer_end; + return; + } + if (s->io.read) { + int blen = (int) (s->img_buffer_end - s->img_buffer); + if (blen < n) { + s->img_buffer = s->img_buffer_end; + (s->io.skip)(s->io_user_data, n - blen); + return; + } + } + s->img_buffer += n; +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_TGA) && defined(STBI_NO_HDR) && defined(STBI_NO_PNM) +// nothing +#else +static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n) +{ + if (s->io.read) { + int blen = (int) (s->img_buffer_end - s->img_buffer); + if (blen < n) { + int res, count; + + memcpy(buffer, s->img_buffer, blen); + + count = (s->io.read)(s->io_user_data, (char*) buffer + blen, n - blen); + res = (count == (n-blen)); + s->img_buffer = s->img_buffer_end; + return res; + } + } + + if (s->img_buffer+n <= s->img_buffer_end) { + memcpy(buffer, s->img_buffer, n); + s->img_buffer += n; + return 1; + } else + return 0; +} +#endif + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC) +// nothing +#else +static int stbi__get16be(stbi__context *s) +{ + int z = stbi__get8(s); + return (z << 8) + stbi__get8(s); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC) +// nothing +#else +static stbi__uint32 stbi__get32be(stbi__context *s) +{ + stbi__uint32 z = stbi__get16be(s); + return (z << 16) + stbi__get16be(s); +} +#endif + +#if defined(STBI_NO_BMP) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) +// nothing +#else +static int stbi__get16le(stbi__context *s) +{ + int z = stbi__get8(s); + return z + (stbi__get8(s) << 8); +} +#endif + +#ifndef STBI_NO_BMP +static stbi__uint32 stbi__get32le(stbi__context *s) +{ + stbi__uint32 z = stbi__get16le(s); + z += (stbi__uint32)stbi__get16le(s) << 16; + return z; +} +#endif + +#define STBI__BYTECAST(x) ((stbi_uc) ((x) & 255)) // truncate int to byte without warnings + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +////////////////////////////////////////////////////////////////////////////// +// +// generic converter from built-in img_n to req_comp +// individual types do this automatically as much as possible (e.g. jpeg +// does all cases internally since it needs to colorspace convert anyway, +// and it never has alpha, so very few cases ). png can automatically +// interleave an alpha=255 channel, but falls back to this for other cases +// +// assume data buffer is malloced, so malloc a new one and free that one +// only failure mode is malloc failing + +static stbi_uc stbi__compute_y(int r, int g, int b) +{ + return (stbi_uc) (((r*77) + (g*150) + (29*b)) >> 8); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int req_comp, unsigned int x, unsigned int y) +{ + int i,j; + unsigned char *good; + + if (req_comp == img_n) return data; + STBI_ASSERT(req_comp >= 1 && req_comp <= 4); + + good = (unsigned char *) stbi__malloc_mad3(req_comp, x, y, 0); + if (good == NULL) { + STBI_FREE(data); + return stbi__errpuc("outofmem", "Out of memory"); + } + + for (j=0; j < (int) y; ++j) { + unsigned char *src = data + j * x * img_n ; + unsigned char *dest = good + j * x * req_comp; + + #define STBI__COMBO(a,b) ((a)*8+(b)) + #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp components; + // avoid switch per pixel, so use switch per scanline and massive macros + switch (STBI__COMBO(img_n, req_comp)) { + STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=255; } break; + STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=255; } break; + STBI__CASE(2,1) { dest[0]=src[0]; } break; + STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; + STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=255; } break; + STBI__CASE(3,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; + STBI__CASE(3,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = 255; } break; + STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; + STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break; + STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; + default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc("unsupported", "Unsupported format conversion"); + } + #undef STBI__CASE + } + + STBI_FREE(data); + return good; +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) +// nothing +#else +static stbi__uint16 stbi__compute_y_16(int r, int g, int b) +{ + return (stbi__uint16) (((r*77) + (g*150) + (29*b)) >> 8); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) +// nothing +#else +static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int req_comp, unsigned int x, unsigned int y) +{ + int i,j; + stbi__uint16 *good; + + if (req_comp == img_n) return data; + STBI_ASSERT(req_comp >= 1 && req_comp <= 4); + + good = (stbi__uint16 *) stbi__malloc(req_comp * x * y * 2); + if (good == NULL) { + STBI_FREE(data); + return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); + } + + for (j=0; j < (int) y; ++j) { + stbi__uint16 *src = data + j * x * img_n ; + stbi__uint16 *dest = good + j * x * req_comp; + + #define STBI__COMBO(a,b) ((a)*8+(b)) + #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp components; + // avoid switch per pixel, so use switch per scanline and massive macros + switch (STBI__COMBO(img_n, req_comp)) { + STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=0xffff; } break; + STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=0xffff; } break; + STBI__CASE(2,1) { dest[0]=src[0]; } break; + STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; + STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=0xffff; } break; + STBI__CASE(3,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; + STBI__CASE(3,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = 0xffff; } break; + STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; + STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break; + STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; + default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc("unsupported", "Unsupported format conversion"); + } + #undef STBI__CASE + } + + STBI_FREE(data); + return good; +} +#endif + +#ifndef STBI_NO_LINEAR +static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp) +{ + int i,k,n; + float *output; + if (!data) return NULL; + output = (float *) stbi__malloc_mad4(x, y, comp, sizeof(float), 0); + if (output == NULL) { STBI_FREE(data); return stbi__errpf("outofmem", "Out of memory"); } + // compute number of non-alpha components + if (comp & 1) n = comp; else n = comp-1; + for (i=0; i < x*y; ++i) { + for (k=0; k < n; ++k) { + output[i*comp + k] = (float) (pow(data[i*comp+k]/255.0f, stbi__l2h_gamma) * stbi__l2h_scale); + } + } + if (n < comp) { + for (i=0; i < x*y; ++i) { + output[i*comp + n] = data[i*comp + n]/255.0f; + } + } + STBI_FREE(data); + return output; +} +#endif + +#ifndef STBI_NO_HDR +#define stbi__float2int(x) ((int) (x)) +static stbi_uc *stbi__hdr_to_ldr(float *data, int x, int y, int comp) +{ + int i,k,n; + stbi_uc *output; + if (!data) return NULL; + output = (stbi_uc *) stbi__malloc_mad3(x, y, comp, 0); + if (output == NULL) { STBI_FREE(data); return stbi__errpuc("outofmem", "Out of memory"); } + // compute number of non-alpha components + if (comp & 1) n = comp; else n = comp-1; + for (i=0; i < x*y; ++i) { + for (k=0; k < n; ++k) { + float z = (float) pow(data[i*comp+k]*stbi__h2l_scale_i, stbi__h2l_gamma_i) * 255 + 0.5f; + if (z < 0) z = 0; + if (z > 255) z = 255; + output[i*comp + k] = (stbi_uc) stbi__float2int(z); + } + if (k < comp) { + float z = data[i*comp+k] * 255 + 0.5f; + if (z < 0) z = 0; + if (z > 255) z = 255; + output[i*comp + k] = (stbi_uc) stbi__float2int(z); + } + } + STBI_FREE(data); + return output; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// "baseline" JPEG/JFIF decoder +// +// simple implementation +// - doesn't support delayed output of y-dimension +// - simple interface (only one output format: 8-bit interleaved RGB) +// - doesn't try to recover corrupt jpegs +// - doesn't allow partial loading, loading multiple at once +// - still fast on x86 (copying globals into locals doesn't help x86) +// - allocates lots of intermediate memory (full size of all components) +// - non-interleaved case requires this anyway +// - allows good upsampling (see next) +// high-quality +// - upsampled channels are bilinearly interpolated, even across blocks +// - quality integer IDCT derived from IJG's 'slow' +// performance +// - fast huffman; reasonable integer IDCT +// - some SIMD kernels for common paths on targets with SSE2/NEON +// - uses a lot of intermediate memory, could cache poorly + +#ifndef STBI_NO_JPEG + +// huffman decoding acceleration +#define FAST_BITS 9 // larger handles more cases; smaller stomps less cache + +typedef struct +{ + stbi_uc fast[1 << FAST_BITS]; + // weirdly, repacking this into AoS is a 10% speed loss, instead of a win + stbi__uint16 code[256]; + stbi_uc values[256]; + stbi_uc size[257]; + unsigned int maxcode[18]; + int delta[17]; // old 'firstsymbol' - old 'firstcode' +} stbi__huffman; + +typedef struct +{ + stbi__context *s; + stbi__huffman huff_dc[4]; + stbi__huffman huff_ac[4]; + stbi__uint16 dequant[4][64]; + stbi__int16 fast_ac[4][1 << FAST_BITS]; + +// sizes for components, interleaved MCUs + int img_h_max, img_v_max; + int img_mcu_x, img_mcu_y; + int img_mcu_w, img_mcu_h; + +// definition of jpeg image component + struct + { + int id; + int h,v; + int tq; + int hd,ha; + int dc_pred; + + int x,y,w2,h2; + stbi_uc *data; + void *raw_data, *raw_coeff; + stbi_uc *linebuf; + short *coeff; // progressive only + int coeff_w, coeff_h; // number of 8x8 coefficient blocks + } img_comp[4]; + + stbi__uint32 code_buffer; // jpeg entropy-coded buffer + int code_bits; // number of valid bits + unsigned char marker; // marker seen while filling entropy buffer + int nomore; // flag if we saw a marker so must stop + + int progressive; + int spec_start; + int spec_end; + int succ_high; + int succ_low; + int eob_run; + int jfif; + int app14_color_transform; // Adobe APP14 tag + int rgb; + + int scan_n, order[4]; + int restart_interval, todo; + +// kernels + void (*idct_block_kernel)(stbi_uc *out, int out_stride, short data[64]); + void (*YCbCr_to_RGB_kernel)(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step); + stbi_uc *(*resample_row_hv_2_kernel)(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs); +} stbi__jpeg; + +static int stbi__build_huffman(stbi__huffman *h, int *count) +{ + int i,j,k=0; + unsigned int code; + // build size list for each symbol (from JPEG spec) + for (i=0; i < 16; ++i) { + for (j=0; j < count[i]; ++j) { + h->size[k++] = (stbi_uc) (i+1); + if(k >= 257) return stbi__err("bad size list","Corrupt JPEG"); + } + } + h->size[k] = 0; + + // compute actual symbols (from jpeg spec) + code = 0; + k = 0; + for(j=1; j <= 16; ++j) { + // compute delta to add to code to compute symbol id + h->delta[j] = k - code; + if (h->size[k] == j) { + while (h->size[k] == j) + h->code[k++] = (stbi__uint16) (code++); + if (code-1 >= (1u << j)) return stbi__err("bad code lengths","Corrupt JPEG"); + } + // compute largest code + 1 for this size, preshifted as needed later + h->maxcode[j] = code << (16-j); + code <<= 1; + } + h->maxcode[j] = 0xffffffff; + + // build non-spec acceleration table; 255 is flag for not-accelerated + memset(h->fast, 255, 1 << FAST_BITS); + for (i=0; i < k; ++i) { + int s = h->size[i]; + if (s <= FAST_BITS) { + int c = h->code[i] << (FAST_BITS-s); + int m = 1 << (FAST_BITS-s); + for (j=0; j < m; ++j) { + h->fast[c+j] = (stbi_uc) i; + } + } + } + return 1; +} + +// build a table that decodes both magnitude and value of small ACs in +// one go. +static void stbi__build_fast_ac(stbi__int16 *fast_ac, stbi__huffman *h) +{ + int i; + for (i=0; i < (1 << FAST_BITS); ++i) { + stbi_uc fast = h->fast[i]; + fast_ac[i] = 0; + if (fast < 255) { + int rs = h->values[fast]; + int run = (rs >> 4) & 15; + int magbits = rs & 15; + int len = h->size[fast]; + + if (magbits && len + magbits <= FAST_BITS) { + // magnitude code followed by receive_extend code + int k = ((i << len) & ((1 << FAST_BITS) - 1)) >> (FAST_BITS - magbits); + int m = 1 << (magbits - 1); + if (k < m) k += (~0U << magbits) + 1; + // if the result is small enough, we can fit it in fast_ac table + if (k >= -128 && k <= 127) + fast_ac[i] = (stbi__int16) ((k * 256) + (run * 16) + (len + magbits)); + } + } + } +} + +static void stbi__grow_buffer_unsafe(stbi__jpeg *j) +{ + do { + unsigned int b = j->nomore ? 0 : stbi__get8(j->s); + if (b == 0xff) { + int c = stbi__get8(j->s); + while (c == 0xff) c = stbi__get8(j->s); // consume fill bytes + if (c != 0) { + j->marker = (unsigned char) c; + j->nomore = 1; + return; + } + } + j->code_buffer |= b << (24 - j->code_bits); + j->code_bits += 8; + } while (j->code_bits <= 24); +} + +// (1 << n) - 1 +static const stbi__uint32 stbi__bmask[17]={0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535}; + +// decode a jpeg huffman value from the bitstream +stbi_inline static int stbi__jpeg_huff_decode(stbi__jpeg *j, stbi__huffman *h) +{ + unsigned int temp; + int c,k; + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + + // look at the top FAST_BITS and determine what symbol ID it is, + // if the code is <= FAST_BITS + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + k = h->fast[c]; + if (k < 255) { + int s = h->size[k]; + if (s > j->code_bits) + return -1; + j->code_buffer <<= s; + j->code_bits -= s; + return h->values[k]; + } + + // naive test is to shift the code_buffer down so k bits are + // valid, then test against maxcode. To speed this up, we've + // preshifted maxcode left so that it has (16-k) 0s at the + // end; in other words, regardless of the number of bits, it + // wants to be compared against something shifted to have 16; + // that way we don't need to shift inside the loop. + temp = j->code_buffer >> 16; + for (k=FAST_BITS+1 ; ; ++k) + if (temp < h->maxcode[k]) + break; + if (k == 17) { + // error! code not found + j->code_bits -= 16; + return -1; + } + + if (k > j->code_bits) + return -1; + + // convert the huffman code to the symbol id + c = ((j->code_buffer >> (32 - k)) & stbi__bmask[k]) + h->delta[k]; + if(c < 0 || c >= 256) // symbol id out of bounds! + return -1; + STBI_ASSERT((((j->code_buffer) >> (32 - h->size[c])) & stbi__bmask[h->size[c]]) == h->code[c]); + + // convert the id to a symbol + j->code_bits -= k; + j->code_buffer <<= k; + return h->values[c]; +} + +// bias[n] = (-1<code_bits < n) stbi__grow_buffer_unsafe(j); + if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing + + sgn = j->code_buffer >> 31; // sign bit always in MSB; 0 if MSB clear (positive), 1 if MSB set (negative) + k = stbi_lrot(j->code_buffer, n); + j->code_buffer = k & ~stbi__bmask[n]; + k &= stbi__bmask[n]; + j->code_bits -= n; + return k + (stbi__jbias[n] & (sgn - 1)); +} + +// get some unsigned bits +stbi_inline static int stbi__jpeg_get_bits(stbi__jpeg *j, int n) +{ + unsigned int k; + if (j->code_bits < n) stbi__grow_buffer_unsafe(j); + if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing + k = stbi_lrot(j->code_buffer, n); + j->code_buffer = k & ~stbi__bmask[n]; + k &= stbi__bmask[n]; + j->code_bits -= n; + return k; +} + +stbi_inline static int stbi__jpeg_get_bit(stbi__jpeg *j) +{ + unsigned int k; + if (j->code_bits < 1) stbi__grow_buffer_unsafe(j); + if (j->code_bits < 1) return 0; // ran out of bits from stream, return 0s intead of continuing + k = j->code_buffer; + j->code_buffer <<= 1; + --j->code_bits; + return k & 0x80000000; +} + +// given a value that's at position X in the zigzag stream, +// where does it appear in the 8x8 matrix coded as row-major? +static const stbi_uc stbi__jpeg_dezigzag[64+15] = +{ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + // let corrupt input sample past end + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63 +}; + +// decode one 64-entry block-- +static int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman *hdc, stbi__huffman *hac, stbi__int16 *fac, int b, stbi__uint16 *dequant) +{ + int diff,dc,k; + int t; + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + t = stbi__jpeg_huff_decode(j, hdc); + if (t < 0 || t > 15) return stbi__err("bad huffman code","Corrupt JPEG"); + + // 0 all the ac values now so we can do it 32-bits at a time + memset(data,0,64*sizeof(data[0])); + + diff = t ? stbi__extend_receive(j, t) : 0; + if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta","Corrupt JPEG"); + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + if (!stbi__mul2shorts_valid(dc, dequant[0])) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + data[0] = (short) (dc * dequant[0]); + + // decode AC components, see JPEG spec + k = 1; + do { + unsigned int zig; + int c,r,s; + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + r = fac[c]; + if (r) { // fast-AC path + k += (r >> 4) & 15; // run + s = r & 15; // combined length + if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); + j->code_buffer <<= s; + j->code_bits -= s; + // decode into unzigzag'd location + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) ((r >> 8) * dequant[zig]); + } else { + int rs = stbi__jpeg_huff_decode(j, hac); + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (rs != 0xf0) break; // end block + k += 16; + } else { + k += r; + // decode into unzigzag'd location + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) (stbi__extend_receive(j,s) * dequant[zig]); + } + } + } while (k < 64); + return 1; +} + +static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__huffman *hdc, int b) +{ + int diff,dc; + int t; + if (j->spec_end != 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + + if (j->succ_high == 0) { + // first scan for DC coefficient, must be first + memset(data,0,64*sizeof(data[0])); // 0 all the ac values now + t = stbi__jpeg_huff_decode(j, hdc); + if (t < 0 || t > 15) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + diff = t ? stbi__extend_receive(j, t) : 0; + + if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta", "Corrupt JPEG"); + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + if (!stbi__mul2shorts_valid(dc, 1 << j->succ_low)) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + data[0] = (short) (dc * (1 << j->succ_low)); + } else { + // refinement scan for DC coefficient + if (stbi__jpeg_get_bit(j)) + data[0] += (short) (1 << j->succ_low); + } + return 1; +} + +// @OPTIMIZE: store non-zigzagged during the decode passes, +// and only de-zigzag when dequantizing +static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__huffman *hac, stbi__int16 *fac) +{ + int k; + if (j->spec_start == 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + + if (j->succ_high == 0) { + int shift = j->succ_low; + + if (j->eob_run) { + --j->eob_run; + return 1; + } + + k = j->spec_start; + do { + unsigned int zig; + int c,r,s; + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + r = fac[c]; + if (r) { // fast-AC path + k += (r >> 4) & 15; // run + s = r & 15; // combined length + if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); + j->code_buffer <<= s; + j->code_bits -= s; + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) ((r >> 8) * (1 << shift)); + } else { + int rs = stbi__jpeg_huff_decode(j, hac); + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + j->eob_run = (1 << r); + if (r) + j->eob_run += stbi__jpeg_get_bits(j, r); + --j->eob_run; + break; + } + k += 16; + } else { + k += r; + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) (stbi__extend_receive(j,s) * (1 << shift)); + } + } + } while (k <= j->spec_end); + } else { + // refinement scan for these AC coefficients + + short bit = (short) (1 << j->succ_low); + + if (j->eob_run) { + --j->eob_run; + for (k = j->spec_start; k <= j->spec_end; ++k) { + short *p = &data[stbi__jpeg_dezigzag[k]]; + if (*p != 0) + if (stbi__jpeg_get_bit(j)) + if ((*p & bit)==0) { + if (*p > 0) + *p += bit; + else + *p -= bit; + } + } + } else { + k = j->spec_start; + do { + int r,s; + int rs = stbi__jpeg_huff_decode(j, hac); // @OPTIMIZE see if we can use the fast path here, advance-by-r is so slow, eh + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + j->eob_run = (1 << r) - 1; + if (r) + j->eob_run += stbi__jpeg_get_bits(j, r); + r = 64; // force end of block + } else { + // r=15 s=0 should write 16 0s, so we just do + // a run of 15 0s and then write s (which is 0), + // so we don't have to do anything special here + } + } else { + if (s != 1) return stbi__err("bad huffman code", "Corrupt JPEG"); + // sign bit + if (stbi__jpeg_get_bit(j)) + s = bit; + else + s = -bit; + } + + // advance by r + while (k <= j->spec_end) { + short *p = &data[stbi__jpeg_dezigzag[k++]]; + if (*p != 0) { + if (stbi__jpeg_get_bit(j)) + if ((*p & bit)==0) { + if (*p > 0) + *p += bit; + else + *p -= bit; + } + } else { + if (r == 0) { + *p = (short) s; + break; + } + --r; + } + } + } while (k <= j->spec_end); + } + } + return 1; +} + +// take a -128..127 value and stbi__clamp it and convert to 0..255 +stbi_inline static stbi_uc stbi__clamp(int x) +{ + // trick to use a single test to catch both cases + if ((unsigned int) x > 255) { + if (x < 0) return 0; + if (x > 255) return 255; + } + return (stbi_uc) x; +} + +#define stbi__f2f(x) ((int) (((x) * 4096 + 0.5))) +#define stbi__fsh(x) ((x) * 4096) + +// derived from jidctint -- DCT_ISLOW +#define STBI__IDCT_1D(s0,s1,s2,s3,s4,s5,s6,s7) \ + int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; \ + p2 = s2; \ + p3 = s6; \ + p1 = (p2+p3) * stbi__f2f(0.5411961f); \ + t2 = p1 + p3*stbi__f2f(-1.847759065f); \ + t3 = p1 + p2*stbi__f2f( 0.765366865f); \ + p2 = s0; \ + p3 = s4; \ + t0 = stbi__fsh(p2+p3); \ + t1 = stbi__fsh(p2-p3); \ + x0 = t0+t3; \ + x3 = t0-t3; \ + x1 = t1+t2; \ + x2 = t1-t2; \ + t0 = s7; \ + t1 = s5; \ + t2 = s3; \ + t3 = s1; \ + p3 = t0+t2; \ + p4 = t1+t3; \ + p1 = t0+t3; \ + p2 = t1+t2; \ + p5 = (p3+p4)*stbi__f2f( 1.175875602f); \ + t0 = t0*stbi__f2f( 0.298631336f); \ + t1 = t1*stbi__f2f( 2.053119869f); \ + t2 = t2*stbi__f2f( 3.072711026f); \ + t3 = t3*stbi__f2f( 1.501321110f); \ + p1 = p5 + p1*stbi__f2f(-0.899976223f); \ + p2 = p5 + p2*stbi__f2f(-2.562915447f); \ + p3 = p3*stbi__f2f(-1.961570560f); \ + p4 = p4*stbi__f2f(-0.390180644f); \ + t3 += p1+p4; \ + t2 += p2+p3; \ + t1 += p2+p4; \ + t0 += p1+p3; + +static void stbi__idct_block(stbi_uc *out, int out_stride, short data[64]) +{ + int i,val[64],*v=val; + stbi_uc *o; + short *d = data; + + // columns + for (i=0; i < 8; ++i,++d, ++v) { + // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing + if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0 + && d[40]==0 && d[48]==0 && d[56]==0) { + // no shortcut 0 seconds + // (1|2|3|4|5|6|7)==0 0 seconds + // all separate -0.047 seconds + // 1 && 2|3 && 4|5 && 6|7: -0.047 seconds + int dcterm = d[0]*4; + v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm; + } else { + STBI__IDCT_1D(d[ 0],d[ 8],d[16],d[24],d[32],d[40],d[48],d[56]) + // constants scaled things up by 1<<12; let's bring them back + // down, but keep 2 extra bits of precision + x0 += 512; x1 += 512; x2 += 512; x3 += 512; + v[ 0] = (x0+t3) >> 10; + v[56] = (x0-t3) >> 10; + v[ 8] = (x1+t2) >> 10; + v[48] = (x1-t2) >> 10; + v[16] = (x2+t1) >> 10; + v[40] = (x2-t1) >> 10; + v[24] = (x3+t0) >> 10; + v[32] = (x3-t0) >> 10; + } + } + + for (i=0, v=val, o=out; i < 8; ++i,v+=8,o+=out_stride) { + // no fast case since the first 1D IDCT spread components out + STBI__IDCT_1D(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7]) + // constants scaled things up by 1<<12, plus we had 1<<2 from first + // loop, plus horizontal and vertical each scale by sqrt(8) so together + // we've got an extra 1<<3, so 1<<17 total we need to remove. + // so we want to round that, which means adding 0.5 * 1<<17, + // aka 65536. Also, we'll end up with -128 to 127 that we want + // to encode as 0..255 by adding 128, so we'll add that before the shift + x0 += 65536 + (128<<17); + x1 += 65536 + (128<<17); + x2 += 65536 + (128<<17); + x3 += 65536 + (128<<17); + // tried computing the shifts into temps, or'ing the temps to see + // if any were out of range, but that was slower + o[0] = stbi__clamp((x0+t3) >> 17); + o[7] = stbi__clamp((x0-t3) >> 17); + o[1] = stbi__clamp((x1+t2) >> 17); + o[6] = stbi__clamp((x1-t2) >> 17); + o[2] = stbi__clamp((x2+t1) >> 17); + o[5] = stbi__clamp((x2-t1) >> 17); + o[3] = stbi__clamp((x3+t0) >> 17); + o[4] = stbi__clamp((x3-t0) >> 17); + } +} + +#ifdef STBI_SSE2 +// sse2 integer IDCT. not the fastest possible implementation but it +// produces bit-identical results to the generic C version so it's +// fully "transparent". +static void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64]) +{ + // This is constructed to match our regular (generic) integer IDCT exactly. + __m128i row0, row1, row2, row3, row4, row5, row6, row7; + __m128i tmp; + + // dot product constant: even elems=x, odd elems=y + #define dct_const(x,y) _mm_setr_epi16((x),(y),(x),(y),(x),(y),(x),(y)) + + // out(0) = c0[even]*x + c0[odd]*y (c0, x, y 16-bit, out 32-bit) + // out(1) = c1[even]*x + c1[odd]*y + #define dct_rot(out0,out1, x,y,c0,c1) \ + __m128i c0##lo = _mm_unpacklo_epi16((x),(y)); \ + __m128i c0##hi = _mm_unpackhi_epi16((x),(y)); \ + __m128i out0##_l = _mm_madd_epi16(c0##lo, c0); \ + __m128i out0##_h = _mm_madd_epi16(c0##hi, c0); \ + __m128i out1##_l = _mm_madd_epi16(c0##lo, c1); \ + __m128i out1##_h = _mm_madd_epi16(c0##hi, c1) + + // out = in << 12 (in 16-bit, out 32-bit) + #define dct_widen(out, in) \ + __m128i out##_l = _mm_srai_epi32(_mm_unpacklo_epi16(_mm_setzero_si128(), (in)), 4); \ + __m128i out##_h = _mm_srai_epi32(_mm_unpackhi_epi16(_mm_setzero_si128(), (in)), 4) + + // wide add + #define dct_wadd(out, a, b) \ + __m128i out##_l = _mm_add_epi32(a##_l, b##_l); \ + __m128i out##_h = _mm_add_epi32(a##_h, b##_h) + + // wide sub + #define dct_wsub(out, a, b) \ + __m128i out##_l = _mm_sub_epi32(a##_l, b##_l); \ + __m128i out##_h = _mm_sub_epi32(a##_h, b##_h) + + // butterfly a/b, add bias, then shift by "s" and pack + #define dct_bfly32o(out0, out1, a,b,bias,s) \ + { \ + __m128i abiased_l = _mm_add_epi32(a##_l, bias); \ + __m128i abiased_h = _mm_add_epi32(a##_h, bias); \ + dct_wadd(sum, abiased, b); \ + dct_wsub(dif, abiased, b); \ + out0 = _mm_packs_epi32(_mm_srai_epi32(sum_l, s), _mm_srai_epi32(sum_h, s)); \ + out1 = _mm_packs_epi32(_mm_srai_epi32(dif_l, s), _mm_srai_epi32(dif_h, s)); \ + } + + // 8-bit interleave step (for transposes) + #define dct_interleave8(a, b) \ + tmp = a; \ + a = _mm_unpacklo_epi8(a, b); \ + b = _mm_unpackhi_epi8(tmp, b) + + // 16-bit interleave step (for transposes) + #define dct_interleave16(a, b) \ + tmp = a; \ + a = _mm_unpacklo_epi16(a, b); \ + b = _mm_unpackhi_epi16(tmp, b) + + #define dct_pass(bias,shift) \ + { \ + /* even part */ \ + dct_rot(t2e,t3e, row2,row6, rot0_0,rot0_1); \ + __m128i sum04 = _mm_add_epi16(row0, row4); \ + __m128i dif04 = _mm_sub_epi16(row0, row4); \ + dct_widen(t0e, sum04); \ + dct_widen(t1e, dif04); \ + dct_wadd(x0, t0e, t3e); \ + dct_wsub(x3, t0e, t3e); \ + dct_wadd(x1, t1e, t2e); \ + dct_wsub(x2, t1e, t2e); \ + /* odd part */ \ + dct_rot(y0o,y2o, row7,row3, rot2_0,rot2_1); \ + dct_rot(y1o,y3o, row5,row1, rot3_0,rot3_1); \ + __m128i sum17 = _mm_add_epi16(row1, row7); \ + __m128i sum35 = _mm_add_epi16(row3, row5); \ + dct_rot(y4o,y5o, sum17,sum35, rot1_0,rot1_1); \ + dct_wadd(x4, y0o, y4o); \ + dct_wadd(x5, y1o, y5o); \ + dct_wadd(x6, y2o, y5o); \ + dct_wadd(x7, y3o, y4o); \ + dct_bfly32o(row0,row7, x0,x7,bias,shift); \ + dct_bfly32o(row1,row6, x1,x6,bias,shift); \ + dct_bfly32o(row2,row5, x2,x5,bias,shift); \ + dct_bfly32o(row3,row4, x3,x4,bias,shift); \ + } + + __m128i rot0_0 = dct_const(stbi__f2f(0.5411961f), stbi__f2f(0.5411961f) + stbi__f2f(-1.847759065f)); + __m128i rot0_1 = dct_const(stbi__f2f(0.5411961f) + stbi__f2f( 0.765366865f), stbi__f2f(0.5411961f)); + __m128i rot1_0 = dct_const(stbi__f2f(1.175875602f) + stbi__f2f(-0.899976223f), stbi__f2f(1.175875602f)); + __m128i rot1_1 = dct_const(stbi__f2f(1.175875602f), stbi__f2f(1.175875602f) + stbi__f2f(-2.562915447f)); + __m128i rot2_0 = dct_const(stbi__f2f(-1.961570560f) + stbi__f2f( 0.298631336f), stbi__f2f(-1.961570560f)); + __m128i rot2_1 = dct_const(stbi__f2f(-1.961570560f), stbi__f2f(-1.961570560f) + stbi__f2f( 3.072711026f)); + __m128i rot3_0 = dct_const(stbi__f2f(-0.390180644f) + stbi__f2f( 2.053119869f), stbi__f2f(-0.390180644f)); + __m128i rot3_1 = dct_const(stbi__f2f(-0.390180644f), stbi__f2f(-0.390180644f) + stbi__f2f( 1.501321110f)); + + // rounding biases in column/row passes, see stbi__idct_block for explanation. + __m128i bias_0 = _mm_set1_epi32(512); + __m128i bias_1 = _mm_set1_epi32(65536 + (128<<17)); + + // load + row0 = _mm_load_si128((const __m128i *) (data + 0*8)); + row1 = _mm_load_si128((const __m128i *) (data + 1*8)); + row2 = _mm_load_si128((const __m128i *) (data + 2*8)); + row3 = _mm_load_si128((const __m128i *) (data + 3*8)); + row4 = _mm_load_si128((const __m128i *) (data + 4*8)); + row5 = _mm_load_si128((const __m128i *) (data + 5*8)); + row6 = _mm_load_si128((const __m128i *) (data + 6*8)); + row7 = _mm_load_si128((const __m128i *) (data + 7*8)); + + // column pass + dct_pass(bias_0, 10); + + { + // 16bit 8x8 transpose pass 1 + dct_interleave16(row0, row4); + dct_interleave16(row1, row5); + dct_interleave16(row2, row6); + dct_interleave16(row3, row7); + + // transpose pass 2 + dct_interleave16(row0, row2); + dct_interleave16(row1, row3); + dct_interleave16(row4, row6); + dct_interleave16(row5, row7); + + // transpose pass 3 + dct_interleave16(row0, row1); + dct_interleave16(row2, row3); + dct_interleave16(row4, row5); + dct_interleave16(row6, row7); + } + + // row pass + dct_pass(bias_1, 17); + + { + // pack + __m128i p0 = _mm_packus_epi16(row0, row1); // a0a1a2a3...a7b0b1b2b3...b7 + __m128i p1 = _mm_packus_epi16(row2, row3); + __m128i p2 = _mm_packus_epi16(row4, row5); + __m128i p3 = _mm_packus_epi16(row6, row7); + + // 8bit 8x8 transpose pass 1 + dct_interleave8(p0, p2); // a0e0a1e1... + dct_interleave8(p1, p3); // c0g0c1g1... + + // transpose pass 2 + dct_interleave8(p0, p1); // a0c0e0g0... + dct_interleave8(p2, p3); // b0d0f0h0... + + // transpose pass 3 + dct_interleave8(p0, p2); // a0b0c0d0... + dct_interleave8(p1, p3); // a4b4c4d4... + + // store + _mm_storel_epi64((__m128i *) out, p0); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p0, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p2); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p2, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p1); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p1, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p3); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p3, 0x4e)); + } + +#undef dct_const +#undef dct_rot +#undef dct_widen +#undef dct_wadd +#undef dct_wsub +#undef dct_bfly32o +#undef dct_interleave8 +#undef dct_interleave16 +#undef dct_pass +} + +#endif // STBI_SSE2 + +#ifdef STBI_NEON + +// NEON integer IDCT. should produce bit-identical +// results to the generic C version. +static void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64]) +{ + int16x8_t row0, row1, row2, row3, row4, row5, row6, row7; + + int16x4_t rot0_0 = vdup_n_s16(stbi__f2f(0.5411961f)); + int16x4_t rot0_1 = vdup_n_s16(stbi__f2f(-1.847759065f)); + int16x4_t rot0_2 = vdup_n_s16(stbi__f2f( 0.765366865f)); + int16x4_t rot1_0 = vdup_n_s16(stbi__f2f( 1.175875602f)); + int16x4_t rot1_1 = vdup_n_s16(stbi__f2f(-0.899976223f)); + int16x4_t rot1_2 = vdup_n_s16(stbi__f2f(-2.562915447f)); + int16x4_t rot2_0 = vdup_n_s16(stbi__f2f(-1.961570560f)); + int16x4_t rot2_1 = vdup_n_s16(stbi__f2f(-0.390180644f)); + int16x4_t rot3_0 = vdup_n_s16(stbi__f2f( 0.298631336f)); + int16x4_t rot3_1 = vdup_n_s16(stbi__f2f( 2.053119869f)); + int16x4_t rot3_2 = vdup_n_s16(stbi__f2f( 3.072711026f)); + int16x4_t rot3_3 = vdup_n_s16(stbi__f2f( 1.501321110f)); + +#define dct_long_mul(out, inq, coeff) \ + int32x4_t out##_l = vmull_s16(vget_low_s16(inq), coeff); \ + int32x4_t out##_h = vmull_s16(vget_high_s16(inq), coeff) + +#define dct_long_mac(out, acc, inq, coeff) \ + int32x4_t out##_l = vmlal_s16(acc##_l, vget_low_s16(inq), coeff); \ + int32x4_t out##_h = vmlal_s16(acc##_h, vget_high_s16(inq), coeff) + +#define dct_widen(out, inq) \ + int32x4_t out##_l = vshll_n_s16(vget_low_s16(inq), 12); \ + int32x4_t out##_h = vshll_n_s16(vget_high_s16(inq), 12) + +// wide add +#define dct_wadd(out, a, b) \ + int32x4_t out##_l = vaddq_s32(a##_l, b##_l); \ + int32x4_t out##_h = vaddq_s32(a##_h, b##_h) + +// wide sub +#define dct_wsub(out, a, b) \ + int32x4_t out##_l = vsubq_s32(a##_l, b##_l); \ + int32x4_t out##_h = vsubq_s32(a##_h, b##_h) + +// butterfly a/b, then shift using "shiftop" by "s" and pack +#define dct_bfly32o(out0,out1, a,b,shiftop,s) \ + { \ + dct_wadd(sum, a, b); \ + dct_wsub(dif, a, b); \ + out0 = vcombine_s16(shiftop(sum_l, s), shiftop(sum_h, s)); \ + out1 = vcombine_s16(shiftop(dif_l, s), shiftop(dif_h, s)); \ + } + +#define dct_pass(shiftop, shift) \ + { \ + /* even part */ \ + int16x8_t sum26 = vaddq_s16(row2, row6); \ + dct_long_mul(p1e, sum26, rot0_0); \ + dct_long_mac(t2e, p1e, row6, rot0_1); \ + dct_long_mac(t3e, p1e, row2, rot0_2); \ + int16x8_t sum04 = vaddq_s16(row0, row4); \ + int16x8_t dif04 = vsubq_s16(row0, row4); \ + dct_widen(t0e, sum04); \ + dct_widen(t1e, dif04); \ + dct_wadd(x0, t0e, t3e); \ + dct_wsub(x3, t0e, t3e); \ + dct_wadd(x1, t1e, t2e); \ + dct_wsub(x2, t1e, t2e); \ + /* odd part */ \ + int16x8_t sum15 = vaddq_s16(row1, row5); \ + int16x8_t sum17 = vaddq_s16(row1, row7); \ + int16x8_t sum35 = vaddq_s16(row3, row5); \ + int16x8_t sum37 = vaddq_s16(row3, row7); \ + int16x8_t sumodd = vaddq_s16(sum17, sum35); \ + dct_long_mul(p5o, sumodd, rot1_0); \ + dct_long_mac(p1o, p5o, sum17, rot1_1); \ + dct_long_mac(p2o, p5o, sum35, rot1_2); \ + dct_long_mul(p3o, sum37, rot2_0); \ + dct_long_mul(p4o, sum15, rot2_1); \ + dct_wadd(sump13o, p1o, p3o); \ + dct_wadd(sump24o, p2o, p4o); \ + dct_wadd(sump23o, p2o, p3o); \ + dct_wadd(sump14o, p1o, p4o); \ + dct_long_mac(x4, sump13o, row7, rot3_0); \ + dct_long_mac(x5, sump24o, row5, rot3_1); \ + dct_long_mac(x6, sump23o, row3, rot3_2); \ + dct_long_mac(x7, sump14o, row1, rot3_3); \ + dct_bfly32o(row0,row7, x0,x7,shiftop,shift); \ + dct_bfly32o(row1,row6, x1,x6,shiftop,shift); \ + dct_bfly32o(row2,row5, x2,x5,shiftop,shift); \ + dct_bfly32o(row3,row4, x3,x4,shiftop,shift); \ + } + + // load + row0 = vld1q_s16(data + 0*8); + row1 = vld1q_s16(data + 1*8); + row2 = vld1q_s16(data + 2*8); + row3 = vld1q_s16(data + 3*8); + row4 = vld1q_s16(data + 4*8); + row5 = vld1q_s16(data + 5*8); + row6 = vld1q_s16(data + 6*8); + row7 = vld1q_s16(data + 7*8); + + // add DC bias + row0 = vaddq_s16(row0, vsetq_lane_s16(1024, vdupq_n_s16(0), 0)); + + // column pass + dct_pass(vrshrn_n_s32, 10); + + // 16bit 8x8 transpose + { +// these three map to a single VTRN.16, VTRN.32, and VSWP, respectively. +// whether compilers actually get this is another story, sadly. +#define dct_trn16(x, y) { int16x8x2_t t = vtrnq_s16(x, y); x = t.val[0]; y = t.val[1]; } +#define dct_trn32(x, y) { int32x4x2_t t = vtrnq_s32(vreinterpretq_s32_s16(x), vreinterpretq_s32_s16(y)); x = vreinterpretq_s16_s32(t.val[0]); y = vreinterpretq_s16_s32(t.val[1]); } +#define dct_trn64(x, y) { int16x8_t x0 = x; int16x8_t y0 = y; x = vcombine_s16(vget_low_s16(x0), vget_low_s16(y0)); y = vcombine_s16(vget_high_s16(x0), vget_high_s16(y0)); } + + // pass 1 + dct_trn16(row0, row1); // a0b0a2b2a4b4a6b6 + dct_trn16(row2, row3); + dct_trn16(row4, row5); + dct_trn16(row6, row7); + + // pass 2 + dct_trn32(row0, row2); // a0b0c0d0a4b4c4d4 + dct_trn32(row1, row3); + dct_trn32(row4, row6); + dct_trn32(row5, row7); + + // pass 3 + dct_trn64(row0, row4); // a0b0c0d0e0f0g0h0 + dct_trn64(row1, row5); + dct_trn64(row2, row6); + dct_trn64(row3, row7); + +#undef dct_trn16 +#undef dct_trn32 +#undef dct_trn64 + } + + // row pass + // vrshrn_n_s32 only supports shifts up to 16, we need + // 17. so do a non-rounding shift of 16 first then follow + // up with a rounding shift by 1. + dct_pass(vshrn_n_s32, 16); + + { + // pack and round + uint8x8_t p0 = vqrshrun_n_s16(row0, 1); + uint8x8_t p1 = vqrshrun_n_s16(row1, 1); + uint8x8_t p2 = vqrshrun_n_s16(row2, 1); + uint8x8_t p3 = vqrshrun_n_s16(row3, 1); + uint8x8_t p4 = vqrshrun_n_s16(row4, 1); + uint8x8_t p5 = vqrshrun_n_s16(row5, 1); + uint8x8_t p6 = vqrshrun_n_s16(row6, 1); + uint8x8_t p7 = vqrshrun_n_s16(row7, 1); + + // again, these can translate into one instruction, but often don't. +#define dct_trn8_8(x, y) { uint8x8x2_t t = vtrn_u8(x, y); x = t.val[0]; y = t.val[1]; } +#define dct_trn8_16(x, y) { uint16x4x2_t t = vtrn_u16(vreinterpret_u16_u8(x), vreinterpret_u16_u8(y)); x = vreinterpret_u8_u16(t.val[0]); y = vreinterpret_u8_u16(t.val[1]); } +#define dct_trn8_32(x, y) { uint32x2x2_t t = vtrn_u32(vreinterpret_u32_u8(x), vreinterpret_u32_u8(y)); x = vreinterpret_u8_u32(t.val[0]); y = vreinterpret_u8_u32(t.val[1]); } + + // sadly can't use interleaved stores here since we only write + // 8 bytes to each scan line! + + // 8x8 8-bit transpose pass 1 + dct_trn8_8(p0, p1); + dct_trn8_8(p2, p3); + dct_trn8_8(p4, p5); + dct_trn8_8(p6, p7); + + // pass 2 + dct_trn8_16(p0, p2); + dct_trn8_16(p1, p3); + dct_trn8_16(p4, p6); + dct_trn8_16(p5, p7); + + // pass 3 + dct_trn8_32(p0, p4); + dct_trn8_32(p1, p5); + dct_trn8_32(p2, p6); + dct_trn8_32(p3, p7); + + // store + vst1_u8(out, p0); out += out_stride; + vst1_u8(out, p1); out += out_stride; + vst1_u8(out, p2); out += out_stride; + vst1_u8(out, p3); out += out_stride; + vst1_u8(out, p4); out += out_stride; + vst1_u8(out, p5); out += out_stride; + vst1_u8(out, p6); out += out_stride; + vst1_u8(out, p7); + +#undef dct_trn8_8 +#undef dct_trn8_16 +#undef dct_trn8_32 + } + +#undef dct_long_mul +#undef dct_long_mac +#undef dct_widen +#undef dct_wadd +#undef dct_wsub +#undef dct_bfly32o +#undef dct_pass +} + +#endif // STBI_NEON + +#define STBI__MARKER_none 0xff +// if there's a pending marker from the entropy stream, return that +// otherwise, fetch from the stream and get a marker. if there's no +// marker, return 0xff, which is never a valid marker value +static stbi_uc stbi__get_marker(stbi__jpeg *j) +{ + stbi_uc x; + if (j->marker != STBI__MARKER_none) { x = j->marker; j->marker = STBI__MARKER_none; return x; } + x = stbi__get8(j->s); + if (x != 0xff) return STBI__MARKER_none; + while (x == 0xff) + x = stbi__get8(j->s); // consume repeated 0xff fill bytes + return x; +} + +// in each scan, we'll have scan_n components, and the order +// of the components is specified by order[] +#define STBI__RESTART(x) ((x) >= 0xd0 && (x) <= 0xd7) + +// after a restart interval, stbi__jpeg_reset the entropy decoder and +// the dc prediction +static void stbi__jpeg_reset(stbi__jpeg *j) +{ + j->code_bits = 0; + j->code_buffer = 0; + j->nomore = 0; + j->img_comp[0].dc_pred = j->img_comp[1].dc_pred = j->img_comp[2].dc_pred = j->img_comp[3].dc_pred = 0; + j->marker = STBI__MARKER_none; + j->todo = j->restart_interval ? j->restart_interval : 0x7fffffff; + j->eob_run = 0; + // no more than 1<<31 MCUs if no restart_interal? that's plenty safe, + // since we don't even allow 1<<30 pixels +} + +static int stbi__parse_entropy_coded_data(stbi__jpeg *z) +{ + stbi__jpeg_reset(z); + if (!z->progressive) { + if (z->scan_n == 1) { + int i,j; + STBI_SIMD_ALIGN(short, data[64]); + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a time, + // in trivial scanline order + // number of blocks to do just depends on how many actual "pixels" this + // component has, independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); + // every data block is an MCU, so countdown the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + // if it's NOT a restart, then just bail, so we get corrupt data + // rather than no data + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } else { // interleaved + int i,j,k,x,y; + STBI_SIMD_ALIGN(short, data[64]); + for (j=0; j < z->img_mcu_y; ++j) { + for (i=0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in order + for (k=0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (y=0; y < z->img_comp[n].v; ++y) { + for (x=0; x < z->img_comp[n].h; ++x) { + int x2 = (i*z->img_comp[n].h + x)*8; + int y2 = (j*z->img_comp[n].v + y)*8; + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*y2+x2, z->img_comp[n].w2, data); + } + } + } + // after all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + } else { + if (z->scan_n == 1) { + int i,j; + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a time, + // in trivial scanline order + // number of blocks to do just depends on how many actual "pixels" this + // component has, independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); + if (z->spec_start == 0) { + if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) + return 0; + } else { + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block_prog_ac(z, data, &z->huff_ac[ha], z->fast_ac[ha])) + return 0; + } + // every data block is an MCU, so countdown the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } else { // interleaved + int i,j,k,x,y; + for (j=0; j < z->img_mcu_y; ++j) { + for (i=0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in order + for (k=0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (y=0; y < z->img_comp[n].v; ++y) { + for (x=0; x < z->img_comp[n].h; ++x) { + int x2 = (i*z->img_comp[n].h + x); + int y2 = (j*z->img_comp[n].v + y); + short *data = z->img_comp[n].coeff + 64 * (x2 + y2 * z->img_comp[n].coeff_w); + if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) + return 0; + } + } + } + // after all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + } +} + +static void stbi__jpeg_dequantize(short *data, stbi__uint16 *dequant) +{ + int i; + for (i=0; i < 64; ++i) + data[i] *= dequant[i]; +} + +static void stbi__jpeg_finish(stbi__jpeg *z) +{ + if (z->progressive) { + // dequantize and idct the data + int i,j,n; + for (n=0; n < z->s->img_n; ++n) { + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); + stbi__jpeg_dequantize(data, z->dequant[z->img_comp[n].tq]); + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); + } + } + } + } +} + +static int stbi__process_marker(stbi__jpeg *z, int m) +{ + int L; + switch (m) { + case STBI__MARKER_none: // no marker found + return stbi__err("expected marker","Corrupt JPEG"); + + case 0xDD: // DRI - specify restart interval + if (stbi__get16be(z->s) != 4) return stbi__err("bad DRI len","Corrupt JPEG"); + z->restart_interval = stbi__get16be(z->s); + return 1; + + case 0xDB: // DQT - define quantization table + L = stbi__get16be(z->s)-2; + while (L > 0) { + int q = stbi__get8(z->s); + int p = q >> 4, sixteen = (p != 0); + int t = q & 15,i; + if (p != 0 && p != 1) return stbi__err("bad DQT type","Corrupt JPEG"); + if (t > 3) return stbi__err("bad DQT table","Corrupt JPEG"); + + for (i=0; i < 64; ++i) + z->dequant[t][stbi__jpeg_dezigzag[i]] = (stbi__uint16)(sixteen ? stbi__get16be(z->s) : stbi__get8(z->s)); + L -= (sixteen ? 129 : 65); + } + return L==0; + + case 0xC4: // DHT - define huffman table + L = stbi__get16be(z->s)-2; + while (L > 0) { + stbi_uc *v; + int sizes[16],i,n=0; + int q = stbi__get8(z->s); + int tc = q >> 4; + int th = q & 15; + if (tc > 1 || th > 3) return stbi__err("bad DHT header","Corrupt JPEG"); + for (i=0; i < 16; ++i) { + sizes[i] = stbi__get8(z->s); + n += sizes[i]; + } + if(n > 256) return stbi__err("bad DHT header","Corrupt JPEG"); // Loop over i < n would write past end of values! + L -= 17; + if (tc == 0) { + if (!stbi__build_huffman(z->huff_dc+th, sizes)) return 0; + v = z->huff_dc[th].values; + } else { + if (!stbi__build_huffman(z->huff_ac+th, sizes)) return 0; + v = z->huff_ac[th].values; + } + for (i=0; i < n; ++i) + v[i] = stbi__get8(z->s); + if (tc != 0) + stbi__build_fast_ac(z->fast_ac[th], z->huff_ac + th); + L -= n; + } + return L==0; + } + + // check for comment block or APP blocks + if ((m >= 0xE0 && m <= 0xEF) || m == 0xFE) { + L = stbi__get16be(z->s); + if (L < 2) { + if (m == 0xFE) + return stbi__err("bad COM len","Corrupt JPEG"); + else + return stbi__err("bad APP len","Corrupt JPEG"); + } + L -= 2; + + if (m == 0xE0 && L >= 5) { // JFIF APP0 segment + static const unsigned char tag[5] = {'J','F','I','F','\0'}; + int ok = 1; + int i; + for (i=0; i < 5; ++i) + if (stbi__get8(z->s) != tag[i]) + ok = 0; + L -= 5; + if (ok) + z->jfif = 1; + } else if (m == 0xEE && L >= 12) { // Adobe APP14 segment + static const unsigned char tag[6] = {'A','d','o','b','e','\0'}; + int ok = 1; + int i; + for (i=0; i < 6; ++i) + if (stbi__get8(z->s) != tag[i]) + ok = 0; + L -= 6; + if (ok) { + stbi__get8(z->s); // version + stbi__get16be(z->s); // flags0 + stbi__get16be(z->s); // flags1 + z->app14_color_transform = stbi__get8(z->s); // color transform + L -= 6; + } + } + + stbi__skip(z->s, L); + return 1; + } + + return stbi__err("unknown marker","Corrupt JPEG"); +} + +// after we see SOS +static int stbi__process_scan_header(stbi__jpeg *z) +{ + int i; + int Ls = stbi__get16be(z->s); + z->scan_n = stbi__get8(z->s); + if (z->scan_n < 1 || z->scan_n > 4 || z->scan_n > (int) z->s->img_n) return stbi__err("bad SOS component count","Corrupt JPEG"); + if (Ls != 6+2*z->scan_n) return stbi__err("bad SOS len","Corrupt JPEG"); + for (i=0; i < z->scan_n; ++i) { + int id = stbi__get8(z->s), which; + int q = stbi__get8(z->s); + for (which = 0; which < z->s->img_n; ++which) + if (z->img_comp[which].id == id) + break; + if (which == z->s->img_n) return 0; // no match + z->img_comp[which].hd = q >> 4; if (z->img_comp[which].hd > 3) return stbi__err("bad DC huff","Corrupt JPEG"); + z->img_comp[which].ha = q & 15; if (z->img_comp[which].ha > 3) return stbi__err("bad AC huff","Corrupt JPEG"); + z->order[i] = which; + } + + { + int aa; + z->spec_start = stbi__get8(z->s); + z->spec_end = stbi__get8(z->s); // should be 63, but might be 0 + aa = stbi__get8(z->s); + z->succ_high = (aa >> 4); + z->succ_low = (aa & 15); + if (z->progressive) { + if (z->spec_start > 63 || z->spec_end > 63 || z->spec_start > z->spec_end || z->succ_high > 13 || z->succ_low > 13) + return stbi__err("bad SOS", "Corrupt JPEG"); + } else { + if (z->spec_start != 0) return stbi__err("bad SOS","Corrupt JPEG"); + if (z->succ_high != 0 || z->succ_low != 0) return stbi__err("bad SOS","Corrupt JPEG"); + z->spec_end = 63; + } + } + + return 1; +} + +static int stbi__free_jpeg_components(stbi__jpeg *z, int ncomp, int why) +{ + int i; + for (i=0; i < ncomp; ++i) { + if (z->img_comp[i].raw_data) { + STBI_FREE(z->img_comp[i].raw_data); + z->img_comp[i].raw_data = NULL; + z->img_comp[i].data = NULL; + } + if (z->img_comp[i].raw_coeff) { + STBI_FREE(z->img_comp[i].raw_coeff); + z->img_comp[i].raw_coeff = 0; + z->img_comp[i].coeff = 0; + } + if (z->img_comp[i].linebuf) { + STBI_FREE(z->img_comp[i].linebuf); + z->img_comp[i].linebuf = NULL; + } + } + return why; +} + +static int stbi__process_frame_header(stbi__jpeg *z, int scan) +{ + stbi__context *s = z->s; + int Lf,p,i,q, h_max=1,v_max=1,c; + Lf = stbi__get16be(s); if (Lf < 11) return stbi__err("bad SOF len","Corrupt JPEG"); // JPEG + p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline + s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG + s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + c = stbi__get8(s); + if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG"); + s->img_n = c; + for (i=0; i < c; ++i) { + z->img_comp[i].data = NULL; + z->img_comp[i].linebuf = NULL; + } + + if (Lf != 8+3*s->img_n) return stbi__err("bad SOF len","Corrupt JPEG"); + + z->rgb = 0; + for (i=0; i < s->img_n; ++i) { + static const unsigned char rgb[3] = { 'R', 'G', 'B' }; + z->img_comp[i].id = stbi__get8(s); + if (s->img_n == 3 && z->img_comp[i].id == rgb[i]) + ++z->rgb; + q = stbi__get8(s); + z->img_comp[i].h = (q >> 4); if (!z->img_comp[i].h || z->img_comp[i].h > 4) return stbi__err("bad H","Corrupt JPEG"); + z->img_comp[i].v = q & 15; if (!z->img_comp[i].v || z->img_comp[i].v > 4) return stbi__err("bad V","Corrupt JPEG"); + z->img_comp[i].tq = stbi__get8(s); if (z->img_comp[i].tq > 3) return stbi__err("bad TQ","Corrupt JPEG"); + } + + if (scan != STBI__SCAN_load) return 1; + + if (!stbi__mad3sizes_valid(s->img_x, s->img_y, s->img_n, 0)) return stbi__err("too large", "Image too large to decode"); + + for (i=0; i < s->img_n; ++i) { + if (z->img_comp[i].h > h_max) h_max = z->img_comp[i].h; + if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v; + } + + // check that plane subsampling factors are integer ratios; our resamplers can't deal with fractional ratios + // and I've never seen a non-corrupted JPEG file actually use them + for (i=0; i < s->img_n; ++i) { + if (h_max % z->img_comp[i].h != 0) return stbi__err("bad H","Corrupt JPEG"); + if (v_max % z->img_comp[i].v != 0) return stbi__err("bad V","Corrupt JPEG"); + } + + // compute interleaved mcu info + z->img_h_max = h_max; + z->img_v_max = v_max; + z->img_mcu_w = h_max * 8; + z->img_mcu_h = v_max * 8; + // these sizes can't be more than 17 bits + z->img_mcu_x = (s->img_x + z->img_mcu_w-1) / z->img_mcu_w; + z->img_mcu_y = (s->img_y + z->img_mcu_h-1) / z->img_mcu_h; + + for (i=0; i < s->img_n; ++i) { + // number of effective pixels (e.g. for non-interleaved MCU) + z->img_comp[i].x = (s->img_x * z->img_comp[i].h + h_max-1) / h_max; + z->img_comp[i].y = (s->img_y * z->img_comp[i].v + v_max-1) / v_max; + // to simplify generation, we'll allocate enough memory to decode + // the bogus oversized data from using interleaved MCUs and their + // big blocks (e.g. a 16x16 iMCU on an image of width 33); we won't + // discard the extra data until colorspace conversion + // + // img_mcu_x, img_mcu_y: <=17 bits; comp[i].h and .v are <=4 (checked earlier) + // so these muls can't overflow with 32-bit ints (which we require) + z->img_comp[i].w2 = z->img_mcu_x * z->img_comp[i].h * 8; + z->img_comp[i].h2 = z->img_mcu_y * z->img_comp[i].v * 8; + z->img_comp[i].coeff = 0; + z->img_comp[i].raw_coeff = 0; + z->img_comp[i].linebuf = NULL; + z->img_comp[i].raw_data = stbi__malloc_mad2(z->img_comp[i].w2, z->img_comp[i].h2, 15); + if (z->img_comp[i].raw_data == NULL) + return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); + // align blocks for idct using mmx/sse + z->img_comp[i].data = (stbi_uc*) (((size_t) z->img_comp[i].raw_data + 15) & ~15); + if (z->progressive) { + // w2, h2 are multiples of 8 (see above) + z->img_comp[i].coeff_w = z->img_comp[i].w2 / 8; + z->img_comp[i].coeff_h = z->img_comp[i].h2 / 8; + z->img_comp[i].raw_coeff = stbi__malloc_mad3(z->img_comp[i].w2, z->img_comp[i].h2, sizeof(short), 15); + if (z->img_comp[i].raw_coeff == NULL) + return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); + z->img_comp[i].coeff = (short*) (((size_t) z->img_comp[i].raw_coeff + 15) & ~15); + } + } + + return 1; +} + +// use comparisons since in some cases we handle more than one case (e.g. SOF) +#define stbi__DNL(x) ((x) == 0xdc) +#define stbi__SOI(x) ((x) == 0xd8) +#define stbi__EOI(x) ((x) == 0xd9) +#define stbi__SOF(x) ((x) == 0xc0 || (x) == 0xc1 || (x) == 0xc2) +#define stbi__SOS(x) ((x) == 0xda) + +#define stbi__SOF_progressive(x) ((x) == 0xc2) + +static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan) +{ + int m; + z->jfif = 0; + z->app14_color_transform = -1; // valid values are 0,1,2 + z->marker = STBI__MARKER_none; // initialize cached marker to empty + m = stbi__get_marker(z); + if (!stbi__SOI(m)) return stbi__err("no SOI","Corrupt JPEG"); + if (scan == STBI__SCAN_type) return 1; + m = stbi__get_marker(z); + while (!stbi__SOF(m)) { + if (!stbi__process_marker(z,m)) return 0; + m = stbi__get_marker(z); + while (m == STBI__MARKER_none) { + // some files have extra padding after their blocks, so ok, we'll scan + if (stbi__at_eof(z->s)) return stbi__err("no SOF", "Corrupt JPEG"); + m = stbi__get_marker(z); + } + } + z->progressive = stbi__SOF_progressive(m); + if (!stbi__process_frame_header(z, scan)) return 0; + return 1; +} + +static stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) +{ + // some JPEGs have junk at end, skip over it but if we find what looks + // like a valid marker, resume there + while (!stbi__at_eof(j->s)) { + stbi_uc x = stbi__get8(j->s); + while (x == 0xff) { // might be a marker + if (stbi__at_eof(j->s)) return STBI__MARKER_none; + x = stbi__get8(j->s); + if (x != 0x00 && x != 0xff) { + // not a stuffed zero or lead-in to another marker, looks + // like an actual marker, return it + return x; + } + // stuffed zero has x=0 now which ends the loop, meaning we go + // back to regular scan loop. + // repeated 0xff keeps trying to read the next byte of the marker. + } + } + return STBI__MARKER_none; +} + +// decode image to YCbCr format +static int stbi__decode_jpeg_image(stbi__jpeg *j) +{ + int m; + for (m = 0; m < 4; m++) { + j->img_comp[m].raw_data = NULL; + j->img_comp[m].raw_coeff = NULL; + } + j->restart_interval = 0; + if (!stbi__decode_jpeg_header(j, STBI__SCAN_load)) return 0; + m = stbi__get_marker(j); + while (!stbi__EOI(m)) { + if (stbi__SOS(m)) { + if (!stbi__process_scan_header(j)) return 0; + if (!stbi__parse_entropy_coded_data(j)) return 0; + if (j->marker == STBI__MARKER_none ) { + j->marker = stbi__skip_jpeg_junk_at_end(j); + // if we reach eof without hitting a marker, stbi__get_marker() below will fail and we'll eventually return 0 + } + m = stbi__get_marker(j); + if (STBI__RESTART(m)) + m = stbi__get_marker(j); + } else if (stbi__DNL(m)) { + int Ld = stbi__get16be(j->s); + stbi__uint32 NL = stbi__get16be(j->s); + if (Ld != 4) return stbi__err("bad DNL len", "Corrupt JPEG"); + if (NL != j->s->img_y) return stbi__err("bad DNL height", "Corrupt JPEG"); + m = stbi__get_marker(j); + } else { + if (!stbi__process_marker(j, m)) return 1; + m = stbi__get_marker(j); + } + } + if (j->progressive) + stbi__jpeg_finish(j); + return 1; +} + +// static jfif-centered resampling (across block boundaries) + +typedef stbi_uc *(*resample_row_func)(stbi_uc *out, stbi_uc *in0, stbi_uc *in1, + int w, int hs); + +#define stbi__div4(x) ((stbi_uc) ((x) >> 2)) + +static stbi_uc *resample_row_1(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + STBI_NOTUSED(out); + STBI_NOTUSED(in_far); + STBI_NOTUSED(w); + STBI_NOTUSED(hs); + return in_near; +} + +static stbi_uc* stbi__resample_row_v_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate two samples vertically for every one in input + int i; + STBI_NOTUSED(hs); + for (i=0; i < w; ++i) + out[i] = stbi__div4(3*in_near[i] + in_far[i] + 2); + return out; +} + +static stbi_uc* stbi__resample_row_h_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate two samples horizontally for every one in input + int i; + stbi_uc *input = in_near; + + if (w == 1) { + // if only one sample, can't do any interpolation + out[0] = out[1] = input[0]; + return out; + } + + out[0] = input[0]; + out[1] = stbi__div4(input[0]*3 + input[1] + 2); + for (i=1; i < w-1; ++i) { + int n = 3*input[i]+2; + out[i*2+0] = stbi__div4(n+input[i-1]); + out[i*2+1] = stbi__div4(n+input[i+1]); + } + out[i*2+0] = stbi__div4(input[w-2]*3 + input[w-1] + 2); + out[i*2+1] = input[w-1]; + + STBI_NOTUSED(in_far); + STBI_NOTUSED(hs); + + return out; +} + +#define stbi__div16(x) ((stbi_uc) ((x) >> 4)) + +static stbi_uc *stbi__resample_row_hv_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i,t0,t1; + if (w == 1) { + out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3*in_near[0] + in_far[0]; + out[0] = stbi__div4(t1+2); + for (i=1; i < w; ++i) { + t0 = t1; + t1 = 3*in_near[i]+in_far[i]; + out[i*2-1] = stbi__div16(3*t0 + t1 + 8); + out[i*2 ] = stbi__div16(3*t1 + t0 + 8); + } + out[w*2-1] = stbi__div4(t1+2); + + STBI_NOTUSED(hs); + + return out; +} + +#if defined(STBI_SSE2) || defined(STBI_NEON) +static stbi_uc *stbi__resample_row_hv_2_simd(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i=0,t0,t1; + + if (w == 1) { + out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3*in_near[0] + in_far[0]; + // process groups of 8 pixels for as long as we can. + // note we can't handle the last pixel in a row in this loop + // because we need to handle the filter boundary conditions. + for (; i < ((w-1) & ~7); i += 8) { +#if defined(STBI_SSE2) + // load and perform the vertical filtering pass + // this uses 3*x + y = 4*x + (y - x) + __m128i zero = _mm_setzero_si128(); + __m128i farb = _mm_loadl_epi64((__m128i *) (in_far + i)); + __m128i nearb = _mm_loadl_epi64((__m128i *) (in_near + i)); + __m128i farw = _mm_unpacklo_epi8(farb, zero); + __m128i nearw = _mm_unpacklo_epi8(nearb, zero); + __m128i diff = _mm_sub_epi16(farw, nearw); + __m128i nears = _mm_slli_epi16(nearw, 2); + __m128i curr = _mm_add_epi16(nears, diff); // current row + + // horizontal filter works the same based on shifted vers of current + // row. "prev" is current row shifted right by 1 pixel; we need to + // insert the previous pixel value (from t1). + // "next" is current row shifted left by 1 pixel, with first pixel + // of next block of 8 pixels added in. + __m128i prv0 = _mm_slli_si128(curr, 2); + __m128i nxt0 = _mm_srli_si128(curr, 2); + __m128i prev = _mm_insert_epi16(prv0, t1, 0); + __m128i next = _mm_insert_epi16(nxt0, 3*in_near[i+8] + in_far[i+8], 7); + + // horizontal filter, polyphase implementation since it's convenient: + // even pixels = 3*cur + prev = cur*4 + (prev - cur) + // odd pixels = 3*cur + next = cur*4 + (next - cur) + // note the shared term. + __m128i bias = _mm_set1_epi16(8); + __m128i curs = _mm_slli_epi16(curr, 2); + __m128i prvd = _mm_sub_epi16(prev, curr); + __m128i nxtd = _mm_sub_epi16(next, curr); + __m128i curb = _mm_add_epi16(curs, bias); + __m128i even = _mm_add_epi16(prvd, curb); + __m128i odd = _mm_add_epi16(nxtd, curb); + + // interleave even and odd pixels, then undo scaling. + __m128i int0 = _mm_unpacklo_epi16(even, odd); + __m128i int1 = _mm_unpackhi_epi16(even, odd); + __m128i de0 = _mm_srli_epi16(int0, 4); + __m128i de1 = _mm_srli_epi16(int1, 4); + + // pack and write output + __m128i outv = _mm_packus_epi16(de0, de1); + _mm_storeu_si128((__m128i *) (out + i*2), outv); +#elif defined(STBI_NEON) + // load and perform the vertical filtering pass + // this uses 3*x + y = 4*x + (y - x) + uint8x8_t farb = vld1_u8(in_far + i); + uint8x8_t nearb = vld1_u8(in_near + i); + int16x8_t diff = vreinterpretq_s16_u16(vsubl_u8(farb, nearb)); + int16x8_t nears = vreinterpretq_s16_u16(vshll_n_u8(nearb, 2)); + int16x8_t curr = vaddq_s16(nears, diff); // current row + + // horizontal filter works the same based on shifted vers of current + // row. "prev" is current row shifted right by 1 pixel; we need to + // insert the previous pixel value (from t1). + // "next" is current row shifted left by 1 pixel, with first pixel + // of next block of 8 pixels added in. + int16x8_t prv0 = vextq_s16(curr, curr, 7); + int16x8_t nxt0 = vextq_s16(curr, curr, 1); + int16x8_t prev = vsetq_lane_s16(t1, prv0, 0); + int16x8_t next = vsetq_lane_s16(3*in_near[i+8] + in_far[i+8], nxt0, 7); + + // horizontal filter, polyphase implementation since it's convenient: + // even pixels = 3*cur + prev = cur*4 + (prev - cur) + // odd pixels = 3*cur + next = cur*4 + (next - cur) + // note the shared term. + int16x8_t curs = vshlq_n_s16(curr, 2); + int16x8_t prvd = vsubq_s16(prev, curr); + int16x8_t nxtd = vsubq_s16(next, curr); + int16x8_t even = vaddq_s16(curs, prvd); + int16x8_t odd = vaddq_s16(curs, nxtd); + + // undo scaling and round, then store with even/odd phases interleaved + uint8x8x2_t o; + o.val[0] = vqrshrun_n_s16(even, 4); + o.val[1] = vqrshrun_n_s16(odd, 4); + vst2_u8(out + i*2, o); +#endif + + // "previous" value for next iter + t1 = 3*in_near[i+7] + in_far[i+7]; + } + + t0 = t1; + t1 = 3*in_near[i] + in_far[i]; + out[i*2] = stbi__div16(3*t1 + t0 + 8); + + for (++i; i < w; ++i) { + t0 = t1; + t1 = 3*in_near[i]+in_far[i]; + out[i*2-1] = stbi__div16(3*t0 + t1 + 8); + out[i*2 ] = stbi__div16(3*t1 + t0 + 8); + } + out[w*2-1] = stbi__div4(t1+2); + + STBI_NOTUSED(hs); + + return out; +} +#endif + +static stbi_uc *stbi__resample_row_generic(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // resample with nearest-neighbor + int i,j; + STBI_NOTUSED(in_far); + for (i=0; i < w; ++i) + for (j=0; j < hs; ++j) + out[i*hs+j] = in_near[i]; + return out; +} + +// this is a reduced-precision calculation of YCbCr-to-RGB introduced +// to make sure the code produces the same results in both SIMD and scalar +#define stbi__float2fixed(x) (((int) ((x) * 4096.0f + 0.5f)) << 8) +static void stbi__YCbCr_to_RGB_row(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step) +{ + int i; + for (i=0; i < count; ++i) { + int y_fixed = (y[i] << 20) + (1<<19); // rounding + int r,g,b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr* stbi__float2fixed(1.40200f); + g = y_fixed + (cr*-stbi__float2fixed(0.71414f)) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); + b = y_fixed + cb* stbi__float2fixed(1.77200f); + r >>= 20; + g >>= 20; + b >>= 20; + if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } + if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } + if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } + out[0] = (stbi_uc)r; + out[1] = (stbi_uc)g; + out[2] = (stbi_uc)b; + out[3] = 255; + out += step; + } +} + +#if defined(STBI_SSE2) || defined(STBI_NEON) +static void stbi__YCbCr_to_RGB_simd(stbi_uc *out, stbi_uc const *y, stbi_uc const *pcb, stbi_uc const *pcr, int count, int step) +{ + int i = 0; + +#ifdef STBI_SSE2 + // step == 3 is pretty ugly on the final interleave, and i'm not convinced + // it's useful in practice (you wouldn't use it for textures, for example). + // so just accelerate step == 4 case. + if (step == 4) { + // this is a fairly straightforward implementation and not super-optimized. + __m128i signflip = _mm_set1_epi8(-0x80); + __m128i cr_const0 = _mm_set1_epi16( (short) ( 1.40200f*4096.0f+0.5f)); + __m128i cr_const1 = _mm_set1_epi16( - (short) ( 0.71414f*4096.0f+0.5f)); + __m128i cb_const0 = _mm_set1_epi16( - (short) ( 0.34414f*4096.0f+0.5f)); + __m128i cb_const1 = _mm_set1_epi16( (short) ( 1.77200f*4096.0f+0.5f)); + __m128i y_bias = _mm_set1_epi8((char) (unsigned char) 128); + __m128i xw = _mm_set1_epi16(255); // alpha channel + + for (; i+7 < count; i += 8) { + // load + __m128i y_bytes = _mm_loadl_epi64((__m128i *) (y+i)); + __m128i cr_bytes = _mm_loadl_epi64((__m128i *) (pcr+i)); + __m128i cb_bytes = _mm_loadl_epi64((__m128i *) (pcb+i)); + __m128i cr_biased = _mm_xor_si128(cr_bytes, signflip); // -128 + __m128i cb_biased = _mm_xor_si128(cb_bytes, signflip); // -128 + + // unpack to short (and left-shift cr, cb by 8) + __m128i yw = _mm_unpacklo_epi8(y_bias, y_bytes); + __m128i crw = _mm_unpacklo_epi8(_mm_setzero_si128(), cr_biased); + __m128i cbw = _mm_unpacklo_epi8(_mm_setzero_si128(), cb_biased); + + // color transform + __m128i yws = _mm_srli_epi16(yw, 4); + __m128i cr0 = _mm_mulhi_epi16(cr_const0, crw); + __m128i cb0 = _mm_mulhi_epi16(cb_const0, cbw); + __m128i cb1 = _mm_mulhi_epi16(cbw, cb_const1); + __m128i cr1 = _mm_mulhi_epi16(crw, cr_const1); + __m128i rws = _mm_add_epi16(cr0, yws); + __m128i gwt = _mm_add_epi16(cb0, yws); + __m128i bws = _mm_add_epi16(yws, cb1); + __m128i gws = _mm_add_epi16(gwt, cr1); + + // descale + __m128i rw = _mm_srai_epi16(rws, 4); + __m128i bw = _mm_srai_epi16(bws, 4); + __m128i gw = _mm_srai_epi16(gws, 4); + + // back to byte, set up for transpose + __m128i brb = _mm_packus_epi16(rw, bw); + __m128i gxb = _mm_packus_epi16(gw, xw); + + // transpose to interleave channels + __m128i t0 = _mm_unpacklo_epi8(brb, gxb); + __m128i t1 = _mm_unpackhi_epi8(brb, gxb); + __m128i o0 = _mm_unpacklo_epi16(t0, t1); + __m128i o1 = _mm_unpackhi_epi16(t0, t1); + + // store + _mm_storeu_si128((__m128i *) (out + 0), o0); + _mm_storeu_si128((__m128i *) (out + 16), o1); + out += 32; + } + } +#endif + +#ifdef STBI_NEON + // in this version, step=3 support would be easy to add. but is there demand? + if (step == 4) { + // this is a fairly straightforward implementation and not super-optimized. + uint8x8_t signflip = vdup_n_u8(0x80); + int16x8_t cr_const0 = vdupq_n_s16( (short) ( 1.40200f*4096.0f+0.5f)); + int16x8_t cr_const1 = vdupq_n_s16( - (short) ( 0.71414f*4096.0f+0.5f)); + int16x8_t cb_const0 = vdupq_n_s16( - (short) ( 0.34414f*4096.0f+0.5f)); + int16x8_t cb_const1 = vdupq_n_s16( (short) ( 1.77200f*4096.0f+0.5f)); + + for (; i+7 < count; i += 8) { + // load + uint8x8_t y_bytes = vld1_u8(y + i); + uint8x8_t cr_bytes = vld1_u8(pcr + i); + uint8x8_t cb_bytes = vld1_u8(pcb + i); + int8x8_t cr_biased = vreinterpret_s8_u8(vsub_u8(cr_bytes, signflip)); + int8x8_t cb_biased = vreinterpret_s8_u8(vsub_u8(cb_bytes, signflip)); + + // expand to s16 + int16x8_t yws = vreinterpretq_s16_u16(vshll_n_u8(y_bytes, 4)); + int16x8_t crw = vshll_n_s8(cr_biased, 7); + int16x8_t cbw = vshll_n_s8(cb_biased, 7); + + // color transform + int16x8_t cr0 = vqdmulhq_s16(crw, cr_const0); + int16x8_t cb0 = vqdmulhq_s16(cbw, cb_const0); + int16x8_t cr1 = vqdmulhq_s16(crw, cr_const1); + int16x8_t cb1 = vqdmulhq_s16(cbw, cb_const1); + int16x8_t rws = vaddq_s16(yws, cr0); + int16x8_t gws = vaddq_s16(vaddq_s16(yws, cb0), cr1); + int16x8_t bws = vaddq_s16(yws, cb1); + + // undo scaling, round, convert to byte + uint8x8x4_t o; + o.val[0] = vqrshrun_n_s16(rws, 4); + o.val[1] = vqrshrun_n_s16(gws, 4); + o.val[2] = vqrshrun_n_s16(bws, 4); + o.val[3] = vdup_n_u8(255); + + // store, interleaving r/g/b/a + vst4_u8(out, o); + out += 8*4; + } + } +#endif + + for (; i < count; ++i) { + int y_fixed = (y[i] << 20) + (1<<19); // rounding + int r,g,b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr* stbi__float2fixed(1.40200f); + g = y_fixed + cr*-stbi__float2fixed(0.71414f) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); + b = y_fixed + cb* stbi__float2fixed(1.77200f); + r >>= 20; + g >>= 20; + b >>= 20; + if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } + if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } + if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } + out[0] = (stbi_uc)r; + out[1] = (stbi_uc)g; + out[2] = (stbi_uc)b; + out[3] = 255; + out += step; + } +} +#endif + +// set up the kernels +static void stbi__setup_jpeg(stbi__jpeg *j) +{ + j->idct_block_kernel = stbi__idct_block; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_row; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2; + +#ifdef STBI_SSE2 + if (stbi__sse2_available()) { + j->idct_block_kernel = stbi__idct_simd; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; + } +#endif + +#ifdef STBI_NEON + j->idct_block_kernel = stbi__idct_simd; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; +#endif +} + +// clean up the temporary component buffers +static void stbi__cleanup_jpeg(stbi__jpeg *j) +{ + stbi__free_jpeg_components(j, j->s->img_n, 0); +} + +typedef struct +{ + resample_row_func resample; + stbi_uc *line0,*line1; + int hs,vs; // expansion factor in each axis + int w_lores; // horizontal pixels pre-expansion + int ystep; // how far through vertical expansion we are + int ypos; // which pre-expansion row we're on +} stbi__resample; + +// fast 0..255 * 0..255 => 0..255 rounded multiplication +static stbi_uc stbi__blinn_8x8(stbi_uc x, stbi_uc y) +{ + unsigned int t = x*y + 128; + return (stbi_uc) ((t + (t >>8)) >> 8); +} + +static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp, int req_comp) +{ + int n, decode_n, is_rgb; + z->s->img_n = 0; // make stbi__cleanup_jpeg safe + + // validate req_comp + if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); + + // load a jpeg image from whichever source, but leave in YCbCr format + if (!stbi__decode_jpeg_image(z)) { stbi__cleanup_jpeg(z); return NULL; } + + // determine actual number of components to generate + n = req_comp ? req_comp : z->s->img_n >= 3 ? 3 : 1; + + is_rgb = z->s->img_n == 3 && (z->rgb == 3 || (z->app14_color_transform == 0 && !z->jfif)); + + if (z->s->img_n == 3 && n < 3 && !is_rgb) + decode_n = 1; + else + decode_n = z->s->img_n; + + // nothing to do if no components requested; check this now to avoid + // accessing uninitialized coutput[0] later + if (decode_n <= 0) { stbi__cleanup_jpeg(z); return NULL; } + + // resample and color-convert + { + int k; + unsigned int i,j; + stbi_uc *output; + stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL }; + + stbi__resample res_comp[4]; + + for (k=0; k < decode_n; ++k) { + stbi__resample *r = &res_comp[k]; + + // allocate line buffer big enough for upsampling off the edges + // with upsample factor of 4 + z->img_comp[k].linebuf = (stbi_uc *) stbi__malloc(z->s->img_x + 3); + if (!z->img_comp[k].linebuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } + + r->hs = z->img_h_max / z->img_comp[k].h; + r->vs = z->img_v_max / z->img_comp[k].v; + r->ystep = r->vs >> 1; + r->w_lores = (z->s->img_x + r->hs-1) / r->hs; + r->ypos = 0; + r->line0 = r->line1 = z->img_comp[k].data; + + if (r->hs == 1 && r->vs == 1) r->resample = resample_row_1; + else if (r->hs == 1 && r->vs == 2) r->resample = stbi__resample_row_v_2; + else if (r->hs == 2 && r->vs == 1) r->resample = stbi__resample_row_h_2; + else if (r->hs == 2 && r->vs == 2) r->resample = z->resample_row_hv_2_kernel; + else r->resample = stbi__resample_row_generic; + } + + // can't error after this so, this is safe + output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1); + if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } + + // now go ahead and resample + for (j=0; j < z->s->img_y; ++j) { + stbi_uc *out = output + n * z->s->img_x * j; + for (k=0; k < decode_n; ++k) { + stbi__resample *r = &res_comp[k]; + int y_bot = r->ystep >= (r->vs >> 1); + coutput[k] = r->resample(z->img_comp[k].linebuf, + y_bot ? r->line1 : r->line0, + y_bot ? r->line0 : r->line1, + r->w_lores, r->hs); + if (++r->ystep >= r->vs) { + r->ystep = 0; + r->line0 = r->line1; + if (++r->ypos < z->img_comp[k].y) + r->line1 += z->img_comp[k].w2; + } + } + if (n >= 3) { + stbi_uc *y = coutput[0]; + if (z->s->img_n == 3) { + if (is_rgb) { + for (i=0; i < z->s->img_x; ++i) { + out[0] = y[i]; + out[1] = coutput[1][i]; + out[2] = coutput[2][i]; + out[3] = 255; + out += n; + } + } else { + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + } + } else if (z->s->img_n == 4) { + if (z->app14_color_transform == 0) { // CMYK + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + out[0] = stbi__blinn_8x8(coutput[0][i], m); + out[1] = stbi__blinn_8x8(coutput[1][i], m); + out[2] = stbi__blinn_8x8(coutput[2][i], m); + out[3] = 255; + out += n; + } + } else if (z->app14_color_transform == 2) { // YCCK + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + out[0] = stbi__blinn_8x8(255 - out[0], m); + out[1] = stbi__blinn_8x8(255 - out[1], m); + out[2] = stbi__blinn_8x8(255 - out[2], m); + out += n; + } + } else { // YCbCr + alpha? Ignore the fourth channel for now + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + } + } else + for (i=0; i < z->s->img_x; ++i) { + out[0] = out[1] = out[2] = y[i]; + out[3] = 255; // not used if n==3 + out += n; + } + } else { + if (is_rgb) { + if (n == 1) + for (i=0; i < z->s->img_x; ++i) + *out++ = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); + else { + for (i=0; i < z->s->img_x; ++i, out += 2) { + out[0] = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); + out[1] = 255; + } + } + } else if (z->s->img_n == 4 && z->app14_color_transform == 0) { + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + stbi_uc r = stbi__blinn_8x8(coutput[0][i], m); + stbi_uc g = stbi__blinn_8x8(coutput[1][i], m); + stbi_uc b = stbi__blinn_8x8(coutput[2][i], m); + out[0] = stbi__compute_y(r, g, b); + out[1] = 255; + out += n; + } + } else if (z->s->img_n == 4 && z->app14_color_transform == 2) { + for (i=0; i < z->s->img_x; ++i) { + out[0] = stbi__blinn_8x8(255 - coutput[0][i], coutput[3][i]); + out[1] = 255; + out += n; + } + } else { + stbi_uc *y = coutput[0]; + if (n == 1) + for (i=0; i < z->s->img_x; ++i) out[i] = y[i]; + else + for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; } + } + } + } + stbi__cleanup_jpeg(z); + *out_x = z->s->img_x; + *out_y = z->s->img_y; + if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output + return output; + } +} + +static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + unsigned char* result; + stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg)); + if (!j) return stbi__errpuc("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + STBI_NOTUSED(ri); + j->s = s; + stbi__setup_jpeg(j); + result = load_jpeg_image(j, x,y,comp,req_comp); + STBI_FREE(j); + return result; +} + +static int stbi__jpeg_test(stbi__context *s) +{ + int r; + stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg)); + if (!j) return stbi__err("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + j->s = s; + stbi__setup_jpeg(j); + r = stbi__decode_jpeg_header(j, STBI__SCAN_type); + stbi__rewind(s); + STBI_FREE(j); + return r; +} + +static int stbi__jpeg_info_raw(stbi__jpeg *j, int *x, int *y, int *comp) +{ + if (!stbi__decode_jpeg_header(j, STBI__SCAN_header)) { + stbi__rewind( j->s ); + return 0; + } + if (x) *x = j->s->img_x; + if (y) *y = j->s->img_y; + if (comp) *comp = j->s->img_n >= 3 ? 3 : 1; + return 1; +} + +static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp) +{ + int result; + stbi__jpeg* j = (stbi__jpeg*) (stbi__malloc(sizeof(stbi__jpeg))); + if (!j) return stbi__err("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + j->s = s; + result = stbi__jpeg_info_raw(j, x, y, comp); + STBI_FREE(j); + return result; +} +#endif + +// public domain zlib decode v0.2 Sean Barrett 2006-11-18 +// simple implementation +// - all input must be provided in an upfront buffer +// - all output is written to a single output buffer (can malloc/realloc) +// performance +// - fast huffman + +#ifndef STBI_NO_ZLIB + +// fast-way is faster to check than jpeg huffman, but slow way is slower +#define STBI__ZFAST_BITS 9 // accelerate all cases in default tables +#define STBI__ZFAST_MASK ((1 << STBI__ZFAST_BITS) - 1) +#define STBI__ZNSYMS 288 // number of symbols in literal/length alphabet + +// zlib-style huffman encoding +// (jpegs packs from left, zlib from right, so can't share code) +typedef struct +{ + stbi__uint16 fast[1 << STBI__ZFAST_BITS]; + stbi__uint16 firstcode[16]; + int maxcode[17]; + stbi__uint16 firstsymbol[16]; + stbi_uc size[STBI__ZNSYMS]; + stbi__uint16 value[STBI__ZNSYMS]; +} stbi__zhuffman; + +stbi_inline static int stbi__bitreverse16(int n) +{ + n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); + n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); + n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); + n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); + return n; +} + +stbi_inline static int stbi__bit_reverse(int v, int bits) +{ + STBI_ASSERT(bits <= 16); + // to bit reverse n bits, reverse 16 and shift + // e.g. 11 bits, bit reverse and shift away 5 + return stbi__bitreverse16(v) >> (16-bits); +} + +static int stbi__zbuild_huffman(stbi__zhuffman *z, const stbi_uc *sizelist, int num) +{ + int i,k=0; + int code, next_code[16], sizes[17]; + + // DEFLATE spec for generating codes + memset(sizes, 0, sizeof(sizes)); + memset(z->fast, 0, sizeof(z->fast)); + for (i=0; i < num; ++i) + ++sizes[sizelist[i]]; + sizes[0] = 0; + for (i=1; i < 16; ++i) + if (sizes[i] > (1 << i)) + return stbi__err("bad sizes", "Corrupt PNG"); + code = 0; + for (i=1; i < 16; ++i) { + next_code[i] = code; + z->firstcode[i] = (stbi__uint16) code; + z->firstsymbol[i] = (stbi__uint16) k; + code = (code + sizes[i]); + if (sizes[i]) + if (code-1 >= (1 << i)) return stbi__err("bad codelengths","Corrupt PNG"); + z->maxcode[i] = code << (16-i); // preshift for inner loop + code <<= 1; + k += sizes[i]; + } + z->maxcode[16] = 0x10000; // sentinel + for (i=0; i < num; ++i) { + int s = sizelist[i]; + if (s) { + int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s]; + stbi__uint16 fastv = (stbi__uint16) ((s << 9) | i); + z->size [c] = (stbi_uc ) s; + z->value[c] = (stbi__uint16) i; + if (s <= STBI__ZFAST_BITS) { + int j = stbi__bit_reverse(next_code[s],s); + while (j < (1 << STBI__ZFAST_BITS)) { + z->fast[j] = fastv; + j += (1 << s); + } + } + ++next_code[s]; + } + } + return 1; +} + +// zlib-from-memory implementation for PNG reading +// because PNG allows splitting the zlib stream arbitrarily, +// and it's annoying structurally to have PNG call ZLIB call PNG, +// we require PNG read all the IDATs and combine them into a single +// memory buffer + +typedef struct +{ + stbi_uc *zbuffer, *zbuffer_end; + int num_bits; + int hit_zeof_once; + stbi__uint32 code_buffer; + + char *zout; + char *zout_start; + char *zout_end; + int z_expandable; + + stbi__zhuffman z_length, z_distance; +} stbi__zbuf; + +stbi_inline static int stbi__zeof(stbi__zbuf *z) +{ + return (z->zbuffer >= z->zbuffer_end); +} + +stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z) +{ + return stbi__zeof(z) ? 0 : *z->zbuffer++; +} + +static void stbi__fill_bits(stbi__zbuf *z) +{ + do { + if (z->code_buffer >= (1U << z->num_bits)) { + z->zbuffer = z->zbuffer_end; /* treat this as EOF so we fail. */ + return; + } + z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; + z->num_bits += 8; + } while (z->num_bits <= 24); +} + +stbi_inline static unsigned int stbi__zreceive(stbi__zbuf *z, int n) +{ + unsigned int k; + if (z->num_bits < n) stbi__fill_bits(z); + k = z->code_buffer & ((1 << n) - 1); + z->code_buffer >>= n; + z->num_bits -= n; + return k; +} + +static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) +{ + int b,s,k; + // not resolved by fast table, so compute it the slow way + // use jpeg approach, which requires MSbits at top + k = stbi__bit_reverse(a->code_buffer, 16); + for (s=STBI__ZFAST_BITS+1; ; ++s) + if (k < z->maxcode[s]) + break; + if (s >= 16) return -1; // invalid code! + // code size is s, so: + b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; + if (b >= STBI__ZNSYMS) return -1; // some data was corrupt somewhere! + if (z->size[b] != s) return -1; // was originally an assert, but report failure instead. + a->code_buffer >>= s; + a->num_bits -= s; + return z->value[b]; +} + +stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) +{ + int b,s; + if (a->num_bits < 16) { + if (stbi__zeof(a)) { + if (!a->hit_zeof_once) { + // This is the first time we hit eof, insert 16 extra padding btis + // to allow us to keep going; if we actually consume any of them + // though, that is invalid data. This is caught later. + a->hit_zeof_once = 1; + a->num_bits += 16; // add 16 implicit zero bits + } else { + // We already inserted our extra 16 padding bits and are again + // out, this stream is actually prematurely terminated. + return -1; + } + } else { + stbi__fill_bits(a); + } + } + b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; + if (b) { + s = b >> 9; + a->code_buffer >>= s; + a->num_bits -= s; + return b & 511; + } + return stbi__zhuffman_decode_slowpath(a, z); +} + +static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes +{ + char *q; + unsigned int cur, limit, old_limit; + z->zout = zout; + if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG"); + cur = (unsigned int) (z->zout - z->zout_start); + limit = old_limit = (unsigned) (z->zout_end - z->zout_start); + if (UINT_MAX - cur < (unsigned) n) return stbi__err("outofmem", "Out of memory"); + while (cur + n > limit) { + if(limit > UINT_MAX / 2) return stbi__err("outofmem", "Out of memory"); + limit *= 2; + } + q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); + STBI_NOTUSED(old_limit); + if (q == NULL) return stbi__err("outofmem", "Out of memory"); + z->zout_start = q; + z->zout = q + cur; + z->zout_end = q + limit; + return 1; +} + +static const int stbi__zlength_base[31] = { + 3,4,5,6,7,8,9,10,11,13, + 15,17,19,23,27,31,35,43,51,59, + 67,83,99,115,131,163,195,227,258,0,0 }; + +static const int stbi__zlength_extra[31]= +{ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 }; + +static const int stbi__zdist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, +257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0}; + +static const int stbi__zdist_extra[32] = +{ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; + +static int stbi__parse_huffman_block(stbi__zbuf *a) +{ + char *zout = a->zout; + for(;;) { + int z = stbi__zhuffman_decode(a, &a->z_length); + if (z < 256) { + if (z < 0) return stbi__err("bad huffman code","Corrupt PNG"); // error in huffman codes + if (zout >= a->zout_end) { + if (!stbi__zexpand(a, zout, 1)) return 0; + zout = a->zout; + } + *zout++ = (char) z; + } else { + stbi_uc *p; + int len,dist; + if (z == 256) { + a->zout = zout; + if (a->hit_zeof_once && a->num_bits < 16) { + // The first time we hit zeof, we inserted 16 extra zero bits into our bit + // buffer so the decoder can just do its speculative decoding. But if we + // actually consumed any of those bits (which is the case when num_bits < 16), + // the stream actually read past the end so it is malformed. + return stbi__err("unexpected end","Corrupt PNG"); + } + return 1; + } + if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data + z -= 257; + len = stbi__zlength_base[z]; + if (stbi__zlength_extra[z]) len += stbi__zreceive(a, stbi__zlength_extra[z]); + z = stbi__zhuffman_decode(a, &a->z_distance); + if (z < 0 || z >= 30) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, distance codes 30 and 31 must not appear in compressed data + dist = stbi__zdist_base[z]; + if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); + if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); + if (len > a->zout_end - zout) { + if (!stbi__zexpand(a, zout, len)) return 0; + zout = a->zout; + } + p = (stbi_uc *) (zout - dist); + if (dist == 1) { // run of one byte; common in images. + stbi_uc v = *p; + if (len) { do *zout++ = v; while (--len); } + } else { + if (len) { do *zout++ = *p++; while (--len); } + } + } + } +} + +static int stbi__compute_huffman_codes(stbi__zbuf *a) +{ + static const stbi_uc length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; + stbi__zhuffman z_codelength; + stbi_uc lencodes[286+32+137];//padding for maximum single op + stbi_uc codelength_sizes[19]; + int i,n; + + int hlit = stbi__zreceive(a,5) + 257; + int hdist = stbi__zreceive(a,5) + 1; + int hclen = stbi__zreceive(a,4) + 4; + int ntot = hlit + hdist; + + memset(codelength_sizes, 0, sizeof(codelength_sizes)); + for (i=0; i < hclen; ++i) { + int s = stbi__zreceive(a,3); + codelength_sizes[length_dezigzag[i]] = (stbi_uc) s; + } + if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0; + + n = 0; + while (n < ntot) { + int c = stbi__zhuffman_decode(a, &z_codelength); + if (c < 0 || c >= 19) return stbi__err("bad codelengths", "Corrupt PNG"); + if (c < 16) + lencodes[n++] = (stbi_uc) c; + else { + stbi_uc fill = 0; + if (c == 16) { + c = stbi__zreceive(a,2)+3; + if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); + fill = lencodes[n-1]; + } else if (c == 17) { + c = stbi__zreceive(a,3)+3; + } else if (c == 18) { + c = stbi__zreceive(a,7)+11; + } else { + return stbi__err("bad codelengths", "Corrupt PNG"); + } + if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); + memset(lencodes+n, fill, c); + n += c; + } + } + if (n != ntot) return stbi__err("bad codelengths","Corrupt PNG"); + if (!stbi__zbuild_huffman(&a->z_length, lencodes, hlit)) return 0; + if (!stbi__zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0; + return 1; +} + +static int stbi__parse_uncompressed_block(stbi__zbuf *a) +{ + stbi_uc header[4]; + int len,nlen,k; + if (a->num_bits & 7) + stbi__zreceive(a, a->num_bits & 7); // discard + // drain the bit-packed data into header + k = 0; + while (a->num_bits > 0) { + header[k++] = (stbi_uc) (a->code_buffer & 255); // suppress MSVC run-time check + a->code_buffer >>= 8; + a->num_bits -= 8; + } + if (a->num_bits < 0) return stbi__err("zlib corrupt","Corrupt PNG"); + // now fill header the normal way + while (k < 4) + header[k++] = stbi__zget8(a); + len = header[1] * 256 + header[0]; + nlen = header[3] * 256 + header[2]; + if (nlen != (len ^ 0xffff)) return stbi__err("zlib corrupt","Corrupt PNG"); + if (a->zbuffer + len > a->zbuffer_end) return stbi__err("read past buffer","Corrupt PNG"); + if (a->zout + len > a->zout_end) + if (!stbi__zexpand(a, a->zout, len)) return 0; + memcpy(a->zout, a->zbuffer, len); + a->zbuffer += len; + a->zout += len; + return 1; +} + +static int stbi__parse_zlib_header(stbi__zbuf *a) +{ + int cmf = stbi__zget8(a); + int cm = cmf & 15; + /* int cinfo = cmf >> 4; */ + int flg = stbi__zget8(a); + if (stbi__zeof(a)) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec + if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec + if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png + if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png + // window = 1 << (8 + cinfo)... but who cares, we fully buffer output + return 1; +} + +static const stbi_uc stbi__zdefault_length[STBI__ZNSYMS] = +{ + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8 +}; +static const stbi_uc stbi__zdefault_distance[32] = +{ + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 +}; +/* +Init algorithm: +{ + int i; // use <= to match clearly with spec + for (i=0; i <= 143; ++i) stbi__zdefault_length[i] = 8; + for ( ; i <= 255; ++i) stbi__zdefault_length[i] = 9; + for ( ; i <= 279; ++i) stbi__zdefault_length[i] = 7; + for ( ; i <= 287; ++i) stbi__zdefault_length[i] = 8; + + for (i=0; i <= 31; ++i) stbi__zdefault_distance[i] = 5; +} +*/ + +static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) +{ + int final, type; + if (parse_header) + if (!stbi__parse_zlib_header(a)) return 0; + a->num_bits = 0; + a->code_buffer = 0; + a->hit_zeof_once = 0; + do { + final = stbi__zreceive(a,1); + type = stbi__zreceive(a,2); + if (type == 0) { + if (!stbi__parse_uncompressed_block(a)) return 0; + } else if (type == 3) { + return 0; + } else { + if (type == 1) { + // use fixed code lengths + if (!stbi__zbuild_huffman(&a->z_length , stbi__zdefault_length , STBI__ZNSYMS)) return 0; + if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance, 32)) return 0; + } else { + if (!stbi__compute_huffman_codes(a)) return 0; + } + if (!stbi__parse_huffman_block(a)) return 0; + } + } while (!final); + return 1; +} + +static int stbi__do_zlib(stbi__zbuf *a, char *obuf, int olen, int exp, int parse_header) +{ + a->zout_start = obuf; + a->zout = obuf; + a->zout_end = obuf + olen; + a->z_expandable = exp; + + return stbi__parse_zlib(a, parse_header); +} + +STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(initial_size); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer + len; + if (stbi__do_zlib(&a, p, initial_size, 1, 1)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF char *stbi_zlib_decode_malloc(char const *buffer, int len, int *outlen) +{ + return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen); +} + +STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(initial_size); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer + len; + if (stbi__do_zlib(&a, p, initial_size, 1, parse_header)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, char const *ibuffer, int ilen) +{ + stbi__zbuf a; + a.zbuffer = (stbi_uc *) ibuffer; + a.zbuffer_end = (stbi_uc *) ibuffer + ilen; + if (stbi__do_zlib(&a, obuffer, olen, 0, 1)) + return (int) (a.zout - a.zout_start); + else + return -1; +} + +STBIDEF char *stbi_zlib_decode_noheader_malloc(char const *buffer, int len, int *outlen) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(16384); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer+len; + if (stbi__do_zlib(&a, p, 16384, 1, 0)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen) +{ + stbi__zbuf a; + a.zbuffer = (stbi_uc *) ibuffer; + a.zbuffer_end = (stbi_uc *) ibuffer + ilen; + if (stbi__do_zlib(&a, obuffer, olen, 0, 0)) + return (int) (a.zout - a.zout_start); + else + return -1; +} +#endif + +// public domain "baseline" PNG decoder v0.10 Sean Barrett 2006-11-18 +// simple implementation +// - only 8-bit samples +// - no CRC checking +// - allocates lots of intermediate memory +// - avoids problem of streaming data between subsystems +// - avoids explicit window management +// performance +// - uses stb_zlib, a PD zlib implementation with fast huffman decoding + +#ifndef STBI_NO_PNG +typedef struct +{ + stbi__uint32 length; + stbi__uint32 type; +} stbi__pngchunk; + +static stbi__pngchunk stbi__get_chunk_header(stbi__context *s) +{ + stbi__pngchunk c; + c.length = stbi__get32be(s); + c.type = stbi__get32be(s); + return c; +} + +static int stbi__check_png_header(stbi__context *s) +{ + static const stbi_uc png_sig[8] = { 137,80,78,71,13,10,26,10 }; + int i; + for (i=0; i < 8; ++i) + if (stbi__get8(s) != png_sig[i]) return stbi__err("bad png sig","Not a PNG"); + return 1; +} + +typedef struct +{ + stbi__context *s; + stbi_uc *idata, *expanded, *out; + int depth; +} stbi__png; + + +enum { + STBI__F_none=0, + STBI__F_sub=1, + STBI__F_up=2, + STBI__F_avg=3, + STBI__F_paeth=4, + // synthetic filter used for first scanline to avoid needing a dummy row of 0s + STBI__F_avg_first +}; + +static stbi_uc first_row_filter[5] = +{ + STBI__F_none, + STBI__F_sub, + STBI__F_none, + STBI__F_avg_first, + STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub +}; + +static int stbi__paeth(int a, int b, int c) +{ + // This formulation looks very different from the reference in the PNG spec, but is + // actually equivalent and has favorable data dependencies and admits straightforward + // generation of branch-free code, which helps performance significantly. + int thresh = c*3 - (a + b); + int lo = a < b ? a : b; + int hi = a < b ? b : a; + int t0 = (hi <= thresh) ? lo : c; + int t1 = (thresh <= lo) ? hi : t0; + return t1; +} + +static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; + +// adds an extra all-255 alpha channel +// dest == src is legal +// img_n must be 1 or 3 +static void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n) +{ + int i; + // must process data backwards since we allow dest==src + if (img_n == 1) { + for (i=x-1; i >= 0; --i) { + dest[i*2+1] = 255; + dest[i*2+0] = src[i]; + } + } else { + STBI_ASSERT(img_n == 3); + for (i=x-1; i >= 0; --i) { + dest[i*4+3] = 255; + dest[i*4+2] = src[i*3+2]; + dest[i*4+1] = src[i*3+1]; + dest[i*4+0] = src[i*3+0]; + } + } +} + +// create the png data from post-deflated data +static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color) +{ + int bytes = (depth == 16 ? 2 : 1); + stbi__context *s = a->s; + stbi__uint32 i,j,stride = x*out_n*bytes; + stbi__uint32 img_len, img_width_bytes; + stbi_uc *filter_buf; + int all_ok = 1; + int k; + int img_n = s->img_n; // copy it into a local for later + + int output_bytes = out_n*bytes; + int filter_bytes = img_n*bytes; + int width = x; + + STBI_ASSERT(out_n == s->img_n || out_n == s->img_n+1); + a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into + if (!a->out) return stbi__err("outofmem", "Out of memory"); + + // note: error exits here don't need to clean up a->out individually, + // stbi__do_png always does on error. + if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG"); + img_width_bytes = (((img_n * x * depth) + 7) >> 3); + if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err("too large", "Corrupt PNG"); + img_len = (img_width_bytes + 1) * y; + + // we used to check for exact match between raw_len and img_len on non-interlaced PNGs, + // but issue #276 reported a PNG in the wild that had extra data at the end (all zeros), + // so just check for raw_len < img_len always. + if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); + + // Allocate two scan lines worth of filter workspace buffer. + filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0); + if (!filter_buf) return stbi__err("outofmem", "Out of memory"); + + // Filtering for low-bit-depth images + if (depth < 8) { + filter_bytes = 1; + width = img_width_bytes; + } + + for (j=0; j < y; ++j) { + // cur/prior filter buffers alternate + stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes; + stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes; + stbi_uc *dest = a->out + stride*j; + int nk = width * filter_bytes; + int filter = *raw++; + + // check filter type + if (filter > 4) { + all_ok = stbi__err("invalid filter","Corrupt PNG"); + break; + } + + // if first row, use special filter that doesn't sample previous row + if (j == 0) filter = first_row_filter[filter]; + + // perform actual filtering + switch (filter) { + case STBI__F_none: + memcpy(cur, raw, nk); + break; + case STBI__F_sub: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); + break; + case STBI__F_up: + for (k = 0; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); + break; + case STBI__F_avg: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); + break; + case STBI__F_paeth: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0) + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes])); + break; + case STBI__F_avg_first: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); + break; + } + + raw += nk; + + // expand decoded bits in cur to dest, also adding an extra alpha channel if desired + if (depth < 8) { + stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range + stbi_uc *in = cur; + stbi_uc *out = dest; + stbi_uc inb = 0; + stbi__uint32 nsmp = x*img_n; + + // expand bits to bytes first + if (depth == 4) { + for (i=0; i < nsmp; ++i) { + if ((i & 1) == 0) inb = *in++; + *out++ = scale * (inb >> 4); + inb <<= 4; + } + } else if (depth == 2) { + for (i=0; i < nsmp; ++i) { + if ((i & 3) == 0) inb = *in++; + *out++ = scale * (inb >> 6); + inb <<= 2; + } + } else { + STBI_ASSERT(depth == 1); + for (i=0; i < nsmp; ++i) { + if ((i & 7) == 0) inb = *in++; + *out++ = scale * (inb >> 7); + inb <<= 1; + } + } + + // insert alpha=255 values if desired + if (img_n != out_n) + stbi__create_png_alpha_expand8(dest, dest, x, img_n); + } else if (depth == 8) { + if (img_n == out_n) + memcpy(dest, cur, x*img_n); + else + stbi__create_png_alpha_expand8(dest, cur, x, img_n); + } else if (depth == 16) { + // convert the image data from big-endian to platform-native + stbi__uint16 *dest16 = (stbi__uint16*)dest; + stbi__uint32 nsmp = x*img_n; + + if (img_n == out_n) { + for (i = 0; i < nsmp; ++i, ++dest16, cur += 2) + *dest16 = (cur[0] << 8) | cur[1]; + } else { + STBI_ASSERT(img_n+1 == out_n); + if (img_n == 1) { + for (i = 0; i < x; ++i, dest16 += 2, cur += 2) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = 0xffff; + } + } else { + STBI_ASSERT(img_n == 3); + for (i = 0; i < x; ++i, dest16 += 4, cur += 6) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = (cur[2] << 8) | cur[3]; + dest16[2] = (cur[4] << 8) | cur[5]; + dest16[3] = 0xffff; + } + } + } + } + } + + STBI_FREE(filter_buf); + if (!all_ok) return 0; + + return 1; +} + +static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint32 image_data_len, int out_n, int depth, int color, int interlaced) +{ + int bytes = (depth == 16 ? 2 : 1); + int out_bytes = out_n * bytes; + stbi_uc *final; + int p; + if (!interlaced) + return stbi__create_png_image_raw(a, image_data, image_data_len, out_n, a->s->img_x, a->s->img_y, depth, color); + + // de-interlacing + final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0); + if (!final) return stbi__err("outofmem", "Out of memory"); + for (p=0; p < 7; ++p) { + int xorig[] = { 0,4,0,2,0,1,0 }; + int yorig[] = { 0,0,4,0,2,0,1 }; + int xspc[] = { 8,8,4,4,2,2,1 }; + int yspc[] = { 8,8,8,4,4,2,2 }; + int i,j,x,y; + // pass1_x[4] = 0, pass1_x[5] = 1, pass1_x[12] = 1 + x = (a->s->img_x - xorig[p] + xspc[p]-1) / xspc[p]; + y = (a->s->img_y - yorig[p] + yspc[p]-1) / yspc[p]; + if (x && y) { + stbi__uint32 img_len = ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y; + if (!stbi__create_png_image_raw(a, image_data, image_data_len, out_n, x, y, depth, color)) { + STBI_FREE(final); + return 0; + } + for (j=0; j < y; ++j) { + for (i=0; i < x; ++i) { + int out_y = j*yspc[p]+yorig[p]; + int out_x = i*xspc[p]+xorig[p]; + memcpy(final + out_y*a->s->img_x*out_bytes + out_x*out_bytes, + a->out + (j*x+i)*out_bytes, out_bytes); + } + } + STBI_FREE(a->out); + image_data += img_len; + image_data_len -= img_len; + } + } + a->out = final; + + return 1; +} + +static int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi_uc *p = z->out; + + // compute color-based transparency, assuming we've + // already got 255 as the alpha value in the output + STBI_ASSERT(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i=0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 255); + p += 2; + } + } else { + for (i=0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int out_n) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi__uint16 *p = (stbi__uint16*) z->out; + + // compute color-based transparency, assuming we've + // already got 65535 as the alpha value in the output + STBI_ASSERT(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i = 0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 65535); + p += 2; + } + } else { + for (i = 0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int pal_img_n) +{ + stbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y; + stbi_uc *p, *temp_out, *orig = a->out; + + p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0); + if (p == NULL) return stbi__err("outofmem", "Out of memory"); + + // between here and free(out) below, exitting would leak + temp_out = p; + + if (pal_img_n == 3) { + for (i=0; i < pixel_count; ++i) { + int n = orig[i]*4; + p[0] = palette[n ]; + p[1] = palette[n+1]; + p[2] = palette[n+2]; + p += 3; + } + } else { + for (i=0; i < pixel_count; ++i) { + int n = orig[i]*4; + p[0] = palette[n ]; + p[1] = palette[n+1]; + p[2] = palette[n+2]; + p[3] = palette[n+3]; + p += 4; + } + } + STBI_FREE(a->out); + a->out = temp_out; + + STBI_NOTUSED(len); + + return 1; +} + +static int stbi__unpremultiply_on_load_global = 0; +static int stbi__de_iphone_flag_global = 0; + +STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) +{ + stbi__unpremultiply_on_load_global = flag_true_if_should_unpremultiply; +} + +STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert) +{ + stbi__de_iphone_flag_global = flag_true_if_should_convert; +} + +#ifndef STBI_THREAD_LOCAL +#define stbi__unpremultiply_on_load stbi__unpremultiply_on_load_global +#define stbi__de_iphone_flag stbi__de_iphone_flag_global +#else +static STBI_THREAD_LOCAL int stbi__unpremultiply_on_load_local, stbi__unpremultiply_on_load_set; +static STBI_THREAD_LOCAL int stbi__de_iphone_flag_local, stbi__de_iphone_flag_set; + +STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply) +{ + stbi__unpremultiply_on_load_local = flag_true_if_should_unpremultiply; + stbi__unpremultiply_on_load_set = 1; +} + +STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert) +{ + stbi__de_iphone_flag_local = flag_true_if_should_convert; + stbi__de_iphone_flag_set = 1; +} + +#define stbi__unpremultiply_on_load (stbi__unpremultiply_on_load_set \ + ? stbi__unpremultiply_on_load_local \ + : stbi__unpremultiply_on_load_global) +#define stbi__de_iphone_flag (stbi__de_iphone_flag_set \ + ? stbi__de_iphone_flag_local \ + : stbi__de_iphone_flag_global) +#endif // STBI_THREAD_LOCAL + +static void stbi__de_iphone(stbi__png *z) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi_uc *p = z->out; + + if (s->img_out_n == 3) { // convert bgr to rgb + for (i=0; i < pixel_count; ++i) { + stbi_uc t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 3; + } + } else { + STBI_ASSERT(s->img_out_n == 4); + if (stbi__unpremultiply_on_load) { + // convert bgr to rgb and unpremultiply + for (i=0; i < pixel_count; ++i) { + stbi_uc a = p[3]; + stbi_uc t = p[0]; + if (a) { + stbi_uc half = a / 2; + p[0] = (p[2] * 255 + half) / a; + p[1] = (p[1] * 255 + half) / a; + p[2] = ( t * 255 + half) / a; + } else { + p[0] = p[2]; + p[2] = t; + } + p += 4; + } + } else { + // convert bgr to rgb + for (i=0; i < pixel_count; ++i) { + stbi_uc t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 4; + } + } + } +} + +#define STBI__PNG_TYPE(a,b,c,d) (((unsigned) (a) << 24) + ((unsigned) (b) << 16) + ((unsigned) (c) << 8) + (unsigned) (d)) + +static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) +{ + stbi_uc palette[1024], pal_img_n=0; + stbi_uc has_trans=0, tc[3]={0}; + stbi__uint16 tc16[3]; + stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0; + int first=1,k,interlace=0, color=0, is_iphone=0; + stbi__context *s = z->s; + + z->expanded = NULL; + z->idata = NULL; + z->out = NULL; + + if (!stbi__check_png_header(s)) return 0; + + if (scan == STBI__SCAN_type) return 1; + + for (;;) { + stbi__pngchunk c = stbi__get_chunk_header(s); + switch (c.type) { + case STBI__PNG_TYPE('C','g','B','I'): + is_iphone = 1; + stbi__skip(s, c.length); + break; + case STBI__PNG_TYPE('I','H','D','R'): { + int comp,filter; + if (!first) return stbi__err("multiple IHDR","Corrupt PNG"); + first = 0; + if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG"); + s->img_x = stbi__get32be(s); + s->img_y = stbi__get32be(s); + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); + color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG"); + if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG"); + if (color == 3) pal_img_n = 3; else if (color & 1) return stbi__err("bad ctype","Corrupt PNG"); + comp = stbi__get8(s); if (comp) return stbi__err("bad comp method","Corrupt PNG"); + filter= stbi__get8(s); if (filter) return stbi__err("bad filter method","Corrupt PNG"); + interlace = stbi__get8(s); if (interlace>1) return stbi__err("bad interlace method","Corrupt PNG"); + if (!s->img_x || !s->img_y) return stbi__err("0-pixel image","Corrupt PNG"); + if (!pal_img_n) { + s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); + if ((1 << 30) / s->img_x / s->img_n < s->img_y) return stbi__err("too large", "Image too large to decode"); + } else { + // if paletted, then pal_n is our final components, and + // img_n is # components to decompress/filter. + s->img_n = 1; + if ((1 << 30) / s->img_x / 4 < s->img_y) return stbi__err("too large","Corrupt PNG"); + } + // even with SCAN_header, have to scan to see if we have a tRNS + break; + } + + case STBI__PNG_TYPE('P','L','T','E'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (c.length > 256*3) return stbi__err("invalid PLTE","Corrupt PNG"); + pal_len = c.length / 3; + if (pal_len * 3 != c.length) return stbi__err("invalid PLTE","Corrupt PNG"); + for (i=0; i < pal_len; ++i) { + palette[i*4+0] = stbi__get8(s); + palette[i*4+1] = stbi__get8(s); + palette[i*4+2] = stbi__get8(s); + palette[i*4+3] = 255; + } + break; + } + + case STBI__PNG_TYPE('t','R','N','S'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (z->idata) return stbi__err("tRNS after IDAT","Corrupt PNG"); + if (pal_img_n) { + if (scan == STBI__SCAN_header) { s->img_n = 4; return 1; } + if (pal_len == 0) return stbi__err("tRNS before PLTE","Corrupt PNG"); + if (c.length > pal_len) return stbi__err("bad tRNS len","Corrupt PNG"); + pal_img_n = 4; + for (i=0; i < c.length; ++i) + palette[i*4+3] = stbi__get8(s); + } else { + if (!(s->img_n & 1)) return stbi__err("tRNS with alpha","Corrupt PNG"); + if (c.length != (stbi__uint32) s->img_n*2) return stbi__err("bad tRNS len","Corrupt PNG"); + has_trans = 1; + // non-paletted with tRNS = constant alpha. if header-scanning, we can stop now. + if (scan == STBI__SCAN_header) { ++s->img_n; return 1; } + if (z->depth == 16) { + for (k = 0; k < s->img_n; ++k) tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is + } else { + for (k = 0; k < s->img_n; ++k) tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth]; // non 8-bit images will be larger + } + } + break; + } + + case STBI__PNG_TYPE('I','D','A','T'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (pal_img_n && !pal_len) return stbi__err("no PLTE","Corrupt PNG"); + if (scan == STBI__SCAN_header) { + // header scan definitely stops at first IDAT + if (pal_img_n) + s->img_n = pal_img_n; + return 1; + } + if (c.length > (1u << 30)) return stbi__err("IDAT size limit", "IDAT section larger than 2^30 bytes"); + if ((int)(ioff + c.length) < (int)ioff) return 0; + if (ioff + c.length > idata_limit) { + stbi__uint32 idata_limit_old = idata_limit; + stbi_uc *p; + if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096; + while (ioff + c.length > idata_limit) + idata_limit *= 2; + STBI_NOTUSED(idata_limit_old); + p = (stbi_uc *) STBI_REALLOC_SIZED(z->idata, idata_limit_old, idata_limit); if (p == NULL) return stbi__err("outofmem", "Out of memory"); + z->idata = p; + } + if (!stbi__getn(s, z->idata+ioff,c.length)) return stbi__err("outofdata","Corrupt PNG"); + ioff += c.length; + break; + } + + case STBI__PNG_TYPE('I','E','N','D'): { + stbi__uint32 raw_len, bpl; + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (scan != STBI__SCAN_load) return 1; + if (z->idata == NULL) return stbi__err("no IDAT","Corrupt PNG"); + // initial guess for decoded data size to avoid unnecessary reallocs + bpl = (s->img_x * z->depth + 7) / 8; // bytes per line, per component + raw_len = bpl * s->img_y * s->img_n /* pixels */ + s->img_y /* filter mode per row */; + z->expanded = (stbi_uc *) stbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, raw_len, (int *) &raw_len, !is_iphone); + if (z->expanded == NULL) return 0; // zlib should set error + STBI_FREE(z->idata); z->idata = NULL; + if ((req_comp == s->img_n+1 && req_comp != 3 && !pal_img_n) || has_trans) + s->img_out_n = s->img_n+1; + else + s->img_out_n = s->img_n; + if (!stbi__create_png_image(z, z->expanded, raw_len, s->img_out_n, z->depth, color, interlace)) return 0; + if (has_trans) { + if (z->depth == 16) { + if (!stbi__compute_transparency16(z, tc16, s->img_out_n)) return 0; + } else { + if (!stbi__compute_transparency(z, tc, s->img_out_n)) return 0; + } + } + if (is_iphone && stbi__de_iphone_flag && s->img_out_n > 2) + stbi__de_iphone(z); + if (pal_img_n) { + // pal_img_n == 3 or 4 + s->img_n = pal_img_n; // record the actual colors we had + s->img_out_n = pal_img_n; + if (req_comp >= 3) s->img_out_n = req_comp; + if (!stbi__expand_png_palette(z, palette, pal_len, s->img_out_n)) + return 0; + } else if (has_trans) { + // non-paletted image with tRNS -> source image has (constant) alpha + ++s->img_n; + } + STBI_FREE(z->expanded); z->expanded = NULL; + // end of PNG chunk, read and skip CRC + stbi__get32be(s); + return 1; + } + + default: + // if critical, fail + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if ((c.type & (1 << 29)) == 0) { + #ifndef STBI_NO_FAILURE_STRINGS + // not threadsafe + static char invalid_chunk[] = "XXXX PNG chunk not known"; + invalid_chunk[0] = STBI__BYTECAST(c.type >> 24); + invalid_chunk[1] = STBI__BYTECAST(c.type >> 16); + invalid_chunk[2] = STBI__BYTECAST(c.type >> 8); + invalid_chunk[3] = STBI__BYTECAST(c.type >> 0); + #endif + return stbi__err(invalid_chunk, "PNG not supported: unknown PNG chunk type"); + } + stbi__skip(s, c.length); + break; + } + // end of PNG chunk, read and skip CRC + stbi__get32be(s); + } +} + +static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, stbi__result_info *ri) +{ + void *result=NULL; + if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); + if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { + if (p->depth <= 8) + ri->bits_per_channel = 8; + else if (p->depth == 16) + ri->bits_per_channel = 16; + else + return stbi__errpuc("bad bits_per_channel", "PNG not supported: unsupported color depth"); + result = p->out; + p->out = NULL; + if (req_comp && req_comp != p->s->img_out_n) { + if (ri->bits_per_channel == 8) + result = stbi__convert_format((unsigned char *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); + else + result = stbi__convert_format16((stbi__uint16 *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); + p->s->img_out_n = req_comp; + if (result == NULL) return result; + } + *x = p->s->img_x; + *y = p->s->img_y; + if (n) *n = p->s->img_n; + } + STBI_FREE(p->out); p->out = NULL; + STBI_FREE(p->expanded); p->expanded = NULL; + STBI_FREE(p->idata); p->idata = NULL; + + return result; +} + +static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi__png p; + p.s = s; + return stbi__do_png(&p, x,y,comp,req_comp, ri); +} + +static int stbi__png_test(stbi__context *s) +{ + int r; + r = stbi__check_png_header(s); + stbi__rewind(s); + return r; +} + +static int stbi__png_info_raw(stbi__png *p, int *x, int *y, int *comp) +{ + if (!stbi__parse_png_file(p, STBI__SCAN_header, 0)) { + stbi__rewind( p->s ); + return 0; + } + if (x) *x = p->s->img_x; + if (y) *y = p->s->img_y; + if (comp) *comp = p->s->img_n; + return 1; +} + +static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp) +{ + stbi__png p; + p.s = s; + return stbi__png_info_raw(&p, x, y, comp); +} + +static int stbi__png_is16(stbi__context *s) +{ + stbi__png p; + p.s = s; + if (!stbi__png_info_raw(&p, NULL, NULL, NULL)) + return 0; + if (p.depth != 16) { + stbi__rewind(p.s); + return 0; + } + return 1; +} +#endif + +// Microsoft/Windows BMP image + +#ifndef STBI_NO_BMP +static int stbi__bmp_test_raw(stbi__context *s) +{ + int r; + int sz; + if (stbi__get8(s) != 'B') return 0; + if (stbi__get8(s) != 'M') return 0; + stbi__get32le(s); // discard filesize + stbi__get16le(s); // discard reserved + stbi__get16le(s); // discard reserved + stbi__get32le(s); // discard data offset + sz = stbi__get32le(s); + r = (sz == 12 || sz == 40 || sz == 56 || sz == 108 || sz == 124); + return r; +} + +static int stbi__bmp_test(stbi__context *s) +{ + int r = stbi__bmp_test_raw(s); + stbi__rewind(s); + return r; +} + + +// returns 0..31 for the highest set bit +static int stbi__high_bit(unsigned int z) +{ + int n=0; + if (z == 0) return -1; + if (z >= 0x10000) { n += 16; z >>= 16; } + if (z >= 0x00100) { n += 8; z >>= 8; } + if (z >= 0x00010) { n += 4; z >>= 4; } + if (z >= 0x00004) { n += 2; z >>= 2; } + if (z >= 0x00002) { n += 1;/* >>= 1;*/ } + return n; +} + +static int stbi__bitcount(unsigned int a) +{ + a = (a & 0x55555555) + ((a >> 1) & 0x55555555); // max 2 + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); // max 4 + a = (a + (a >> 4)) & 0x0f0f0f0f; // max 8 per 4, now 8 bits + a = (a + (a >> 8)); // max 16 per 8 bits + a = (a + (a >> 16)); // max 32 per 8 bits + return a & 0xff; +} + +// extract an arbitrarily-aligned N-bit value (N=bits) +// from v, and then make it 8-bits long and fractionally +// extend it to full full range. +static int stbi__shiftsigned(unsigned int v, int shift, int bits) +{ + static unsigned int mul_table[9] = { + 0, + 0xff/*0b11111111*/, 0x55/*0b01010101*/, 0x49/*0b01001001*/, 0x11/*0b00010001*/, + 0x21/*0b00100001*/, 0x41/*0b01000001*/, 0x81/*0b10000001*/, 0x01/*0b00000001*/, + }; + static unsigned int shift_table[9] = { + 0, 0,0,1,0,2,4,6,0, + }; + if (shift < 0) + v <<= -shift; + else + v >>= shift; + STBI_ASSERT(v < 256); + v >>= (8-bits); + STBI_ASSERT(bits >= 0 && bits <= 8); + return (int) ((unsigned) v * mul_table[bits]) >> shift_table[bits]; +} + +typedef struct +{ + int bpp, offset, hsz; + unsigned int mr,mg,mb,ma, all_a; + int extra_read; +} stbi__bmp_data; + +static int stbi__bmp_set_mask_defaults(stbi__bmp_data *info, int compress) +{ + // BI_BITFIELDS specifies masks explicitly, don't override + if (compress == 3) + return 1; + + if (compress == 0) { + if (info->bpp == 16) { + info->mr = 31u << 10; + info->mg = 31u << 5; + info->mb = 31u << 0; + } else if (info->bpp == 32) { + info->mr = 0xffu << 16; + info->mg = 0xffu << 8; + info->mb = 0xffu << 0; + info->ma = 0xffu << 24; + info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0 + } else { + // otherwise, use defaults, which is all-0 + info->mr = info->mg = info->mb = info->ma = 0; + } + return 1; + } + return 0; // error +} + +static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) +{ + int hsz; + if (stbi__get8(s) != 'B' || stbi__get8(s) != 'M') return stbi__errpuc("not BMP", "Corrupt BMP"); + stbi__get32le(s); // discard filesize + stbi__get16le(s); // discard reserved + stbi__get16le(s); // discard reserved + info->offset = stbi__get32le(s); + info->hsz = hsz = stbi__get32le(s); + info->mr = info->mg = info->mb = info->ma = 0; + info->extra_read = 14; + + if (info->offset < 0) return stbi__errpuc("bad BMP", "bad BMP"); + + if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown"); + if (hsz == 12) { + s->img_x = stbi__get16le(s); + s->img_y = stbi__get16le(s); + } else { + s->img_x = stbi__get32le(s); + s->img_y = stbi__get32le(s); + } + if (stbi__get16le(s) != 1) return stbi__errpuc("bad BMP", "bad BMP"); + info->bpp = stbi__get16le(s); + if (hsz != 12) { + int compress = stbi__get32le(s); + if (compress == 1 || compress == 2) return stbi__errpuc("BMP RLE", "BMP type not supported: RLE"); + if (compress >= 4) return stbi__errpuc("BMP JPEG/PNG", "BMP type not supported: unsupported compression"); // this includes PNG/JPEG modes + if (compress == 3 && info->bpp != 16 && info->bpp != 32) return stbi__errpuc("bad BMP", "bad BMP"); // bitfields requires 16 or 32 bits/pixel + stbi__get32le(s); // discard sizeof + stbi__get32le(s); // discard hres + stbi__get32le(s); // discard vres + stbi__get32le(s); // discard colorsused + stbi__get32le(s); // discard max important + if (hsz == 40 || hsz == 56) { + if (hsz == 56) { + stbi__get32le(s); + stbi__get32le(s); + stbi__get32le(s); + stbi__get32le(s); + } + if (info->bpp == 16 || info->bpp == 32) { + if (compress == 0) { + stbi__bmp_set_mask_defaults(info, compress); + } else if (compress == 3) { + info->mr = stbi__get32le(s); + info->mg = stbi__get32le(s); + info->mb = stbi__get32le(s); + info->extra_read += 12; + // not documented, but generated by photoshop and handled by mspaint + if (info->mr == info->mg && info->mg == info->mb) { + // ?!?!? + return stbi__errpuc("bad BMP", "bad BMP"); + } + } else + return stbi__errpuc("bad BMP", "bad BMP"); + } + } else { + // V4/V5 header + int i; + if (hsz != 108 && hsz != 124) + return stbi__errpuc("bad BMP", "bad BMP"); + info->mr = stbi__get32le(s); + info->mg = stbi__get32le(s); + info->mb = stbi__get32le(s); + info->ma = stbi__get32le(s); + if (compress != 3) // override mr/mg/mb unless in BI_BITFIELDS mode, as per docs + stbi__bmp_set_mask_defaults(info, compress); + stbi__get32le(s); // discard color space + for (i=0; i < 12; ++i) + stbi__get32le(s); // discard color space parameters + if (hsz == 124) { + stbi__get32le(s); // discard rendering intent + stbi__get32le(s); // discard offset of profile data + stbi__get32le(s); // discard size of profile data + stbi__get32le(s); // discard reserved + } + } + } + return (void *) 1; +} + + +static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *out; + unsigned int mr=0,mg=0,mb=0,ma=0, all_a; + stbi_uc pal[256][4]; + int psize=0,i,j,width; + int flip_vertically, pad, target; + stbi__bmp_data info; + STBI_NOTUSED(ri); + + info.all_a = 255; + if (stbi__bmp_parse_header(s, &info) == NULL) + return NULL; // error code already set + + flip_vertically = ((int) s->img_y) > 0; + s->img_y = abs((int) s->img_y); + + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + mr = info.mr; + mg = info.mg; + mb = info.mb; + ma = info.ma; + all_a = info.all_a; + + if (info.hsz == 12) { + if (info.bpp < 24) + psize = (info.offset - info.extra_read - 24) / 3; + } else { + if (info.bpp < 16) + psize = (info.offset - info.extra_read - info.hsz) >> 2; + } + if (psize == 0) { + // accept some number of extra bytes after the header, but if the offset points either to before + // the header ends or implies a large amount of extra data, reject the file as malformed + int bytes_read_so_far = s->callback_already_read + (int)(s->img_buffer - s->img_buffer_original); + int header_limit = 1024; // max we actually read is below 256 bytes currently. + int extra_data_limit = 256*4; // what ordinarily goes here is a palette; 256 entries*4 bytes is its max size. + if (bytes_read_so_far <= 0 || bytes_read_so_far > header_limit) { + return stbi__errpuc("bad header", "Corrupt BMP"); + } + // we established that bytes_read_so_far is positive and sensible. + // the first half of this test rejects offsets that are either too small positives, or + // negative, and guarantees that info.offset >= bytes_read_so_far > 0. this in turn + // ensures the number computed in the second half of the test can't overflow. + if (info.offset < bytes_read_so_far || info.offset - bytes_read_so_far > extra_data_limit) { + return stbi__errpuc("bad offset", "Corrupt BMP"); + } else { + stbi__skip(s, info.offset - bytes_read_so_far); + } + } + + if (info.bpp == 24 && ma == 0xff000000) + s->img_n = 3; + else + s->img_n = ma ? 4 : 3; + if (req_comp && req_comp >= 3) // we can directly decode 3 or 4 + target = req_comp; + else + target = s->img_n; // if they want monochrome, we'll post-convert + + // sanity-check size + if (!stbi__mad3sizes_valid(target, s->img_x, s->img_y, 0)) + return stbi__errpuc("too large", "Corrupt BMP"); + + out = (stbi_uc *) stbi__malloc_mad3(target, s->img_x, s->img_y, 0); + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + if (info.bpp < 16) { + int z=0; + if (psize == 0 || psize > 256) { STBI_FREE(out); return stbi__errpuc("invalid", "Corrupt BMP"); } + for (i=0; i < psize; ++i) { + pal[i][2] = stbi__get8(s); + pal[i][1] = stbi__get8(s); + pal[i][0] = stbi__get8(s); + if (info.hsz != 12) stbi__get8(s); + pal[i][3] = 255; + } + stbi__skip(s, info.offset - info.extra_read - info.hsz - psize * (info.hsz == 12 ? 3 : 4)); + if (info.bpp == 1) width = (s->img_x + 7) >> 3; + else if (info.bpp == 4) width = (s->img_x + 1) >> 1; + else if (info.bpp == 8) width = s->img_x; + else { STBI_FREE(out); return stbi__errpuc("bad bpp", "Corrupt BMP"); } + pad = (-width)&3; + if (info.bpp == 1) { + for (j=0; j < (int) s->img_y; ++j) { + int bit_offset = 7, v = stbi__get8(s); + for (i=0; i < (int) s->img_x; ++i) { + int color = (v>>bit_offset)&0x1; + out[z++] = pal[color][0]; + out[z++] = pal[color][1]; + out[z++] = pal[color][2]; + if (target == 4) out[z++] = 255; + if (i+1 == (int) s->img_x) break; + if((--bit_offset) < 0) { + bit_offset = 7; + v = stbi__get8(s); + } + } + stbi__skip(s, pad); + } + } else { + for (j=0; j < (int) s->img_y; ++j) { + for (i=0; i < (int) s->img_x; i += 2) { + int v=stbi__get8(s),v2=0; + if (info.bpp == 4) { + v2 = v & 15; + v >>= 4; + } + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) out[z++] = 255; + if (i+1 == (int) s->img_x) break; + v = (info.bpp == 8) ? stbi__get8(s) : v2; + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) out[z++] = 255; + } + stbi__skip(s, pad); + } + } + } else { + int rshift=0,gshift=0,bshift=0,ashift=0,rcount=0,gcount=0,bcount=0,acount=0; + int z = 0; + int easy=0; + stbi__skip(s, info.offset - info.extra_read - info.hsz); + if (info.bpp == 24) width = 3 * s->img_x; + else if (info.bpp == 16) width = 2*s->img_x; + else /* bpp = 32 and pad = 0 */ width=0; + pad = (-width) & 3; + if (info.bpp == 24) { + easy = 1; + } else if (info.bpp == 32) { + if (mb == 0xff && mg == 0xff00 && mr == 0x00ff0000 && ma == 0xff000000) + easy = 2; + } + if (!easy) { + if (!mr || !mg || !mb) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } + // right shift amt to put high bit in position #7 + rshift = stbi__high_bit(mr)-7; rcount = stbi__bitcount(mr); + gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg); + bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb); + ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma); + if (rcount > 8 || gcount > 8 || bcount > 8 || acount > 8) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } + } + for (j=0; j < (int) s->img_y; ++j) { + if (easy) { + for (i=0; i < (int) s->img_x; ++i) { + unsigned char a; + out[z+2] = stbi__get8(s); + out[z+1] = stbi__get8(s); + out[z+0] = stbi__get8(s); + z += 3; + a = (easy == 2 ? stbi__get8(s) : 255); + all_a |= a; + if (target == 4) out[z++] = a; + } + } else { + int bpp = info.bpp; + for (i=0; i < (int) s->img_x; ++i) { + stbi__uint32 v = (bpp == 16 ? (stbi__uint32) stbi__get16le(s) : stbi__get32le(s)); + unsigned int a; + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mr, rshift, rcount)); + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mg, gshift, gcount)); + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mb, bshift, bcount)); + a = (ma ? stbi__shiftsigned(v & ma, ashift, acount) : 255); + all_a |= a; + if (target == 4) out[z++] = STBI__BYTECAST(a); + } + } + stbi__skip(s, pad); + } + } + + // if alpha channel is all 0s, replace with all 255s + if (target == 4 && all_a == 0) + for (i=4*s->img_x*s->img_y-1; i >= 0; i -= 4) + out[i] = 255; + + if (flip_vertically) { + stbi_uc t; + for (j=0; j < (int) s->img_y>>1; ++j) { + stbi_uc *p1 = out + j *s->img_x*target; + stbi_uc *p2 = out + (s->img_y-1-j)*s->img_x*target; + for (i=0; i < (int) s->img_x*target; ++i) { + t = p1[i]; p1[i] = p2[i]; p2[i] = t; + } + } + } + + if (req_comp && req_comp != target) { + out = stbi__convert_format(out, target, req_comp, s->img_x, s->img_y); + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + + *x = s->img_x; + *y = s->img_y; + if (comp) *comp = s->img_n; + return out; +} +#endif + +// Targa Truevision - TGA +// by Jonathan Dummer +#ifndef STBI_NO_TGA +// returns STBI_rgb or whatever, 0 on error +static int stbi__tga_get_comp(int bits_per_pixel, int is_grey, int* is_rgb16) +{ + // only RGB or RGBA (incl. 16bit) or grey allowed + if (is_rgb16) *is_rgb16 = 0; + switch(bits_per_pixel) { + case 8: return STBI_grey; + case 16: if(is_grey) return STBI_grey_alpha; + // fallthrough + case 15: if(is_rgb16) *is_rgb16 = 1; + return STBI_rgb; + case 24: // fallthrough + case 32: return bits_per_pixel/8; + default: return 0; + } +} + +static int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp) +{ + int tga_w, tga_h, tga_comp, tga_image_type, tga_bits_per_pixel, tga_colormap_bpp; + int sz, tga_colormap_type; + stbi__get8(s); // discard Offset + tga_colormap_type = stbi__get8(s); // colormap type + if( tga_colormap_type > 1 ) { + stbi__rewind(s); + return 0; // only RGB or indexed allowed + } + tga_image_type = stbi__get8(s); // image type + if ( tga_colormap_type == 1 ) { // colormapped (paletted) image + if (tga_image_type != 1 && tga_image_type != 9) { + stbi__rewind(s); + return 0; + } + stbi__skip(s,4); // skip index of first colormap entry and number of entries + sz = stbi__get8(s); // check bits per palette color entry + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) { + stbi__rewind(s); + return 0; + } + stbi__skip(s,4); // skip image x and y origin + tga_colormap_bpp = sz; + } else { // "normal" image w/o colormap - only RGB or grey allowed, +/- RLE + if ( (tga_image_type != 2) && (tga_image_type != 3) && (tga_image_type != 10) && (tga_image_type != 11) ) { + stbi__rewind(s); + return 0; // only RGB or grey allowed, +/- RLE + } + stbi__skip(s,9); // skip colormap specification and image x/y origin + tga_colormap_bpp = 0; + } + tga_w = stbi__get16le(s); + if( tga_w < 1 ) { + stbi__rewind(s); + return 0; // test width + } + tga_h = stbi__get16le(s); + if( tga_h < 1 ) { + stbi__rewind(s); + return 0; // test height + } + tga_bits_per_pixel = stbi__get8(s); // bits per pixel + stbi__get8(s); // ignore alpha bits + if (tga_colormap_bpp != 0) { + if((tga_bits_per_pixel != 8) && (tga_bits_per_pixel != 16)) { + // when using a colormap, tga_bits_per_pixel is the size of the indexes + // I don't think anything but 8 or 16bit indexes makes sense + stbi__rewind(s); + return 0; + } + tga_comp = stbi__tga_get_comp(tga_colormap_bpp, 0, NULL); + } else { + tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3) || (tga_image_type == 11), NULL); + } + if(!tga_comp) { + stbi__rewind(s); + return 0; + } + if (x) *x = tga_w; + if (y) *y = tga_h; + if (comp) *comp = tga_comp; + return 1; // seems to have passed everything +} + +static int stbi__tga_test(stbi__context *s) +{ + int res = 0; + int sz, tga_color_type; + stbi__get8(s); // discard Offset + tga_color_type = stbi__get8(s); // color type + if ( tga_color_type > 1 ) goto errorEnd; // only RGB or indexed allowed + sz = stbi__get8(s); // image type + if ( tga_color_type == 1 ) { // colormapped (paletted) image + if (sz != 1 && sz != 9) goto errorEnd; // colortype 1 demands image type 1 or 9 + stbi__skip(s,4); // skip index of first colormap entry and number of entries + sz = stbi__get8(s); // check bits per palette color entry + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd; + stbi__skip(s,4); // skip image x and y origin + } else { // "normal" image w/o colormap + if ( (sz != 2) && (sz != 3) && (sz != 10) && (sz != 11) ) goto errorEnd; // only RGB or grey allowed, +/- RLE + stbi__skip(s,9); // skip colormap specification and image x/y origin + } + if ( stbi__get16le(s) < 1 ) goto errorEnd; // test width + if ( stbi__get16le(s) < 1 ) goto errorEnd; // test height + sz = stbi__get8(s); // bits per pixel + if ( (tga_color_type == 1) && (sz != 8) && (sz != 16) ) goto errorEnd; // for colormapped images, bpp is size of an index + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd; + + res = 1; // if we got this far, everything's good and we can return 1 instead of 0 + +errorEnd: + stbi__rewind(s); + return res; +} + +// read 16bit value and convert to 24bit RGB +static void stbi__tga_read_rgb16(stbi__context *s, stbi_uc* out) +{ + stbi__uint16 px = (stbi__uint16)stbi__get16le(s); + stbi__uint16 fiveBitMask = 31; + // we have 3 channels with 5bits each + int r = (px >> 10) & fiveBitMask; + int g = (px >> 5) & fiveBitMask; + int b = px & fiveBitMask; + // Note that this saves the data in RGB(A) order, so it doesn't need to be swapped later + out[0] = (stbi_uc)((r * 255)/31); + out[1] = (stbi_uc)((g * 255)/31); + out[2] = (stbi_uc)((b * 255)/31); + + // some people claim that the most significant bit might be used for alpha + // (possibly if an alpha-bit is set in the "image descriptor byte") + // but that only made 16bit test images completely translucent.. + // so let's treat all 15 and 16bit TGAs as RGB with no alpha. +} + +static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + // read in the TGA header stuff + int tga_offset = stbi__get8(s); + int tga_indexed = stbi__get8(s); + int tga_image_type = stbi__get8(s); + int tga_is_RLE = 0; + int tga_palette_start = stbi__get16le(s); + int tga_palette_len = stbi__get16le(s); + int tga_palette_bits = stbi__get8(s); + int tga_x_origin = stbi__get16le(s); + int tga_y_origin = stbi__get16le(s); + int tga_width = stbi__get16le(s); + int tga_height = stbi__get16le(s); + int tga_bits_per_pixel = stbi__get8(s); + int tga_comp, tga_rgb16=0; + int tga_inverted = stbi__get8(s); + // int tga_alpha_bits = tga_inverted & 15; // the 4 lowest bits - unused (useless?) + // image data + unsigned char *tga_data; + unsigned char *tga_palette = NULL; + int i, j; + unsigned char raw_data[4] = {0}; + int RLE_count = 0; + int RLE_repeating = 0; + int read_next_pixel = 1; + STBI_NOTUSED(ri); + STBI_NOTUSED(tga_x_origin); // @TODO + STBI_NOTUSED(tga_y_origin); // @TODO + + if (tga_height > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (tga_width > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + // do a tiny bit of precessing + if ( tga_image_type >= 8 ) + { + tga_image_type -= 8; + tga_is_RLE = 1; + } + tga_inverted = 1 - ((tga_inverted >> 5) & 1); + + // If I'm paletted, then I'll use the number of bits from the palette + if ( tga_indexed ) tga_comp = stbi__tga_get_comp(tga_palette_bits, 0, &tga_rgb16); + else tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3), &tga_rgb16); + + if(!tga_comp) // shouldn't really happen, stbi__tga_test() should have ensured basic consistency + return stbi__errpuc("bad format", "Can't find out TGA pixelformat"); + + // tga info + *x = tga_width; + *y = tga_height; + if (comp) *comp = tga_comp; + + if (!stbi__mad3sizes_valid(tga_width, tga_height, tga_comp, 0)) + return stbi__errpuc("too large", "Corrupt TGA"); + + tga_data = (unsigned char*)stbi__malloc_mad3(tga_width, tga_height, tga_comp, 0); + if (!tga_data) return stbi__errpuc("outofmem", "Out of memory"); + + // skip to the data's starting position (offset usually = 0) + stbi__skip(s, tga_offset ); + + if ( !tga_indexed && !tga_is_RLE && !tga_rgb16 ) { + for (i=0; i < tga_height; ++i) { + int row = tga_inverted ? tga_height -i - 1 : i; + stbi_uc *tga_row = tga_data + row*tga_width*tga_comp; + stbi__getn(s, tga_row, tga_width * tga_comp); + } + } else { + // do I need to load a palette? + if ( tga_indexed) + { + if (tga_palette_len == 0) { /* you have to have at least one entry! */ + STBI_FREE(tga_data); + return stbi__errpuc("bad palette", "Corrupt TGA"); + } + + // any data to skip? (offset usually = 0) + stbi__skip(s, tga_palette_start ); + // load the palette + tga_palette = (unsigned char*)stbi__malloc_mad2(tga_palette_len, tga_comp, 0); + if (!tga_palette) { + STBI_FREE(tga_data); + return stbi__errpuc("outofmem", "Out of memory"); + } + if (tga_rgb16) { + stbi_uc *pal_entry = tga_palette; + STBI_ASSERT(tga_comp == STBI_rgb); + for (i=0; i < tga_palette_len; ++i) { + stbi__tga_read_rgb16(s, pal_entry); + pal_entry += tga_comp; + } + } else if (!stbi__getn(s, tga_palette, tga_palette_len * tga_comp)) { + STBI_FREE(tga_data); + STBI_FREE(tga_palette); + return stbi__errpuc("bad palette", "Corrupt TGA"); + } + } + // load the data + for (i=0; i < tga_width * tga_height; ++i) + { + // if I'm in RLE mode, do I need to get a RLE stbi__pngchunk? + if ( tga_is_RLE ) + { + if ( RLE_count == 0 ) + { + // yep, get the next byte as a RLE command + int RLE_cmd = stbi__get8(s); + RLE_count = 1 + (RLE_cmd & 127); + RLE_repeating = RLE_cmd >> 7; + read_next_pixel = 1; + } else if ( !RLE_repeating ) + { + read_next_pixel = 1; + } + } else + { + read_next_pixel = 1; + } + // OK, if I need to read a pixel, do it now + if ( read_next_pixel ) + { + // load however much data we did have + if ( tga_indexed ) + { + // read in index, then perform the lookup + int pal_idx = (tga_bits_per_pixel == 8) ? stbi__get8(s) : stbi__get16le(s); + if ( pal_idx >= tga_palette_len ) { + // invalid index + pal_idx = 0; + } + pal_idx *= tga_comp; + for (j = 0; j < tga_comp; ++j) { + raw_data[j] = tga_palette[pal_idx+j]; + } + } else if(tga_rgb16) { + STBI_ASSERT(tga_comp == STBI_rgb); + stbi__tga_read_rgb16(s, raw_data); + } else { + // read in the data raw + for (j = 0; j < tga_comp; ++j) { + raw_data[j] = stbi__get8(s); + } + } + // clear the reading flag for the next pixel + read_next_pixel = 0; + } // end of reading a pixel + + // copy data + for (j = 0; j < tga_comp; ++j) + tga_data[i*tga_comp+j] = raw_data[j]; + + // in case we're in RLE mode, keep counting down + --RLE_count; + } + // do I need to invert the image? + if ( tga_inverted ) + { + for (j = 0; j*2 < tga_height; ++j) + { + int index1 = j * tga_width * tga_comp; + int index2 = (tga_height - 1 - j) * tga_width * tga_comp; + for (i = tga_width * tga_comp; i > 0; --i) + { + unsigned char temp = tga_data[index1]; + tga_data[index1] = tga_data[index2]; + tga_data[index2] = temp; + ++index1; + ++index2; + } + } + } + // clear my palette, if I had one + if ( tga_palette != NULL ) + { + STBI_FREE( tga_palette ); + } + } + + // swap RGB - if the source data was RGB16, it already is in the right order + if (tga_comp >= 3 && !tga_rgb16) + { + unsigned char* tga_pixel = tga_data; + for (i=0; i < tga_width * tga_height; ++i) + { + unsigned char temp = tga_pixel[0]; + tga_pixel[0] = tga_pixel[2]; + tga_pixel[2] = temp; + tga_pixel += tga_comp; + } + } + + // convert to target component count + if (req_comp && req_comp != tga_comp) + tga_data = stbi__convert_format(tga_data, tga_comp, req_comp, tga_width, tga_height); + + // the things I do to get rid of an error message, and yet keep + // Microsoft's C compilers happy... [8^( + tga_palette_start = tga_palette_len = tga_palette_bits = + tga_x_origin = tga_y_origin = 0; + STBI_NOTUSED(tga_palette_start); + // OK, done + return tga_data; +} +#endif + +// ************************************************************************************************* +// Photoshop PSD loader -- PD by Thatcher Ulrich, integration by Nicolas Schulz, tweaked by STB + +#ifndef STBI_NO_PSD +static int stbi__psd_test(stbi__context *s) +{ + int r = (stbi__get32be(s) == 0x38425053); + stbi__rewind(s); + return r; +} + +static int stbi__psd_decode_rle(stbi__context *s, stbi_uc *p, int pixelCount) +{ + int count, nleft, len; + + count = 0; + while ((nleft = pixelCount - count) > 0) { + len = stbi__get8(s); + if (len == 128) { + // No-op. + } else if (len < 128) { + // Copy next len+1 bytes literally. + len++; + if (len > nleft) return 0; // corrupt data + count += len; + while (len) { + *p = stbi__get8(s); + p += 4; + len--; + } + } else if (len > 128) { + stbi_uc val; + // Next -len+1 bytes in the dest are replicated from next source byte. + // (Interpret len as a negative 8-bit int.) + len = 257 - len; + if (len > nleft) return 0; // corrupt data + val = stbi__get8(s); + count += len; + while (len) { + *p = val; + p += 4; + len--; + } + } + } + + return 1; +} + +static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) +{ + int pixelCount; + int channelCount, compression; + int channel, i; + int bitdepth; + int w,h; + stbi_uc *out; + STBI_NOTUSED(ri); + + // Check identifier + if (stbi__get32be(s) != 0x38425053) // "8BPS" + return stbi__errpuc("not PSD", "Corrupt PSD image"); + + // Check file type version. + if (stbi__get16be(s) != 1) + return stbi__errpuc("wrong version", "Unsupported version of PSD image"); + + // Skip 6 reserved bytes. + stbi__skip(s, 6 ); + + // Read the number of channels (R, G, B, A, etc). + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) + return stbi__errpuc("wrong channel count", "Unsupported number of channels in PSD image"); + + // Read the rows and columns of the image. + h = stbi__get32be(s); + w = stbi__get32be(s); + + if (h > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (w > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + // Make sure the depth is 8 bits. + bitdepth = stbi__get16be(s); + if (bitdepth != 8 && bitdepth != 16) + return stbi__errpuc("unsupported bit depth", "PSD bit depth is not 8 or 16 bit"); + + // Make sure the color mode is RGB. + // Valid options are: + // 0: Bitmap + // 1: Grayscale + // 2: Indexed color + // 3: RGB color + // 4: CMYK color + // 7: Multichannel + // 8: Duotone + // 9: Lab color + if (stbi__get16be(s) != 3) + return stbi__errpuc("wrong color format", "PSD is not in RGB color format"); + + // Skip the Mode Data. (It's the palette for indexed color; other info for other modes.) + stbi__skip(s,stbi__get32be(s) ); + + // Skip the image resources. (resolution, pen tool paths, etc) + stbi__skip(s, stbi__get32be(s) ); + + // Skip the reserved data. + stbi__skip(s, stbi__get32be(s) ); + + // Find out if the data is compressed. + // Known values: + // 0: no compression + // 1: RLE compressed + compression = stbi__get16be(s); + if (compression > 1) + return stbi__errpuc("bad compression", "PSD has an unknown compression format"); + + // Check size + if (!stbi__mad3sizes_valid(4, w, h, 0)) + return stbi__errpuc("too large", "Corrupt PSD"); + + // Create the destination image. + + if (!compression && bitdepth == 16 && bpc == 16) { + out = (stbi_uc *) stbi__malloc_mad3(8, w, h, 0); + ri->bits_per_channel = 16; + } else + out = (stbi_uc *) stbi__malloc(4 * w*h); + + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + pixelCount = w*h; + + // Initialize the data to zero. + //memset( out, 0, pixelCount * 4 ); + + // Finally, the image data. + if (compression) { + // RLE as used by .PSD and .TIFF + // Loop until you get the number of unpacked bytes you are expecting: + // Read the next source byte into n. + // If n is between 0 and 127 inclusive, copy the next n+1 bytes literally. + // Else if n is between -127 and -1 inclusive, copy the next byte -n+1 times. + // Else if n is 128, noop. + // Endloop + + // The RLE-compressed data is preceded by a 2-byte data count for each row in the data, + // which we're going to just skip. + stbi__skip(s, h * channelCount * 2 ); + + // Read the RLE data by channel. + for (channel = 0; channel < 4; channel++) { + stbi_uc *p; + + p = out+channel; + if (channel >= channelCount) { + // Fill this channel with default data. + for (i = 0; i < pixelCount; i++, p += 4) + *p = (channel == 3 ? 255 : 0); + } else { + // Read the RLE data. + if (!stbi__psd_decode_rle(s, p, pixelCount)) { + STBI_FREE(out); + return stbi__errpuc("corrupt", "bad RLE data"); + } + } + } + + } else { + // We're at the raw image data. It's each channel in order (Red, Green, Blue, Alpha, ...) + // where each channel consists of an 8-bit (or 16-bit) value for each pixel in the image. + + // Read the data by channel. + for (channel = 0; channel < 4; channel++) { + if (channel >= channelCount) { + // Fill this channel with default data. + if (bitdepth == 16 && bpc == 16) { + stbi__uint16 *q = ((stbi__uint16 *) out) + channel; + stbi__uint16 val = channel == 3 ? 65535 : 0; + for (i = 0; i < pixelCount; i++, q += 4) + *q = val; + } else { + stbi_uc *p = out+channel; + stbi_uc val = channel == 3 ? 255 : 0; + for (i = 0; i < pixelCount; i++, p += 4) + *p = val; + } + } else { + if (ri->bits_per_channel == 16) { // output bpc + stbi__uint16 *q = ((stbi__uint16 *) out) + channel; + for (i = 0; i < pixelCount; i++, q += 4) + *q = (stbi__uint16) stbi__get16be(s); + } else { + stbi_uc *p = out+channel; + if (bitdepth == 16) { // input bpc + for (i = 0; i < pixelCount; i++, p += 4) + *p = (stbi_uc) (stbi__get16be(s) >> 8); + } else { + for (i = 0; i < pixelCount; i++, p += 4) + *p = stbi__get8(s); + } + } + } + } + } + + // remove weird white matte from PSD + if (channelCount >= 4) { + if (ri->bits_per_channel == 16) { + for (i=0; i < w*h; ++i) { + stbi__uint16 *pixel = (stbi__uint16 *) out + 4*i; + if (pixel[3] != 0 && pixel[3] != 65535) { + float a = pixel[3] / 65535.0f; + float ra = 1.0f / a; + float inv_a = 65535.0f * (1 - ra); + pixel[0] = (stbi__uint16) (pixel[0]*ra + inv_a); + pixel[1] = (stbi__uint16) (pixel[1]*ra + inv_a); + pixel[2] = (stbi__uint16) (pixel[2]*ra + inv_a); + } + } + } else { + for (i=0; i < w*h; ++i) { + unsigned char *pixel = out + 4*i; + if (pixel[3] != 0 && pixel[3] != 255) { + float a = pixel[3] / 255.0f; + float ra = 1.0f / a; + float inv_a = 255.0f * (1 - ra); + pixel[0] = (unsigned char) (pixel[0]*ra + inv_a); + pixel[1] = (unsigned char) (pixel[1]*ra + inv_a); + pixel[2] = (unsigned char) (pixel[2]*ra + inv_a); + } + } + } + } + + // convert to desired output format + if (req_comp && req_comp != 4) { + if (ri->bits_per_channel == 16) + out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, 4, req_comp, w, h); + else + out = stbi__convert_format(out, 4, req_comp, w, h); + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + + if (comp) *comp = 4; + *y = h; + *x = w; + + return out; +} +#endif + +// ************************************************************************************************* +// Softimage PIC loader +// by Tom Seddon +// +// See http://softimage.wiki.softimage.com/index.php/INFO:_PIC_file_format +// See http://ozviz.wasp.uwa.edu.au/~pbourke/dataformats/softimagepic/ + +#ifndef STBI_NO_PIC +static int stbi__pic_is4(stbi__context *s,const char *str) +{ + int i; + for (i=0; i<4; ++i) + if (stbi__get8(s) != (stbi_uc)str[i]) + return 0; + + return 1; +} + +static int stbi__pic_test_core(stbi__context *s) +{ + int i; + + if (!stbi__pic_is4(s,"\x53\x80\xF6\x34")) + return 0; + + for(i=0;i<84;++i) + stbi__get8(s); + + if (!stbi__pic_is4(s,"PICT")) + return 0; + + return 1; +} + +typedef struct +{ + stbi_uc size,type,channel; +} stbi__pic_packet; + +static stbi_uc *stbi__readval(stbi__context *s, int channel, stbi_uc *dest) +{ + int mask=0x80, i; + + for (i=0; i<4; ++i, mask>>=1) { + if (channel & mask) { + if (stbi__at_eof(s)) return stbi__errpuc("bad file","PIC file too short"); + dest[i]=stbi__get8(s); + } + } + + return dest; +} + +static void stbi__copyval(int channel,stbi_uc *dest,const stbi_uc *src) +{ + int mask=0x80,i; + + for (i=0;i<4; ++i, mask>>=1) + if (channel&mask) + dest[i]=src[i]; +} + +static stbi_uc *stbi__pic_load_core(stbi__context *s,int width,int height,int *comp, stbi_uc *result) +{ + int act_comp=0,num_packets=0,y,chained; + stbi__pic_packet packets[10]; + + // this will (should...) cater for even some bizarre stuff like having data + // for the same channel in multiple packets. + do { + stbi__pic_packet *packet; + + if (num_packets==sizeof(packets)/sizeof(packets[0])) + return stbi__errpuc("bad format","too many packets"); + + packet = &packets[num_packets++]; + + chained = stbi__get8(s); + packet->size = stbi__get8(s); + packet->type = stbi__get8(s); + packet->channel = stbi__get8(s); + + act_comp |= packet->channel; + + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (reading packets)"); + if (packet->size != 8) return stbi__errpuc("bad format","packet isn't 8bpp"); + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); // has alpha channel? + + for(y=0; ytype) { + default: + return stbi__errpuc("bad format","packet has bad compression type"); + + case 0: {//uncompressed + int x; + + for(x=0;xchannel,dest)) + return 0; + break; + } + + case 1://Pure RLE + { + int left=width, i; + + while (left>0) { + stbi_uc count,value[4]; + + count=stbi__get8(s); + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pure read count)"); + + if (count > left) + count = (stbi_uc) left; + + if (!stbi__readval(s,packet->channel,value)) return 0; + + for(i=0; ichannel,dest,value); + left -= count; + } + } + break; + + case 2: {//Mixed RLE + int left=width; + while (left>0) { + int count = stbi__get8(s), i; + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (mixed read count)"); + + if (count >= 128) { // Repeated + stbi_uc value[4]; + + if (count==128) + count = stbi__get16be(s); + else + count -= 127; + if (count > left) + return stbi__errpuc("bad file","scanline overrun"); + + if (!stbi__readval(s,packet->channel,value)) + return 0; + + for(i=0;ichannel,dest,value); + } else { // Raw + ++count; + if (count>left) return stbi__errpuc("bad file","scanline overrun"); + + for(i=0;ichannel,dest)) + return 0; + } + left-=count; + } + break; + } + } + } + } + + return result; +} + +static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_comp, stbi__result_info *ri) +{ + stbi_uc *result; + int i, x,y, internal_comp; + STBI_NOTUSED(ri); + + if (!comp) comp = &internal_comp; + + for (i=0; i<92; ++i) + stbi__get8(s); + + x = stbi__get16be(s); + y = stbi__get16be(s); + + if (y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pic header)"); + if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode"); + + stbi__get32be(s); //skip `ratio' + stbi__get16be(s); //skip `fields' + stbi__get16be(s); //skip `pad' + + // intermediate buffer is RGBA + result = (stbi_uc *) stbi__malloc_mad3(x, y, 4, 0); + if (!result) return stbi__errpuc("outofmem", "Out of memory"); + memset(result, 0xff, x*y*4); + + if (!stbi__pic_load_core(s,x,y,comp, result)) { + STBI_FREE(result); + result=0; + } + *px = x; + *py = y; + if (req_comp == 0) req_comp = *comp; + result=stbi__convert_format(result,4,req_comp,x,y); + + return result; +} + +static int stbi__pic_test(stbi__context *s) +{ + int r = stbi__pic_test_core(s); + stbi__rewind(s); + return r; +} +#endif + +// ************************************************************************************************* +// GIF loader -- public domain by Jean-Marc Lienher -- simplified/shrunk by stb + +#ifndef STBI_NO_GIF +typedef struct +{ + stbi__int16 prefix; + stbi_uc first; + stbi_uc suffix; +} stbi__gif_lzw; + +typedef struct +{ + int w,h; + stbi_uc *out; // output buffer (always 4 components) + stbi_uc *background; // The current "background" as far as a gif is concerned + stbi_uc *history; + int flags, bgindex, ratio, transparent, eflags; + stbi_uc pal[256][4]; + stbi_uc lpal[256][4]; + stbi__gif_lzw codes[8192]; + stbi_uc *color_table; + int parse, step; + int lflags; + int start_x, start_y; + int max_x, max_y; + int cur_x, cur_y; + int line_size; + int delay; +} stbi__gif; + +static int stbi__gif_test_raw(stbi__context *s) +{ + int sz; + if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') return 0; + sz = stbi__get8(s); + if (sz != '9' && sz != '7') return 0; + if (stbi__get8(s) != 'a') return 0; + return 1; +} + +static int stbi__gif_test(stbi__context *s) +{ + int r = stbi__gif_test_raw(s); + stbi__rewind(s); + return r; +} + +static void stbi__gif_parse_colortable(stbi__context *s, stbi_uc pal[256][4], int num_entries, int transp) +{ + int i; + for (i=0; i < num_entries; ++i) { + pal[i][2] = stbi__get8(s); + pal[i][1] = stbi__get8(s); + pal[i][0] = stbi__get8(s); + pal[i][3] = transp == i ? 0 : 255; + } +} + +static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_info) +{ + stbi_uc version; + if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') + return stbi__err("not GIF", "Corrupt GIF"); + + version = stbi__get8(s); + if (version != '7' && version != '9') return stbi__err("not GIF", "Corrupt GIF"); + if (stbi__get8(s) != 'a') return stbi__err("not GIF", "Corrupt GIF"); + + stbi__g_failure_reason = ""; + g->w = stbi__get16le(s); + g->h = stbi__get16le(s); + g->flags = stbi__get8(s); + g->bgindex = stbi__get8(s); + g->ratio = stbi__get8(s); + g->transparent = -1; + + if (g->w > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (g->h > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + + if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments + + if (is_info) return 1; + + if (g->flags & 0x80) + stbi__gif_parse_colortable(s,g->pal, 2 << (g->flags & 7), -1); + + return 1; +} + +static int stbi__gif_info_raw(stbi__context *s, int *x, int *y, int *comp) +{ + stbi__gif* g = (stbi__gif*) stbi__malloc(sizeof(stbi__gif)); + if (!g) return stbi__err("outofmem", "Out of memory"); + if (!stbi__gif_header(s, g, comp, 1)) { + STBI_FREE(g); + stbi__rewind( s ); + return 0; + } + if (x) *x = g->w; + if (y) *y = g->h; + STBI_FREE(g); + return 1; +} + +static void stbi__out_gif_code(stbi__gif *g, stbi__uint16 code) +{ + stbi_uc *p, *c; + int idx; + + // recurse to decode the prefixes, since the linked-list is backwards, + // and working backwards through an interleaved image would be nasty + if (g->codes[code].prefix >= 0) + stbi__out_gif_code(g, g->codes[code].prefix); + + if (g->cur_y >= g->max_y) return; + + idx = g->cur_x + g->cur_y; + p = &g->out[idx]; + g->history[idx / 4] = 1; + + c = &g->color_table[g->codes[code].suffix * 4]; + if (c[3] > 128) { // don't render transparent pixels; + p[0] = c[2]; + p[1] = c[1]; + p[2] = c[0]; + p[3] = c[3]; + } + g->cur_x += 4; + + if (g->cur_x >= g->max_x) { + g->cur_x = g->start_x; + g->cur_y += g->step; + + while (g->cur_y >= g->max_y && g->parse > 0) { + g->step = (1 << g->parse) * g->line_size; + g->cur_y = g->start_y + (g->step >> 1); + --g->parse; + } + } +} + +static stbi_uc *stbi__process_gif_raster(stbi__context *s, stbi__gif *g) +{ + stbi_uc lzw_cs; + stbi__int32 len, init_code; + stbi__uint32 first; + stbi__int32 codesize, codemask, avail, oldcode, bits, valid_bits, clear; + stbi__gif_lzw *p; + + lzw_cs = stbi__get8(s); + if (lzw_cs > 12) return NULL; + clear = 1 << lzw_cs; + first = 1; + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + bits = 0; + valid_bits = 0; + for (init_code = 0; init_code < clear; init_code++) { + g->codes[init_code].prefix = -1; + g->codes[init_code].first = (stbi_uc) init_code; + g->codes[init_code].suffix = (stbi_uc) init_code; + } + + // support no starting clear code + avail = clear+2; + oldcode = -1; + + len = 0; + for(;;) { + if (valid_bits < codesize) { + if (len == 0) { + len = stbi__get8(s); // start new block + if (len == 0) + return g->out; + } + --len; + bits |= (stbi__int32) stbi__get8(s) << valid_bits; + valid_bits += 8; + } else { + stbi__int32 code = bits & codemask; + bits >>= codesize; + valid_bits -= codesize; + // @OPTIMIZE: is there some way we can accelerate the non-clear path? + if (code == clear) { // clear code + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + avail = clear + 2; + oldcode = -1; + first = 0; + } else if (code == clear + 1) { // end of stream code + stbi__skip(s, len); + while ((len = stbi__get8(s)) > 0) + stbi__skip(s,len); + return g->out; + } else if (code <= avail) { + if (first) { + return stbi__errpuc("no clear code", "Corrupt GIF"); + } + + if (oldcode >= 0) { + p = &g->codes[avail++]; + if (avail > 8192) { + return stbi__errpuc("too many codes", "Corrupt GIF"); + } + + p->prefix = (stbi__int16) oldcode; + p->first = g->codes[oldcode].first; + p->suffix = (code == avail) ? p->first : g->codes[code].first; + } else if (code == avail) + return stbi__errpuc("illegal code in raster", "Corrupt GIF"); + + stbi__out_gif_code(g, (stbi__uint16) code); + + if ((avail & codemask) == 0 && avail <= 0x0FFF) { + codesize++; + codemask = (1 << codesize) - 1; + } + + oldcode = code; + } else { + return stbi__errpuc("illegal code in raster", "Corrupt GIF"); + } + } + } +} + +// this function is designed to support animated gifs, although stb_image doesn't support it +// two back is the image from two frames ago, used for a very specific disposal format +static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, int req_comp, stbi_uc *two_back) +{ + int dispose; + int first_frame; + int pi; + int pcount; + STBI_NOTUSED(req_comp); + + // on first frame, any non-written pixels get the background colour (non-transparent) + first_frame = 0; + if (g->out == 0) { + if (!stbi__gif_header(s, g, comp,0)) return 0; // stbi__g_failure_reason set by stbi__gif_header + if (!stbi__mad3sizes_valid(4, g->w, g->h, 0)) + return stbi__errpuc("too large", "GIF image is too large"); + pcount = g->w * g->h; + g->out = (stbi_uc *) stbi__malloc(4 * pcount); + g->background = (stbi_uc *) stbi__malloc(4 * pcount); + g->history = (stbi_uc *) stbi__malloc(pcount); + if (!g->out || !g->background || !g->history) + return stbi__errpuc("outofmem", "Out of memory"); + + // image is treated as "transparent" at the start - ie, nothing overwrites the current background; + // background colour is only used for pixels that are not rendered first frame, after that "background" + // color refers to the color that was there the previous frame. + memset(g->out, 0x00, 4 * pcount); + memset(g->background, 0x00, 4 * pcount); // state of the background (starts transparent) + memset(g->history, 0x00, pcount); // pixels that were affected previous frame + first_frame = 1; + } else { + // second frame - how do we dispose of the previous one? + dispose = (g->eflags & 0x1C) >> 2; + pcount = g->w * g->h; + + if ((dispose == 3) && (two_back == 0)) { + dispose = 2; // if I don't have an image to revert back to, default to the old background + } + + if (dispose == 3) { // use previous graphic + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi]) { + memcpy( &g->out[pi * 4], &two_back[pi * 4], 4 ); + } + } + } else if (dispose == 2) { + // restore what was changed last frame to background before that frame; + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi]) { + memcpy( &g->out[pi * 4], &g->background[pi * 4], 4 ); + } + } + } else { + // This is a non-disposal case eithe way, so just + // leave the pixels as is, and they will become the new background + // 1: do not dispose + // 0: not specified. + } + + // background is what out is after the undoing of the previou frame; + memcpy( g->background, g->out, 4 * g->w * g->h ); + } + + // clear my history; + memset( g->history, 0x00, g->w * g->h ); // pixels that were affected previous frame + + for (;;) { + int tag = stbi__get8(s); + switch (tag) { + case 0x2C: /* Image Descriptor */ + { + stbi__int32 x, y, w, h; + stbi_uc *o; + + x = stbi__get16le(s); + y = stbi__get16le(s); + w = stbi__get16le(s); + h = stbi__get16le(s); + if (((x + w) > (g->w)) || ((y + h) > (g->h))) + return stbi__errpuc("bad Image Descriptor", "Corrupt GIF"); + + g->line_size = g->w * 4; + g->start_x = x * 4; + g->start_y = y * g->line_size; + g->max_x = g->start_x + w * 4; + g->max_y = g->start_y + h * g->line_size; + g->cur_x = g->start_x; + g->cur_y = g->start_y; + + // if the width of the specified rectangle is 0, that means + // we may not see *any* pixels or the image is malformed; + // to make sure this is caught, move the current y down to + // max_y (which is what out_gif_code checks). + if (w == 0) + g->cur_y = g->max_y; + + g->lflags = stbi__get8(s); + + if (g->lflags & 0x40) { + g->step = 8 * g->line_size; // first interlaced spacing + g->parse = 3; + } else { + g->step = g->line_size; + g->parse = 0; + } + + if (g->lflags & 0x80) { + stbi__gif_parse_colortable(s,g->lpal, 2 << (g->lflags & 7), g->eflags & 0x01 ? g->transparent : -1); + g->color_table = (stbi_uc *) g->lpal; + } else if (g->flags & 0x80) { + g->color_table = (stbi_uc *) g->pal; + } else + return stbi__errpuc("missing color table", "Corrupt GIF"); + + o = stbi__process_gif_raster(s, g); + if (!o) return NULL; + + // if this was the first frame, + pcount = g->w * g->h; + if (first_frame && (g->bgindex > 0)) { + // if first frame, any pixel not drawn to gets the background color + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi] == 0) { + g->pal[g->bgindex][3] = 255; // just in case it was made transparent, undo that; It will be reset next frame if need be; + memcpy( &g->out[pi * 4], &g->pal[g->bgindex], 4 ); + } + } + } + + return o; + } + + case 0x21: // Comment Extension. + { + int len; + int ext = stbi__get8(s); + if (ext == 0xF9) { // Graphic Control Extension. + len = stbi__get8(s); + if (len == 4) { + g->eflags = stbi__get8(s); + g->delay = 10 * stbi__get16le(s); // delay - 1/100th of a second, saving as 1/1000ths. + + // unset old transparent + if (g->transparent >= 0) { + g->pal[g->transparent][3] = 255; + } + if (g->eflags & 0x01) { + g->transparent = stbi__get8(s); + if (g->transparent >= 0) { + g->pal[g->transparent][3] = 0; + } + } else { + // don't need transparent + stbi__skip(s, 1); + g->transparent = -1; + } + } else { + stbi__skip(s, len); + break; + } + } + while ((len = stbi__get8(s)) != 0) { + stbi__skip(s, len); + } + break; + } + + case 0x3B: // gif stream termination code + return (stbi_uc *) s; // using '1' causes warning on some compilers + + default: + return stbi__errpuc("unknown code", "Corrupt GIF"); + } + } +} + +static void *stbi__load_gif_main_outofmem(stbi__gif *g, stbi_uc *out, int **delays) +{ + STBI_FREE(g->out); + STBI_FREE(g->history); + STBI_FREE(g->background); + + if (out) STBI_FREE(out); + if (delays && *delays) STBI_FREE(*delays); + return stbi__errpuc("outofmem", "Out of memory"); +} + +static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp) +{ + if (stbi__gif_test(s)) { + int layers = 0; + stbi_uc *u = 0; + stbi_uc *out = 0; + stbi_uc *two_back = 0; + stbi__gif g; + int stride; + int out_size = 0; + int delays_size = 0; + + STBI_NOTUSED(out_size); + STBI_NOTUSED(delays_size); + + memset(&g, 0, sizeof(g)); + if (delays) { + *delays = 0; + } + + do { + u = stbi__gif_load_next(s, &g, comp, req_comp, two_back); + if (u == (stbi_uc *) s) u = 0; // end of animated gif marker + + if (u) { + *x = g.w; + *y = g.h; + ++layers; + stride = g.w * g.h * 4; + + if (out) { + void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride ); + if (!tmp) + return stbi__load_gif_main_outofmem(&g, out, delays); + else { + out = (stbi_uc*) tmp; + out_size = layers * stride; + } + + if (delays) { + int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers ); + if (!new_delays) + return stbi__load_gif_main_outofmem(&g, out, delays); + *delays = new_delays; + delays_size = layers * sizeof(int); + } + } else { + out = (stbi_uc*)stbi__malloc( layers * stride ); + if (!out) + return stbi__load_gif_main_outofmem(&g, out, delays); + out_size = layers * stride; + if (delays) { + *delays = (int*) stbi__malloc( layers * sizeof(int) ); + if (!*delays) + return stbi__load_gif_main_outofmem(&g, out, delays); + delays_size = layers * sizeof(int); + } + } + memcpy( out + ((layers - 1) * stride), u, stride ); + if (layers >= 2) { + two_back = out - 2 * stride; + } + + if (delays) { + (*delays)[layers - 1U] = g.delay; + } + } + } while (u != 0); + + // free temp buffer; + STBI_FREE(g.out); + STBI_FREE(g.history); + STBI_FREE(g.background); + + // do the final conversion after loading everything; + if (req_comp && req_comp != 4) + out = stbi__convert_format(out, 4, req_comp, layers * g.w, g.h); + + *z = layers; + return out; + } else { + return stbi__errpuc("not GIF", "Image was not as a gif type."); + } +} + +static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *u = 0; + stbi__gif g; + memset(&g, 0, sizeof(g)); + STBI_NOTUSED(ri); + + u = stbi__gif_load_next(s, &g, comp, req_comp, 0); + if (u == (stbi_uc *) s) u = 0; // end of animated gif marker + if (u) { + *x = g.w; + *y = g.h; + + // moved conversion to after successful load so that the same + // can be done for multiple frames. + if (req_comp && req_comp != 4) + u = stbi__convert_format(u, 4, req_comp, g.w, g.h); + } else if (g.out) { + // if there was an error and we allocated an image buffer, free it! + STBI_FREE(g.out); + } + + // free buffers needed for multiple frame loading; + STBI_FREE(g.history); + STBI_FREE(g.background); + + return u; +} + +static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp) +{ + return stbi__gif_info_raw(s,x,y,comp); +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR loader +// originally by Nicolas Schulz +#ifndef STBI_NO_HDR +static int stbi__hdr_test_core(stbi__context *s, const char *signature) +{ + int i; + for (i=0; signature[i]; ++i) + if (stbi__get8(s) != signature[i]) + return 0; + stbi__rewind(s); + return 1; +} + +static int stbi__hdr_test(stbi__context* s) +{ + int r = stbi__hdr_test_core(s, "#?RADIANCE\n"); + stbi__rewind(s); + if(!r) { + r = stbi__hdr_test_core(s, "#?RGBE\n"); + stbi__rewind(s); + } + return r; +} + +#define STBI__HDR_BUFLEN 1024 +static char *stbi__hdr_gettoken(stbi__context *z, char *buffer) +{ + int len=0; + char c = '\0'; + + c = (char) stbi__get8(z); + + while (!stbi__at_eof(z) && c != '\n') { + buffer[len++] = c; + if (len == STBI__HDR_BUFLEN-1) { + // flush to end of line + while (!stbi__at_eof(z) && stbi__get8(z) != '\n') + ; + break; + } + c = (char) stbi__get8(z); + } + + buffer[len] = 0; + return buffer; +} + +static void stbi__hdr_convert(float *output, stbi_uc *input, int req_comp) +{ + if ( input[3] != 0 ) { + float f1; + // Exponent + f1 = (float) ldexp(1.0f, input[3] - (int)(128 + 8)); + if (req_comp <= 2) + output[0] = (input[0] + input[1] + input[2]) * f1 / 3; + else { + output[0] = input[0] * f1; + output[1] = input[1] * f1; + output[2] = input[2] * f1; + } + if (req_comp == 2) output[1] = 1; + if (req_comp == 4) output[3] = 1; + } else { + switch (req_comp) { + case 4: output[3] = 1; /* fallthrough */ + case 3: output[0] = output[1] = output[2] = 0; + break; + case 2: output[1] = 1; /* fallthrough */ + case 1: output[0] = 0; + break; + } + } +} + +static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + char buffer[STBI__HDR_BUFLEN]; + char *token; + int valid = 0; + int width, height; + stbi_uc *scanline; + float *hdr_data; + int len; + unsigned char count, value; + int i, j, k, c1,c2, z; + const char *headerToken; + STBI_NOTUSED(ri); + + // Check identifier + headerToken = stbi__hdr_gettoken(s,buffer); + if (strcmp(headerToken, "#?RADIANCE") != 0 && strcmp(headerToken, "#?RGBE") != 0) + return stbi__errpf("not HDR", "Corrupt HDR image"); + + // Parse header + for(;;) { + token = stbi__hdr_gettoken(s,buffer); + if (token[0] == 0) break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; + } + + if (!valid) return stbi__errpf("unsupported format", "Unsupported HDR format"); + + // Parse width and height + // can't use sscanf() if we're not using stdio! + token = stbi__hdr_gettoken(s,buffer); + if (strncmp(token, "-Y ", 3)) return stbi__errpf("unsupported data layout", "Unsupported HDR format"); + token += 3; + height = (int) strtol(token, &token, 10); + while (*token == ' ') ++token; + if (strncmp(token, "+X ", 3)) return stbi__errpf("unsupported data layout", "Unsupported HDR format"); + token += 3; + width = (int) strtol(token, NULL, 10); + + if (height > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); + if (width > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); + + *x = width; + *y = height; + + if (comp) *comp = 3; + if (req_comp == 0) req_comp = 3; + + if (!stbi__mad4sizes_valid(width, height, req_comp, sizeof(float), 0)) + return stbi__errpf("too large", "HDR image is too large"); + + // Read data + hdr_data = (float *) stbi__malloc_mad4(width, height, req_comp, sizeof(float), 0); + if (!hdr_data) + return stbi__errpf("outofmem", "Out of memory"); + + // Load image data + // image data is stored as some number of sca + if ( width < 8 || width >= 32768) { + // Read flat data + for (j=0; j < height; ++j) { + for (i=0; i < width; ++i) { + stbi_uc rgbe[4]; + main_decode_loop: + stbi__getn(s, rgbe, 4); + stbi__hdr_convert(hdr_data + j * width * req_comp + i * req_comp, rgbe, req_comp); + } + } + } else { + // Read RLE-encoded data + scanline = NULL; + + for (j = 0; j < height; ++j) { + c1 = stbi__get8(s); + c2 = stbi__get8(s); + len = stbi__get8(s); + if (c1 != 2 || c2 != 2 || (len & 0x80)) { + // not run-length encoded, so we have to actually use THIS data as a decoded + // pixel (note this can't be a valid pixel--one of RGB must be >= 128) + stbi_uc rgbe[4]; + rgbe[0] = (stbi_uc) c1; + rgbe[1] = (stbi_uc) c2; + rgbe[2] = (stbi_uc) len; + rgbe[3] = (stbi_uc) stbi__get8(s); + stbi__hdr_convert(hdr_data, rgbe, req_comp); + i = 1; + j = 0; + STBI_FREE(scanline); + goto main_decode_loop; // yes, this makes no sense + } + len <<= 8; + len |= stbi__get8(s); + if (len != width) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("invalid decoded scanline length", "corrupt HDR"); } + if (scanline == NULL) { + scanline = (stbi_uc *) stbi__malloc_mad2(width, 4, 0); + if (!scanline) { + STBI_FREE(hdr_data); + return stbi__errpf("outofmem", "Out of memory"); + } + } + + for (k = 0; k < 4; ++k) { + int nleft; + i = 0; + while ((nleft = width - i) > 0) { + count = stbi__get8(s); + if (count > 128) { + // Run + value = stbi__get8(s); + count -= 128; + if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = value; + } else { + // Dump + if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = stbi__get8(s); + } + } + } + for (i=0; i < width; ++i) + stbi__hdr_convert(hdr_data+(j*width + i)*req_comp, scanline + i*4, req_comp); + } + if (scanline) + STBI_FREE(scanline); + } + + return hdr_data; +} + +static int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp) +{ + char buffer[STBI__HDR_BUFLEN]; + char *token; + int valid = 0; + int dummy; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + if (stbi__hdr_test(s) == 0) { + stbi__rewind( s ); + return 0; + } + + for(;;) { + token = stbi__hdr_gettoken(s,buffer); + if (token[0] == 0) break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; + } + + if (!valid) { + stbi__rewind( s ); + return 0; + } + token = stbi__hdr_gettoken(s,buffer); + if (strncmp(token, "-Y ", 3)) { + stbi__rewind( s ); + return 0; + } + token += 3; + *y = (int) strtol(token, &token, 10); + while (*token == ' ') ++token; + if (strncmp(token, "+X ", 3)) { + stbi__rewind( s ); + return 0; + } + token += 3; + *x = (int) strtol(token, NULL, 10); + *comp = 3; + return 1; +} +#endif // STBI_NO_HDR + +#ifndef STBI_NO_BMP +static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp) +{ + void *p; + stbi__bmp_data info; + + info.all_a = 255; + p = stbi__bmp_parse_header(s, &info); + if (p == NULL) { + stbi__rewind( s ); + return 0; + } + if (x) *x = s->img_x; + if (y) *y = s->img_y; + if (comp) { + if (info.bpp == 24 && info.ma == 0xff000000) + *comp = 3; + else + *comp = info.ma ? 4 : 3; + } + return 1; +} +#endif + +#ifndef STBI_NO_PSD +static int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp) +{ + int channelCount, dummy, depth; + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + if (stbi__get32be(s) != 0x38425053) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 1) { + stbi__rewind( s ); + return 0; + } + stbi__skip(s, 6); + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) { + stbi__rewind( s ); + return 0; + } + *y = stbi__get32be(s); + *x = stbi__get32be(s); + depth = stbi__get16be(s); + if (depth != 8 && depth != 16) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 3) { + stbi__rewind( s ); + return 0; + } + *comp = 4; + return 1; +} + +static int stbi__psd_is16(stbi__context *s) +{ + int channelCount, depth; + if (stbi__get32be(s) != 0x38425053) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 1) { + stbi__rewind( s ); + return 0; + } + stbi__skip(s, 6); + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) { + stbi__rewind( s ); + return 0; + } + STBI_NOTUSED(stbi__get32be(s)); + STBI_NOTUSED(stbi__get32be(s)); + depth = stbi__get16be(s); + if (depth != 16) { + stbi__rewind( s ); + return 0; + } + return 1; +} +#endif + +#ifndef STBI_NO_PIC +static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp) +{ + int act_comp=0,num_packets=0,chained,dummy; + stbi__pic_packet packets[10]; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + if (!stbi__pic_is4(s,"\x53\x80\xF6\x34")) { + stbi__rewind(s); + return 0; + } + + stbi__skip(s, 88); + + *x = stbi__get16be(s); + *y = stbi__get16be(s); + if (stbi__at_eof(s)) { + stbi__rewind( s); + return 0; + } + if ( (*x) != 0 && (1 << 28) / (*x) < (*y)) { + stbi__rewind( s ); + return 0; + } + + stbi__skip(s, 8); + + do { + stbi__pic_packet *packet; + + if (num_packets==sizeof(packets)/sizeof(packets[0])) + return 0; + + packet = &packets[num_packets++]; + chained = stbi__get8(s); + packet->size = stbi__get8(s); + packet->type = stbi__get8(s); + packet->channel = stbi__get8(s); + act_comp |= packet->channel; + + if (stbi__at_eof(s)) { + stbi__rewind( s ); + return 0; + } + if (packet->size != 8) { + stbi__rewind( s ); + return 0; + } + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); + + return 1; +} +#endif + +// ************************************************************************************************* +// Portable Gray Map and Portable Pixel Map loader +// by Ken Miller +// +// PGM: http://netpbm.sourceforge.net/doc/pgm.html +// PPM: http://netpbm.sourceforge.net/doc/ppm.html +// +// Known limitations: +// Does not support comments in the header section +// Does not support ASCII image data (formats P2 and P3) + +#ifndef STBI_NO_PNM + +static int stbi__pnm_test(stbi__context *s) +{ + char p, t; + p = (char) stbi__get8(s); + t = (char) stbi__get8(s); + if (p != 'P' || (t != '5' && t != '6')) { + stbi__rewind( s ); + return 0; + } + return 1; +} + +static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *out; + STBI_NOTUSED(ri); + + ri->bits_per_channel = stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n); + if (ri->bits_per_channel == 0) + return 0; + + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + *x = s->img_x; + *y = s->img_y; + if (comp) *comp = s->img_n; + + if (!stbi__mad4sizes_valid(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0)) + return stbi__errpuc("too large", "PNM too large"); + + out = (stbi_uc *) stbi__malloc_mad4(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0); + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + if (!stbi__getn(s, out, s->img_n * s->img_x * s->img_y * (ri->bits_per_channel / 8))) { + STBI_FREE(out); + return stbi__errpuc("bad PNM", "PNM file truncated"); + } + + if (req_comp && req_comp != s->img_n) { + if (ri->bits_per_channel == 16) { + out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, s->img_n, req_comp, s->img_x, s->img_y); + } else { + out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y); + } + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + return out; +} + +static int stbi__pnm_isspace(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +static void stbi__pnm_skip_whitespace(stbi__context *s, char *c) +{ + for (;;) { + while (!stbi__at_eof(s) && stbi__pnm_isspace(*c)) + *c = (char) stbi__get8(s); + + if (stbi__at_eof(s) || *c != '#') + break; + + while (!stbi__at_eof(s) && *c != '\n' && *c != '\r' ) + *c = (char) stbi__get8(s); + } +} + +static int stbi__pnm_isdigit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int stbi__pnm_getinteger(stbi__context *s, char *c) +{ + int value = 0; + + while (!stbi__at_eof(s) && stbi__pnm_isdigit(*c)) { + value = value*10 + (*c - '0'); + *c = (char) stbi__get8(s); + if((value > 214748364) || (value == 214748364 && *c > '7')) + return stbi__err("integer parse overflow", "Parsing an integer in the PPM header overflowed a 32-bit int"); + } + + return value; +} + +static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp) +{ + int maxv, dummy; + char c, p, t; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + stbi__rewind(s); + + // Get identifier + p = (char) stbi__get8(s); + t = (char) stbi__get8(s); + if (p != 'P' || (t != '5' && t != '6')) { + stbi__rewind(s); + return 0; + } + + *comp = (t == '6') ? 3 : 1; // '5' is 1-component .pgm; '6' is 3-component .ppm + + c = (char) stbi__get8(s); + stbi__pnm_skip_whitespace(s, &c); + + *x = stbi__pnm_getinteger(s, &c); // read width + if(*x == 0) + return stbi__err("invalid width", "PPM image header had zero or overflowing width"); + stbi__pnm_skip_whitespace(s, &c); + + *y = stbi__pnm_getinteger(s, &c); // read height + if (*y == 0) + return stbi__err("invalid width", "PPM image header had zero or overflowing width"); + stbi__pnm_skip_whitespace(s, &c); + + maxv = stbi__pnm_getinteger(s, &c); // read max value + if (maxv > 65535) + return stbi__err("max value > 65535", "PPM image supports only 8-bit and 16-bit images"); + else if (maxv > 255) + return 16; + else + return 8; +} + +static int stbi__pnm_is16(stbi__context *s) +{ + if (stbi__pnm_info(s, NULL, NULL, NULL) == 16) + return 1; + return 0; +} +#endif + +static int stbi__info_main(stbi__context *s, int *x, int *y, int *comp) +{ + #ifndef STBI_NO_JPEG + if (stbi__jpeg_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PNG + if (stbi__png_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_GIF + if (stbi__gif_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_BMP + if (stbi__bmp_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PSD + if (stbi__psd_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PIC + if (stbi__pic_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PNM + if (stbi__pnm_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_HDR + if (stbi__hdr_info(s, x, y, comp)) return 1; + #endif + + // test tga last because it's a crappy test! + #ifndef STBI_NO_TGA + if (stbi__tga_info(s, x, y, comp)) + return 1; + #endif + return stbi__err("unknown image type", "Image not of any known type, or corrupt"); +} + +static int stbi__is_16_main(stbi__context *s) +{ + #ifndef STBI_NO_PNG + if (stbi__png_is16(s)) return 1; + #endif + + #ifndef STBI_NO_PSD + if (stbi__psd_is16(s)) return 1; + #endif + + #ifndef STBI_NO_PNM + if (stbi__pnm_is16(s)) return 1; + #endif + return 0; +} + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_info(char const *filename, int *x, int *y, int *comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result; + if (!f) return stbi__err("can't fopen", "Unable to open file"); + result = stbi_info_from_file(f, x, y, comp); + fclose(f); + return result; +} + +STBIDEF int stbi_info_from_file(FILE *f, int *x, int *y, int *comp) +{ + int r; + stbi__context s; + long pos = ftell(f); + stbi__start_file(&s, f); + r = stbi__info_main(&s,x,y,comp); + fseek(f,pos,SEEK_SET); + return r; +} + +STBIDEF int stbi_is_16_bit(char const *filename) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result; + if (!f) return stbi__err("can't fopen", "Unable to open file"); + result = stbi_is_16_bit_from_file(f); + fclose(f); + return result; +} + +STBIDEF int stbi_is_16_bit_from_file(FILE *f) +{ + int r; + stbi__context s; + long pos = ftell(f); + stbi__start_file(&s, f); + r = stbi__is_16_main(&s); + fseek(f,pos,SEEK_SET); + return r; +} +#endif // !STBI_NO_STDIO + +STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__info_main(&s,x,y,comp); +} + +STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *c, void *user, int *x, int *y, int *comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); + return stbi__info_main(&s,x,y,comp); +} + +STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__is_16_main(&s); +} + +STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *c, void *user) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); + return stbi__is_16_main(&s); +} + +#endif // STB_IMAGE_IMPLEMENTATION + +/* + revision history: + 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs + 2.19 (2018-02-11) fix warning + 2.18 (2018-01-30) fix warnings + 2.17 (2018-01-29) change sbti__shiftsigned to avoid clang -O2 bug + 1-bit BMP + *_is_16_bit api + avoid warnings + 2.16 (2017-07-23) all functions have 16-bit variants; + STBI_NO_STDIO works again; + compilation fixes; + fix rounding in unpremultiply; + optimize vertical flip; + disable raw_len validation; + documentation fixes + 2.15 (2017-03-18) fix png-1,2,4 bug; now all Imagenet JPGs decode; + warning fixes; disable run-time SSE detection on gcc; + uniform handling of optional "return" values; + thread-safe initialization of zlib tables + 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs + 2.13 (2016-11-29) add 16-bit API, only supported for PNG right now + 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes + 2.11 (2016-04-02) allocate large structures on the stack + remove white matting for transparent PSD + fix reported channel count for PNG & BMP + re-enable SSE2 in non-gcc 64-bit + support RGB-formatted JPEG + read 16-bit PNGs (only as 8-bit) + 2.10 (2016-01-22) avoid warning introduced in 2.09 by STBI_REALLOC_SIZED + 2.09 (2016-01-16) allow comments in PNM files + 16-bit-per-pixel TGA (not bit-per-component) + info() for TGA could break due to .hdr handling + info() for BMP to shares code instead of sloppy parse + can use STBI_REALLOC_SIZED if allocator doesn't support realloc + code cleanup + 2.08 (2015-09-13) fix to 2.07 cleanup, reading RGB PSD as RGBA + 2.07 (2015-09-13) fix compiler warnings + partial animated GIF support + limited 16-bpc PSD support + #ifdef unused functions + bug with < 92 byte PIC,PNM,HDR,TGA + 2.06 (2015-04-19) fix bug where PSD returns wrong '*comp' value + 2.05 (2015-04-19) fix bug in progressive JPEG handling, fix warning + 2.04 (2015-04-15) try to re-enable SIMD on MinGW 64-bit + 2.03 (2015-04-12) extra corruption checking (mmozeiko) + stbi_set_flip_vertically_on_load (nguillemot) + fix NEON support; fix mingw support + 2.02 (2015-01-19) fix incorrect assert, fix warning + 2.01 (2015-01-17) fix various warnings; suppress SIMD on gcc 32-bit without -msse2 + 2.00b (2014-12-25) fix STBI_MALLOC in progressive JPEG + 2.00 (2014-12-25) optimize JPG, including x86 SSE2 & NEON SIMD (ryg) + progressive JPEG (stb) + PGM/PPM support (Ken Miller) + STBI_MALLOC,STBI_REALLOC,STBI_FREE + GIF bugfix -- seemingly never worked + STBI_NO_*, STBI_ONLY_* + 1.48 (2014-12-14) fix incorrectly-named assert() + 1.47 (2014-12-14) 1/2/4-bit PNG support, both direct and paletted (Omar Cornut & stb) + optimize PNG (ryg) + fix bug in interlaced PNG with user-specified channel count (stb) + 1.46 (2014-08-26) + fix broken tRNS chunk (colorkey-style transparency) in non-paletted PNG + 1.45 (2014-08-16) + fix MSVC-ARM internal compiler error by wrapping malloc + 1.44 (2014-08-07) + various warning fixes from Ronny Chevalier + 1.43 (2014-07-15) + fix MSVC-only compiler problem in code changed in 1.42 + 1.42 (2014-07-09) + don't define _CRT_SECURE_NO_WARNINGS (affects user code) + fixes to stbi__cleanup_jpeg path + added STBI_ASSERT to avoid requiring assert.h + 1.41 (2014-06-25) + fix search&replace from 1.36 that messed up comments/error messages + 1.40 (2014-06-22) + fix gcc struct-initialization warning + 1.39 (2014-06-15) + fix to TGA optimization when req_comp != number of components in TGA; + fix to GIF loading because BMP wasn't rewinding (whoops, no GIFs in my test suite) + add support for BMP version 5 (more ignored fields) + 1.38 (2014-06-06) + suppress MSVC warnings on integer casts truncating values + fix accidental rename of 'skip' field of I/O + 1.37 (2014-06-04) + remove duplicate typedef + 1.36 (2014-06-03) + convert to header file single-file library + if de-iphone isn't set, load iphone images color-swapped instead of returning NULL + 1.35 (2014-05-27) + various warnings + fix broken STBI_SIMD path + fix bug where stbi_load_from_file no longer left file pointer in correct place + fix broken non-easy path for 32-bit BMP (possibly never used) + TGA optimization by Arseny Kapoulkine + 1.34 (unknown) + use STBI_NOTUSED in stbi__resample_row_generic(), fix one more leak in tga failure case + 1.33 (2011-07-14) + make stbi_is_hdr work in STBI_NO_HDR (as specified), minor compiler-friendly improvements + 1.32 (2011-07-13) + support for "info" function for all supported filetypes (SpartanJ) + 1.31 (2011-06-20) + a few more leak fixes, bug in PNG handling (SpartanJ) + 1.30 (2011-06-11) + added ability to load files via callbacks to accomidate custom input streams (Ben Wenger) + removed deprecated format-specific test/load functions + removed support for installable file formats (stbi_loader) -- would have been broken for IO callbacks anyway + error cases in bmp and tga give messages and don't leak (Raymond Barbiero, grisha) + fix inefficiency in decoding 32-bit BMP (David Woo) + 1.29 (2010-08-16) + various warning fixes from Aurelien Pocheville + 1.28 (2010-08-01) + fix bug in GIF palette transparency (SpartanJ) + 1.27 (2010-08-01) + cast-to-stbi_uc to fix warnings + 1.26 (2010-07-24) + fix bug in file buffering for PNG reported by SpartanJ + 1.25 (2010-07-17) + refix trans_data warning (Won Chun) + 1.24 (2010-07-12) + perf improvements reading from files on platforms with lock-heavy fgetc() + minor perf improvements for jpeg + deprecated type-specific functions so we'll get feedback if they're needed + attempt to fix trans_data warning (Won Chun) + 1.23 fixed bug in iPhone support + 1.22 (2010-07-10) + removed image *writing* support + stbi_info support from Jetro Lauha + GIF support from Jean-Marc Lienher + iPhone PNG-extensions from James Brown + warning-fixes from Nicolas Schulz and Janez Zemva (i.stbi__err. Janez (U+017D)emva) + 1.21 fix use of 'stbi_uc' in header (reported by jon blow) + 1.20 added support for Softimage PIC, by Tom Seddon + 1.19 bug in interlaced PNG corruption check (found by ryg) + 1.18 (2008-08-02) + fix a threading bug (local mutable static) + 1.17 support interlaced PNG + 1.16 major bugfix - stbi__convert_format converted one too many pixels + 1.15 initialize some fields for thread safety + 1.14 fix threadsafe conversion bug + header-file-only version (#define STBI_HEADER_FILE_ONLY before including) + 1.13 threadsafe + 1.12 const qualifiers in the API + 1.11 Support installable IDCT, colorspace conversion routines + 1.10 Fixes for 64-bit (don't use "unsigned long") + optimized upsampling by Fabian "ryg" Giesen + 1.09 Fix format-conversion for PSD code (bad global variables!) + 1.08 Thatcher Ulrich's PSD code integrated by Nicolas Schulz + 1.07 attempt to fix C++ warning/errors again + 1.06 attempt to fix C++ warning/errors again + 1.05 fix TGA loading to return correct *comp and use good luminance calc + 1.04 default float alpha is 1, not 255; use 'void *' for stbi_image_free + 1.03 bugfixes to STBI_NO_STDIO, STBI_NO_HDR + 1.02 support for (subset of) HDR files, float interface for preferred access to them + 1.01 fix bug: possible bug in handling right-side up bmps... not sure + fix bug: the stbi__bmp_load() and stbi__tga_load() functions didn't work at all + 1.00 interface to zlib that skips zlib header + 0.99 correct handling of alpha in palette + 0.98 TGA loader by lonesock; dynamically add loaders (untested) + 0.97 jpeg errors on too large a file; also catch another malloc failure + 0.96 fix detection of invalid v value - particleman@mollyrocket forum + 0.95 during header scan, seek to markers in case of padding + 0.94 STBI_NO_STDIO to disable stdio usage; rename all #defines the same + 0.93 handle jpegtran output; verbose errors + 0.92 read 4,8,16,24,32-bit BMP files of several formats + 0.91 output 24-bit Windows 3.0 BMP files + 0.90 fix a few more warnings; bump version number to approach 1.0 + 0.61 bugfixes due to Marc LeBlanc, Christopher Lloyd + 0.60 fix compiling as c++ + 0.59 fix warnings: merge Dave Moore's -Wall fixes + 0.58 fix bug: zlib uncompressed mode len/nlen was wrong endian + 0.57 fix bug: jpg last huffman symbol before marker was >9 bits but less than 16 available + 0.56 fix bug: zlib uncompressed mode len vs. nlen + 0.55 fix bug: restart_interval not initialized to 0 + 0.54 allow NULL for 'int *comp' + 0.53 fix bug in png 3->4; speedup png decoding + 0.52 png handles req_comp=3,4 directly; minor cleanup; jpeg comments + 0.51 obey req_comp requests, 1-component jpegs return as 1-component, + on 'test' only check type, not whether we support this variant + 0.50 (2006-11-19) + first released version +*/ + + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/fonts/AncientModernTales-a7Po.ttf b/fonts/AncientModernTales-a7Po.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c172fc8060c187d9ed9c2e907fe85c8592abf2c4 GIT binary patch literal 31228 zcmeHQeUx2QbwBsL`N~YbhDn%Yg2@|(B!J0GlF5V!62zE@O8kH%iW+2+3?wj_#N>?- zooA^{t<}`BAVmdqDfCN3>_TfTSXV)7#adcY?IJ9zgY^$^SyvE(Z<5SRf4{x=x%b>R z?@eYxsHV z`u1se;Cr3RuoN5anRds?oO2C}@NJ;JU0UK5zx_x`e&i0hDN^L`^w$A?&93M+SIuS# z?8r<#;`|3$Pz3M>b(2%)uDCLnbFXu`)^n#WAPm#)$PVH58(0I>S_c-jJIC+Y?f4Apr+SlKV;Sr4Shq&oVjZ^0dw2{#zD(M}J^ar5efgAC z+TWCiw46uw@4vGT*5#2O-b>2z$Pe%5u|9yk_E9TCJN}&adYE}6jJ(Ek4@)H+SckOY zb)K`&5Jx!&eSDd3FRWK}#JQ?1+76F|$G6FI-b*}unQ!ZKjvl}9?j?=OeLUSuuMwy4 zn8^;yS(kmY zkMI+=!Fs$F!svMijmp~R9Mw1X=O~F#c8;rZut{#dn~9R??!#_0x;nZg`n~y|%gxHo z&*gIG<`(5H&OOk2?&v7$<=hvdtK#c(?D}i;`sja;K03O4^ev<7ezth73@omtA&A?0-N06rn+f>Z3bX$bZ5CHH_Od- zXS-%M$IW#uZXU3B6);$UyIbA4uFak27P>`lu{+;g;4XBpc1zq+*X}ypGPm5VaGh?Y z>vF5yMQ*iwjl0-g;$G`6b!*&Zuuhk|*Sodu4Q`!V@80OHaBp&Nc5iW4x()8F?rrYv z?j7zbceUH-Ho492o$g)k8uxDZ9`|0i#a-*(=dN?zz~K+wM?uq%yW8ET+-Kdr?hEcd z_ZOh}U%J0`_q#8;FTqPZ=)UZ};{L`xi*XKo%_0b#69f3>Hfhz;ok4Ix$SO; z`!{#9`@GxjZgf9%{cb1P{v-Dp?Dyj1-1Y7Qa@5^T?n5~FEBE*AR=3sdim&v-n*KY! z{k}T@5A&#d4Cmi<{{nCF&+dEf8~FB*?%UY+xF_9V9Cdr#{VPuXz^|xK z)ZOO(1b*SqP=1HI+uh^tvbuZhxF5%}?eAjO>mG~NM@KVPW%@F=Wgf~5X6v(yvKMDJ zXK&9wk^N=W>Z-e{zF$4HdTaG3tAAZ{Ma>6m9;o?ot*c#D`!RTw?RCGJ^5!XDug}z9 zS^r@DFB{q$4mJFw;l-)zr+#?q52mf0cGvV-(?31^=^3+TY@G4=8O52aW`1twgEN0I z^LXQhjcXb=Hs0R[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 literal 0 HcmV?d00001 diff --git a/imgs/Yoshi.png b/imgs/Yoshi.png new file mode 100644 index 0000000000000000000000000000000000000000..2d9fd165551a4ebae5b95e242de734d36e08abb0 GIT binary patch literal 240242 zcmW(+cRbYpAAjF+XCJcH8Oe5vtl~(e%xqbwMM^GP;y!15(J(5bNkk!$?0JfajFJ(` z8KLYg&fV|&{c(@G&)wtR@6Y=+p0C&I`Fed4ZLCgmbBJ*O0Ju+?nc4z?VtquWU=Xag z2P*sr|6O?-TN(rKHkp&|_3u|$pzTQ$@TObhH|v6(WabhG0H?se7YsalA_4#c*jU&f zKS{FK{5!q6GrPL;XM?`Hu{*!EySPDL-Jx%8?ro$e1u!=!nQOy?vqOyavA*3OV;d9k zab}aVG)FfH(w#&mlL>sd!6i1}%LcCS0unC>Kfh!g|`cn~iJ`fWk9 z2pF~n<4&OX5a>AxY79UM4&*6=G+9ul1#;w=*89Ov2hczO=h>K-0eGeY2CP6J3VbjH zUu{4*7WAD4x23?N10YElOgV!-W9A8I(0+pH2*49*@LB~l9|av2z!MG{4l(@z$i;)7 zmUYH}8HQj+BEj%U&`Ds5%Yic0^k?_MuQR{}o{$y|-W>tmHXv9KwCeye+n5yqX(EYF zfAIsnCbpiuU`5$HVyK*LGUV!pe#z2E?tLGWAJ;Lr9P(@6ME zPEwQ~4D{-@9S2OMgP=uYWp8cW8SJFmw4^dST9R;VDcoWWoJ6NKm76id(-Tcbv> zLK8&p_sZ2_us04KzI)cUHVAyhsQoRCzdm*!fpxUCR(96{H<6s|XOUHjYo|eSdK~y; z&kO|LzV@~>)q!5M?2j)?Xzz2?;SZa#(q3n_tabY(1*YjDYAebBhk99YQGQPeoL%fj zS8Bn_Jn&O5sVFt4B6sEsc-!*s(PSC?Dcjx7?7MaMKUKgd_lbCA2Nc#7**?4s!W7|` z6d8em0KvxC`Zd%7ysIJsisvdlJ7MHS(ck9lOWgmkIwzF^jeS!jdNY&Gp z#%GDWw>c$9`kfQ+R;FLPbUnOxMSRXef7Dmg<%vRzCpzht)<{EUxHiY<=i#4jp6zXQ zF?ECMmog?>24p(`|jHBJE~iSa!h1Z^HP15CQs$3QrzV)$3C38X5j7dWQs2JW^Tum zSs^qWKD4G|g$st=x0_y|=X@aI;=wH{K(i`P5YQ3+Kni z7cE^SNq5~v8@07iwVE5mKLg8YfhG0nDe^4>iKZILfB6qL9o$?tYNG!sVlXy0cXsaG zYwYMay{)L^f-L_0GfPZrq+DB5Q|T0=ji2=g{=LdO?%|}#qVu=+?+*_TmJALKHr}2k zrs25EPYVeNJ$pTxbt6n!<`WOaQ|P=Rf8ya6_wM1QerScvecVes4tYizCy}$|MC2LG zPGK)`nNH5Om?*qaRmp`FMsH2mN0h?;1etw_rsCG@iHWY(caLs!$c?scZf>66dqyM? zakDG?!fYs{{$|jm} zLr;%dFR?SPq^JniPG%L`R99BwdHII!W!06I9t>7LHQ||RR=xr-+l#PW&E1y9ZduOuxr} zIQhqp@7q0+RZ^2TG5h2v>DAp73PZ}s=8N?Dd43Ww1 zpr)sf#+h9R(bc?MM7LIE?S5kW@wT!&;}5U2%H}*zTxO(Uo>UkN9@*MTL-5)uy4mzT zUR&R^YDGUbV!wdM7bbrj;YN?RPbv$2bFwKfaWSZOTtE+Th(7x#J)L> ztSq;8__cXlcB}1YMhGF*Y@PRW(-w@}Har&5GwDs2*~}#OFMl*&5~XBvw;YJKr98tG zSrt4hk{RO1RHURA-qKQxw|L6W%d$6yS6bK(%-%3$H=^)Z-4?3eVrLLP8*p&S5SKwx;yd6b$78XxYr;jJuu?oqI81(i zwBTns&LOAogBe+4_a3{Zrl<4iy*rX#?ZRjiN;GAPoF;Ce{dzn%q$7B5c&NMQR9A`p zs(`g4FMu5kxm5yY$2H&fn^;ak70R(q(C)o|e|kRtQ%?^XINHCs_(JP=KELOH>p6M3 ze{u?qZxq>kcBAww`L*=0hkD$Dz4M!C=^0X5LZxm71W#P)V@cc$*h@l~3cOl{%&4b_ zcz^O33MP8e9OSf^NTvg%hP=rLx6vOxTRQZ8W_^owbZ62>a6+B0(?_xgm7^h8tQBDa1X_qZ zDdv#T-G{c`kH8)wIyfBnJbghXVC&OR&qZY`mO(F7kzJ+VNT~B{1UZFz{>kq^_rB2* zpoN0(CwWrd6S)Z*an!nSMV@M>&rk8xCJ8h&1?()jxFT|%i?Xq2I;P$e(h~>|*zqhy zmPsL7rEZz#FyVd?3j?Q_i{M+7C^>T%v4-ZwV=)O*%h_<8gRw4>#|QmN40ab~{}dm5 z74``x3xknMm5r*Db<}j!_N$$5`|<0((M#L*%4!rJEZaU@IE!rWkoB{{2baCQW2|1R! zJ3o(^Yc%XEw~s&R-OojCh;3RAI?a^a-v8IAl-vPJPoGnCPBSh?sdldv$m6_0u=pLN z7)5j@uUvDD2qG78gacS4UL^;5y}iKasG#~Dme-oL%PW=ScMijWVY%?w_q~Sl;LZ={ z+ctv*n7xdJQ$6yqzdZb~xBT!XtvGB5q=5E0j1Zdf0YaJBOi5zovjCQb-|XkW=szl} zQq8NWX*4nre4OtU$VFEeO|8>bc=w(qkfkN&y`M%K#Fe2%-)T?ibkoD*=mfGdA`F_% zD2Sm2bi%&Mm%79gr0`ZeSV>~T?>GgxBNmo?>B5Bv+8NrygX27+Lyv7Rp~U%>n>>Kk$t&FDD=FT9JFGC(`G@z{)+ zmnG~w@V@d%C=DRy224u^0C)I9>B9NG;D++Ur!OQdV^qD%n z;X<7%WK0lK4u9H*ocqS{aJZw;Q1<}+!k?gfng6t^vrcAzsmP4ayW>0bH~v`D@nPog zHeJF8LL2NW9y8izZ?JMZST6e=fB$K?iS!*6Ha0{iCr_z~3-u`$StX&uvm*$5*RRn3 zzCQhyp{{(@!SzS;JpVL$h_U}!^p3^@&00-{T5A`&z3v!vbe+6?fs%8cI0=%oQ7`&3 zcZ2xxZ8OSn1T&i2RBOKg=FGPZ-Lmc@U(-}+9yS3kdvDM2>Y5#SZHh!9P4&$Is!`kiLHSoHJs!idMOolScaGmm7XrTvlxntUSmQYq225gYC| zg%Ij%>TU_gzeK~cAhsobDV+AEMu*@urn?s}N`3wMn9DHTWn1NzQZx}+pS4{TF=luG zc~~K3yKT3$n#0X{jT(MNlN1*KDOoD{KUf&$`2jUgJjqTzihJr<(iW1LE~F?LgTABs zB*JAU=iep=bQyC?i|nuuc%XiidPXD7sEkKYlpzB@h-5NACCSd@-Bw$|D-%GmB4{`K~>LNEU0nA6zeJ0GL@LFOF!9WnJ9}kiZuHIxe{Wxm80RbR@y!TJq2BAzz~M}|GC~F9Ke~&z;&Ss+ zrCA|qk4LJU8V9rN?rinP7~i6dq(a|x?9v`{l2;dh{I*--1pf{oFG3lq{C$G?7W_0^ zUDRl!kHOS$UhvuqO#jgITGoKA@`Uk(0RctWG9);lKC$h%@Yq;?P~tQTrsY5V4>wgO!1{0WsPfW@8&y!WV zVV5qpo+YTTRi;lYjm7g|LHpfay;i+NKRhY~^Q15ksu#^VMm&UlLU{`LFIrd`4wJc5 zlrV%$Ou?k(*;|<;t3!FR0bU{V55T`2j>DFBNzAY4J@OEU1k>c}(>k0B(jS{#T;2k$ z;@;m$h;^xP{nEowHBYB^QnWJK1VJ@aj>D=$UHKj$cALzwk`E$5&H&L?I5#0*4I6>+@#rMBF|_YmE{HeJ!StiTWX(MjoqNo|*Pc zO+$?4c2IX~_=dfmD1+A_F-Ug(^zle%rOq+z9UwtHXeN0zvAc7Q+K*2Q zvVK=7iU|w-JL>pXSc!$CHybwly@tqQ9AR4zr0KtA(682N%z~SI5ZjgRY)NMm@&ci^ zJSoSBO$U+?7R57Gvx;C^A6T4HQW>c^6dvIcu=qA=n-fcJzUp>A8Ph`M(BN>A^eZq+ zq-l1kt@_)t#8m@Z7ODmZdtL;@xd^OYu+xan-|=;GoR*$%2Is^{Cp{2A%|X_A3@WL zB$FKa2n+IzY)1IsBA=|2P>w|I-aVaBa(1CK8`=M49(XsZMBpaiq1}d)c;qE;5EdBV zHb*QAm&yLlmOEflf+h^JBTiALMj0RWmYtB7#dzMd!Wb!#?`KJ z{IiuYiVlAF)yE@F3z|{e%n(^JJV~ zz%qXg$LGNkl&E|2U_nH86Mw_^=`WCaE>0aSB8XTTZ9E?l5P(8p*+op@1HW;Zm;?~} zFr|)MEj!}HVhea3&%OV!wUfMU5{5u-qxUr8OIJjXWW#B-|6r~GL(lpJI&%Y{y4$&f z&9AtKA%(xdT<$TNo3{w^JngB0hqaZJ`V};spHa$_a|GwX{^c_D&K(6=V$Re*xZ@3m zUS3C+SJl<-68}mear)F_`ny{pLI%{}K&TRd_ zb^WtcLpTi_=|Hajce7e)aPTFkOln;y6R)ab`)}QE(;Bc>T*lX&(dc+Ld}<+8M3Nn+swFwY5q)DdQ2bVWbtz1E=kzxeQY@j{MofiT+~ zPH+~cVJdan=$`_O8U7E-hu1+`(uKgk17D@jpVKUmszMu061W zOgElVLo1#XZdRc2?yhI-beAne#wS7uF-VdmiG(>90q1@4xN3~x0|EPoA|I=sd zgP_F`xZ5qK9#JShXt_u&djXca`8;0$*4J0S_t70yX)Wu*`P{C{o#-1=FzDCYeNk5& zieQIQkg9W6j6NU7y4^i%lQEduCRA#D5tcl`)10ry?bfB#Ih(HuyNT^^7~@++cHxK=qX3j^6hu?$zH~^v4%~0$CamoH14fikEw=HfEa= zI;mmG?y!{PPuf2kNGp-Q6Ik_b&$x6q^>3XF%K0vv&4N+hcV14ie^W#EVUcUGNg+@^ zhi&}`8_x3WcRlKnLmVgTh^yMnLLZs(KX7{;(nX=R|>g; zq>wrAieD?VF>3nrkHAwUpPE?mDsQ}J71{40!O<*8)L2>S)gb@vJm#{AX@4h4JmZG0 z*2w3zf#DNC%e-W5(~)0*m5CL+E`EKSejRuFm}E-Vl@q&8Z1flxT{taC(!X(cakH#J zFl~PUfnciG(i3Mv;Cb3V-@e&yopI5q&`kbDI=}R$wD>F!RZv$I0!&&WjA+l(`;G8b z4{p_S^Ab<51>Ek35G_7=F}m6CQaQQ82e`hvk8&IRk}KM!v^?*Z-#PnQCxjqTKlynx zbS%0bf3isV3+mFKfDs?0#>T3d?vyR0uvy$wWx31QG0?cvKmNqVIv8^al7V439E-BhpcvE1oJw4sUNVd32$ub=k?`x^Y)mm*{puP0ef3_IT-*=BNIR zHMy7D>XsTe1~$v4%jx%S3@f}cp`8u3OceL-*i^`>A#K$zD{oa+P+bZ zU~{6iZ8yErqp8-R*rc=|;@&I1N0l;Rm-$h1TU)Ab*pr*?^U zsY~VwkEfV;l|ud$&e$6y=@4vTQ|wlbKIguBk3GnH@9#Usv2fQ%(ofvr>o~9AQe9Ai zSP4$jMmS}3Ktq}N5@e<=zD8KIiJ9FfxCLp0&=GKCUe7`eGCq-wAe3LL?l|6Ot+}MA zXT0HX%9ylUx8$*{gk9#w5!t)>?!#V0Vy2&vPJa}){N&#lJRZvxn_!v~{9v0#Dm{R_ zg01C)>XDD=p6L$EgbGLjWyXW$&Rzgpb1t+IsWLvK3I`RwYs%NVE*%DuC~piCzT?`Ah7Lz!^<^D z9amaenF2C9k*1tO-;n5j&nA*r_?r1SR@Qt*V*jYmQkxQukiCzKsNhO{T{T!lu9m=! z7@dbzAYvUb9`AU1y{BmbK^!(}lusYnF6qynxH!V4vwO`50f{OZIPfnQhdvkMI!cM` zHNgZw5PF4h1*V+a14*31`4<(O<@sB`RK88yJZE&x81rC45?hauc*&@{#`nU^-=tK6 z#uozJ6u4H^j|d!^f8UcIC{_J|Ew~?&Y7wA?l6|r6j4oelGw~M zsUw3-g#P7U5mH2T|_^Vyp&}pf5u*#S6dIm*3g2}M*>-EK|e}p|M zqIP33{e;pTW4em5hr@NR!PW+49Yn4gjy_&~=U|*sDZ~SFo%Pq!BngZP{Nxl`F6IpF z`P6hZ)odR(^+myPPhNnXhE}lrC#dlg7SskwVJM&L$!{&VEXcKk2DCp%U;XH*T>b{U zt}Z#AtFAEQFm{VVJOp3M)AXn&aPg$i)eA2Rq%ak^`CzG{H4EDYW#^g7>p>v z4BSJg?gtZ6nN^djgal92C4{pnL*wY7AF6@_kSAv2_l@otgBY6 zT&c{}6qj0@yFj4oblN`ZQzoSrerHc`Me;!gKO0I_(I%#{sh!OI&pbYD#?u=Kkk0sd zj@|(1`aALs$?~876=ryyVJj`)cY>1a%&G=5&%;tesnpGM{)@~hy-q>WauIM&E#L+QgV%h`fmJDl~ki6E*>5{@2se8 zPrrZv{_EF)7V0f!@w6_bPb2rMu535(EU{BAM%W$fMNZh-Z_jyUa4qn~ei=VRgysD+ zV%3JSdq4Unw-(qKY0^g$Eb(7>@XuhX0q&-@jN}_c0O_)`=$Z~NA*A{Y{I}}A|H5xA z=C%9JZ<`7|!|o0bi&YG##2m01tvSTW6*T-p)3kf&as7{F4ZZJ~)Uu}$scAL);BxS- z!`cn723A>w1waAkqXAr|Isws8^@F?DWk6QM0~K?U@(H9bjmwrg ziBr#cb6U8kz%W0^4&^^uGxaey1sTg{FjsSv4^wH01}e8XMMWJ7(QJ%UccA;)jC=RH zc6G*(0-66^q$#@{Xa_gnsfO<+9TL8z^QnoHYBps`-5NP|^l04|Sb9df6itsf0{gf{ zK3+M>(xCIL7}7k5BoGcmg}{UlpJwDXzV!Zm$GqePY4++Z^81t3XDG|}Pb^bH9jVAm zvtsP9R#tKz4`DQUT0aRZFM2%@22T} zF-Gx`Tt>o(k>MNGpA8c)km4-@KmOEd-f2Mf^Zwdhww2f>un`}+!TUn*11%EQLX*cvlrT@Zu5H}M^ZM#l@?w#Gp%cSFlTo_96LQU;k9{q2Yzj&!JOW#~yjHiGcPQ(~b^XT0SYD&(+#P0NgT_)q)avy||;t zmey9Zb@7lhibXo4yxy44x(K>tGlL9}ClIsi;E@bVYqvKdYd$w!DkEE`YcnjUyNR+M znJUyjPVRsz&~QNL;O1)gw+81e%Suzs=$VhpAB1ntqc|p9OQV$xU=NiH7CpjUScgqq z7FxdB8bup3oR3yNfPTeAdoDY-?)rL9rHm`PCccGh^uEXGEdmBIJcM#1RTH|J)TZ*4 znCLnVA)!Um$k>6;jfr1UQ+rMrO8NQt>_?t+X^|mnI8)Qi1j4D)Wf%NF=j>#c&e+ZK zGu=-eSFL4ij4p=BF5cx77Wm~^Z?>Kr67}Z_t1RRZ-caE3ii&Et{pcbPBHnEwpoq#_3?Gdi=rHR+q{_alQ|L;rcC((haDGa`}Nbp^>o5XM5 z1z$fD{8^@ZFKC(eHp#MD^Xl*~nM;yjJd%!FokVh0^C37F63Gu-#eeq?4-H3LJ0XR(q#mlv( z^LVjQ*$Bv{>QXZ*8)|K7TOSl&+h8Pd9>+TD871x6C9m4+Z_V>z^F43exN+%5z6!b; zohL^`W_uv+3Z`@j&Aj4U?y+cyq)TT-?I^*q>(WHf439dZ&w-pjhaMUpLMfD1R0-;P z$)+B>1=Xw^AMg2Z%p)kuAeC%c%2V;(kHfB;%bZ~?&)-nK;7G=PgpSFuDsNsNpzq%E z=VBJjap|iiPp>+>?;s%!UVfICjmS^QFe=&(-9%Q_6K2dmOm$iJkZ-K@jp1QN2zb=t zgVC6IS>A-7CFU~AElwG$rAk4;no6nJ-a&2T>+y^Nr{}(Ci~|;#{RsZ{EHGc)(2sDYml1w9S-q`A+dHPpb8m6=BbM zDD-dFViK1Kn9aqA)PiPM0Mf!QN-Tu#82|ovI>P>O>DPsQxGB)N^&kc@`cjstjpRC+ zQf6oFTl6j8ZYW~of>643#6q zO|&uha>hOW&bo~U3Op>>Y$*5RT&HqnBTRvbutokPxXra8f$*_x%9{(M#x_|5^L7`) zBSfKAy{KvF{a}r?p{%g(fcXdi2YYqloAcouP%2r+J8Dvw@GAOFh!XD*=C{!l(f|C? zRU|9{WB=BVqvYsu?FGy0-2t{$ zZ*rv6A&5W~J%BcIAO=BC_O2i}hY}R9VqT9hzTl|GZAcSO6Z@`R0;hKNfWj;w}rS{DmoAXU_Fr!wHZJk~g#wE3n!dYr$ZVv!L#*#v84N zL&Cji@nuTnZ57a95V{yU;yms#`V1vvOyt#4kA8j>GYXFZpV>eF(=+f;b;U(hX4L8@Z_q@J=V<@?&btN~ zt&P>jY>CQY#bJW<+GUjWF{h=c?N&Q=9go)e_`L5MCU;k?h>7mB;k;i-$T1Emx*F~U zUWZtc5rXLZSw zxO-!zNyJo6>#XiB4Yguz#(y2sgr;{{&jw}idW6p)>E@Axg;#Lx4JTTSmLaq9eT6gU zXmiWFRiAndjin^d49)+k4fEE7hu*7ZzqH%>B4abkvW7%JAx2L~9ZeoK! zYxv75$iw-aUjiAnOX#8YdATRXqT38vha1B91!2c8B@@z1-<;S|0^+&-4ygZZ!&j?v z3;Z{Plmq(`qbydgN%%3!_VBt?5JyE)W3Ivqgz@j%?^1JeoY$PDdV4~kFzzHSqNWoR zLv6a})_B21DA6rbJgEE|X4!yq-GI!{Q2-wg`vgE*?D0=~r;Wu}lYLpEcTI(y((mrt z!SEk=i&XA5Twoo5ySSK@Q+pcrA;IVNF!S#BEM{63e(g4MFT=Vi<^8iT!n z@DVgA``n@84nLDs&8dIXimmq`^bD>W{lMv7j{vUAk~37BIX!zm%I_Y1%<@{aqY|~J znmeVIhz-B{h#X50`R{Sw`;$Y(CpK(2O5b)_^cWuqUTS$-XIW}2vp>M;Bf&IB`u@F2 z1%;L!YH`)hQ3j06`vRze0BcS?ss*a{bxs%pr_6MEF`8un|LK1byqnY4=UD3tQ~Z!b z7KG0@8Y%2Vo{sFjh91ghpQVBkp+8C_l6zADu_-z}d-+cEr4sC|&-)a~FSU2)3bp%@DY zG>q%M7b`so^}_mmxQMJtk~u3RENnU|n$@1!;ge_bS6+kfO=U?LF?QzKlBJn_)=^~_ z*Ydku1@2Y)ja2@+9pAR&g^875vEjyLkUfe;Z3or;^pY|}oSg2lC)`1`IP^NQVa160 zBm}&l*w=joHQ)6dL(%8kUg>&h?frV&DUK?ApD73T-8|!pD^%mz!$c99NE&(s2gJu7 z4E1N+!A#?%JD&`1rq0G$ZTOj=K0UhH(}Y-zTFW%Gs&TgnpU!C7N`Kcz2MGMhcpl3m_cxbQ5hSeEDkw{uDmAlXFys3A=&QLH~D z#yLH8=rtphL8GW+yFiqz0B6gz!~=23orkz;Ve}pG4-J0x#+`85X}c4FeH}`gEM&ON zNsVpkGWsh?UHYlfIO5RfV)w2npRFmI=I8A_6FRmxAx@>;4dI2C#-qpWF?aY%;|Qza z3d?F`D!7Nrh*@O{5%ZcGcL{_e-^9fSKbB2>>9adM({?6AEsV4T+V!!7Rtq^4Wf&8ENLAC?@4W%L$qqQ5i z+dRpLl`R?9&h|%r4 z9DS-st1kOcv1PA4{NuJxraM}t^c76>FS?R=mDZif8Pe-cYpk;HP|lr@Q-Cqm7EbZ|pI5&R5p5y?>1D=ybro{u__^CeCydrLG5Ev_$Qvtug7#A+>k+GI?vZ?fln}xCk+u>Q8}Xsp+paht&Hu~^UG1~)ssFfd9b0#^MYSh5N7UGpA{p~X01Ti)0ma535eD7KVq88(zB|Flb6`WC8@RKn7 z=s5(Vi?Zj+8QQ#;Y)xKs`7r#oUWPU*_Qm?UksWf{7|ThfVNRx!-5YqegBFt;VaqXe zPw_kuQM@QRkE&mbulq2;G|dsVb#2<-`58}baK9D#^iG32otwd(#~ZT8NN$ET1wfQ* z83>C|RosEUw_bbx5ugsRAqPb;CC1CYWr!2f{?B)Q+qE{OKA!2BU=)qrgd?ByVI=&K z+e(bck4l7K!8gqXdxv{t7`(g0;jk%$j%UG3?(c)`u0)?e+?OQ*#tbof@zec5R#WW@ zrw^0=@X^Mt@lfr;#kb}LRQI~1rLt_Z8!z+M$n+kg5F-0fk0qYUA}(bfXrCdaB8Etb@*-H;6-Q;{rdd z|2V9DP!3>AZ}v0sB>Mk>F&y?XWxnwXPNyacYoI6?`Lu&v_6gZ~Jx z{p+1Z{seoZlp^O0pH_1&MHU%mh0;DCC4?X^H0|sp*Xq(B2_=FJTbtDGgm;H_-T4d{ zClV<;vbHhmd$J7bp5+}`29D#}`PK0jbaZLzwU-}stZ7w>=vOMZ+88#4iSCZ%+h*$~ zaqmYe>5GqflcnR`q*2xCi+Y9MFFwKD++kFOJHsj48IM?1jW06&9YXaNkCFDDxf^=^ zedf86G(HwzEQ)oGc5peaV$*qW@gA&YA5qwpnm`DGbb81_j&Vd2+M`t#X|yKF3r~JT z%MgHa1x`2AZ;Pe~d$_v8jYBO(M25^em&FT^ucL_$29%lmk5>ic_!|hPZ(N?6^pEPV znbX~8#I>e~YA-jqM(#_5nD%Yp0^*e|Zm)`}Qw9sEcT z)dpCo^gdvFCk^|~@&k3cBvc7o7`-rAp$Pbwkf+!qhM@bZyLOOIkm^Jbh zQG?2F7Ja^Jkwi%_)v#eK<;Avb*~gf_{1dL3mmJJZdoy-UfUDRz+n87~Sj7G+08-|j zy&91?0}d6l`Wck@?@c{i6y%)#wIa#hQn(;D?oLzj8|_i8{n%;*UGwmJ-3Y$-7(x;h z&J$ZOm|Y~@ZnO!x3*_1}-NmJYF5fIZqxRm{UbFN#LG=|~=kE$%qK5>y#@KnL^!MPe zB!gLlYmq-Mios>z3_IW6v0Mq<6Voln3~^rP+5;>i^Jo%UYpx_mB3l_|$BR=lo_%$Z zIXKz^!*?^)iz~WqP)G@yWmqtUd>*0O_7tB9w-+J>eG6*7bHaeI4CP{fpP1y${oPLz zenXhe6L?hoM#@(rVYh<5&5{KNy8wAr+iEXlAO#h4)5A?~EM5$s1nWC)aUP_xtNwV` zrb`XFGTsnXGb+6w&JN{8JV3PAQBgstq>}8VI3VICwO|BkRxeM53xW@fzK0(xR`iCG zv(FQGpz)Y3<7BH>Qik#}Ee*K>7KH6qAGxjG79S;xV^nA4wF(m=!?iX_&PbbucR8^d z*?*U+>uAl_msi|-zHaSeF#-to%BTF3mdke@-*#{%FDhwoCA&KYA(bl~HEL_AEYVoSYak$|D_< zi!Ldy=;wjHfTY7W6`uCWe)+f+wXJkr;IPpg)`c8M%*mdsA+tiy_cJAyl zC?bd5jH~6q(aq=g5}(2JOs){=S_n7K&iKy9L^MMpu4!%;jSdjNooPb*0s9%};P(f|8q2A-lb4@s``u(K2y?ZgN?GHbQ_g-h zk;EHZM>kGOb8uAL*YUTd-=^ES!n9#B?7BDlmEmi>=(6M$9pkKgP1I1u8(6Gj^<530 z%3(jq_kMP{$!$}OX!Pi2>v@sAh50G=lu7M$W>NShaOXDm2K^FNJ=PEJaR5F0xuq4t z7YdvW{XDD9V&mb=vbEKsb#Clurh6Z~ZWD8a_(5L1@sF>ot7O)U6suXa{}n|M*z#a^ z-D?$QwZ1Bf6J&8d7rw!8>Y{FE%XD79ImcP933VH)6-2WPG4}9>?KTY%v2RbxN$}^S zwswz~bR3m@4s5r?>+!w^Pv)X!mlN!e`2*gY8o%YZI=-4#)+u4mzvt8C`vAA}k0 z#TBzGfD;QoY(IXt`qO2L|0Q$35EP6!8r6sFvveQ|z!1#SgBy^({0YWodRltn@!lXV zk$fly(QNV#DJTFDjw;aP!f1zZSQ4rpj?hh(d;cF4=yymAD&%<0i8~=6Cmpl3qlXGFvlK#sQn^lRLST=hu|GpwSDT6k8AbFKNUQf3%*H zg*EWfC(*1~NAoOhLJ0XG8#JIsJPh^1?{2Gf4mHC{Lm)1~XQaB!zP1^cX)B2|%E@^N zTopr`gn7-E5Q&%5N9)F`RE8f=Lm)WUVH;?U+iGTHr zSN*#VReN*c5LiADo4MsLfS+D30n0ENf#>Yz@?rTyc)p4eCZzLOj6@AeAEi%BAfKih z#jscpUnxJ_ur3>I_;dR!6||Y4X9i3{GETO=-7GgL^b_-on27U#aemrIgoG8~wJ_aV zs9$QUlXCoi>tK6sDdae7-~jg9=pfW&*~($oZUO&*V==SC4>9UwN9^ZFNO@~!3M8`z zw)P=8Ij&qsJFfkV<6IjBkzJb5VOSz%7F@PX!c^-|cEI#S33-BRpJx(MwXyA&7Tr$_aEi+x0Vg;scR8V3xY#C^b3 z54&FTKki(@f3IMd7$hiSA;+q?H>gxBi8G60Ub7qYY$VGMvE3d*8PtO5@qaWF3FS$V zCA#ks;W;pgrd zpo@3|eeN=l>>V81J9^^+t{wA4x(cTIlcpT~>buT=xSO1fR>>9!aPq*&`cca}P{c|w zJUyEqIM@ZB7uF+;=0YXx>d`@`r0%3#^h) zu}E?!oKm{L?1kyK^~U(JsZV~kgVQQR=tl!02*1F%iZ=W#Uull?l?(8Px?P4?ATscL zQ5k{#8}EQcP;2mEjUM{B*947U@Y%=@(}w9A@k6jZN-)DrZh4zElx-%wnF^`PN?{P zh@dksP(hesOwqc9u!PX`YRuQ$-~##;w)&Fv{XqTa-czdg%*szC8u+bmLL-_35I0clmr4r$4{PlF%2g;a_w;?s-5v~x}R|Owl6jkv^%-naj2I2(gXOfikiC_@UH%|M*%BI7ql!ejCop_HvWWnfiZNacaObnvjzqiR>Mv1xS0DX~Z%uC-_f9!i^+ z(m-rKO85C@vzc7BS-EkA+T{`R$BV}3An%ur(U*&BOTKT6T4KuCFF^+(H)O%IGUjlZ zeMTT=UODbauG*ugG^yDAu-^OU#i{$bsVC2{6T~3}_Jn@yl;g-$tUFS3G1;?^PE}q0 zTzFPC=02{o7xZyfjT7s`ZeFvJ!`_1gNL=pLG3$SxLM+nOCl`*LsA`igTbug^%KbeI z{rRHL#>FX7v0_x9ZR`}dUUV|L7&lZSEMd3H8eodD;plauDJjP8$DFh~NaIX{u?N5$LMd5eJ-x) zcopKx{+ZdUasP8)2TFd^Q&H5}-I*S@xQ&;%48o0X@+?sHP)5(GW)GH@1U0-N=w_et z!o%-7jSKH1aax>a@s-vkH4$JE+j|;rThqc#xCryx*5e@s1zb)MQtB7fmbd4mg-+x5m$6c1|ojf6-oJ8D@ zG?W7~1tIV(V%>Y?HyIC|jA@awBV%Uo-Q7QgvT}4k1P7Yi`p{RdLuambYo@M?w zK+ecw%~i$t9!e+ZU*^;jr;0g=6T1S@l((EIf6->w@1k%sA{of^1fDn&{p@_Cp1X@R+R7QFfm&Q&vB&Wt( z?z@5JT@odO69-8pt7QDXHa~oBvG=c+Ed9O$bg>98LQn*MfJQ*9_PSa3NAA zJkS2uu^(%PTKRL%EL;_)&9py9;_a-H?)KWDGl(7z4i3X#3d=C7{u{&e>O&YhE8&1I zi_L%)Wm^ffZeox8K@tZ@;K3hxb&cC|^6u_HNb}!jy%0!?Yw&cle@Q>T8jH_9TqwoN zdH3#P_g;VX!1IA4P3#IRD(9Qz*BGiB!^Tq!Y=z}=XWvJuzFhRqmfi%5W7gnxk>wSu zq7GdSS1f?cHznF@@AgjCV&ML>FTQ-8YOm#6exV8V_2DrGjo!SwGdU35AKs5KiwOn< zEvTXUl8+)H^y&X~d|r8cB{ZKcM`4_C#~nY$NPkSUM- zfVx%6W)Yu~l%JPpPe5G|JVp!eLvGiLVYJI^#JT)p8~+e`oT)`baG>O?Ka6=o8+29F zT<3&hwo2 z^E~hO`>5}R8NeEN`@4{Y4h|X2Bv^{dknPXr{W>(IIMamYeFE@uLk_(6iUX0*@H0Vh zEEd1EHvG8a>yh0mE@IgZC@^A3XF> zeo4xv`n+5X-$q1EEblEY&gk!#^-^3~08}bY@I7`r{d)T9KiMp4>*J3hKe;oP>UkY? zywuujF*o*XNzZ1e{~ZSXk35cU#2g8Gw5)IZ8XaSa2GU$n-%9ln_4%T2GS zZM4?7kKD|aYM8xP4ju65N2$wi+w*5kPbMR|DV*QSL)BPz7q2-({LI$xgGKM|`P~wI z{1N!4uQb8sB$Ep>U0$l5dfV0Up81q9Dj?`jT}i}{ME_NKPpSUAerX7BHbmY2@&wT- zf`|L_ZO@(Fb9LBU`8V|Oa~4-l(;WnY=*r*Mz(-RYKo_JPv~`qE;IKfNo6xO@)^D7t zmroCx5xNzVLG=whkA`-4m%ti<%ANS^=x90`xm}O*1)7Fm5GgV7KFK;S1!!B+9>O;7CG+&(b zRz*#)1jD&+nXUZ4<#5DqKn*Z3u1T#NAplxCt4NY&T_@cGH3jLB?HH~1*EOT;F2i3j z45pypZAoc~Y1z~7WScqB`c{+7Hzbs)&! zz?jyy-u`0hTzld%xw@95Nw#RZ3EUFIzos-q?RuRWZR0A@BLH%%6|)Rp+aYbNej|8> zQ09&0|21AV5Q4%uLX|>$-C)}3GuCc0GIBMIuRUe(J|n}xUxxz!<^_ZCliNldb-ogC zOPo}EL&EqW7&)t!Wf(#|rsRr<-A5YY680Y@!B(F`u{kr5=3Xji0xf|dWgg2uOLm4i;9CaMw}nSp|SoMvqUO*R`GYq)QlP*MA{LU zM|K?P?hOwU(ysc|gFC0i@jApj z9hAjM;+AxkE^0n?YEp0pb%qzO0Xz6x$lv#}1gK>JG+l+$?+umTeEWJoDybk42lmyv z^Vm#T4;eyXnaN}(H`!5~QmOs9E~h2%S(zGw8U`n-aHF+}|NqGiYq{^HAE<}72*aT< zlWf^pVt5ub3YPLD4e+9`2+qJibLgWJn|_9mi6o4cljA97tO)HjoNBQq30*zFI(A@Z z2qc`n=^Xo2uoTqLuGWws)LbMR-4VOsZ(mhnB#=;qwlNrjYE$9BtOno<_&}lc}v`OqIvT zg5h$`9#+T$nLxB~hklz|r9%Gy4tJ834UjSo3@4LYUJ5Xo2j*ctQvF>S-R$gjw_1Gx zX&VXZzYOKfyJNrun{DXh`fIf&(QBv0EkRY8jHdAIZ z^`R+mhLw*nM&z_=EWB;(oXeRX9pd^@COUijUA{N`x1?p|MP7ZKMu+J8rY};_#KxpQ zKhZLxh+(}OQb7~Inzds8C7-`}*+_nS-M2=c3b0-;Jk%c&x#BSRN6;lTF|n0;z|vxD z*7JB>z2jDE$MXGT$f9-PjN5E;u_Re-$Bfk)aT`iGI%T98HeI1s)qDxb0XzO@R;{Pf zz2*5_6qElwP!jSX`b-Q*}r_QpFTto$bf z*r89e(*L(TN6TVEZZ2Jnr>YVq4<)GMijgloNI0%%C-497yKuEXPoK=IRRszuMWdPJ zQCMH1HkD`@3WjeeRlR~ON|SoRiJ34j;ERZ~0+x?-NfGqVk*3v#d4dJLp((nn8hYJ^ zcOb`GLw`hiT5XH`&b?%-zuB`>B`W8aA^+_^dw~l;yAV#5e*!mk&SKM)CLYRhK3f3m z7lp0-cf#1Pn3o=*2}O6hB^43ZPm&@I;4>-kYKGl5aR7D(5hrAc;-bF2)DIk20Ne#Z zZH_uq9Ns0vQU%(6S0zJ!-?F+ie;BImxXzBEZ2i5E?~BnR$Nbz(!-KxKIA$08@HhyY zRy*h@g;9?a{2!jPBXAE(G-KQ|qa^=Ga1?;CNZ3C!(k>27<~G$P%J7W&Xgr2v5Sv{4 z;g+7%w|<@ir!Oh$Qnhc!UT{$m`BG+uUbCvUzOpb&kI}daIDR~l7PIoS%|*kDt=zDw z+ws%d7F*zfzVFU&$`_OJZrVFgFfR1K>)K88WS!?%=e{`V+H(abN{L*=^~8$BjD2!9H-OZy{2#OcCANe z@X56^ZLdT~C<$WAXYl+bpMHw>&t%D@(L2~h;9fKd44qaW@^HYOi&Hiie5C5QS1H~J znU+os^8s#!shLylVb{&-&rC@k>|v!kAAf#)*SA(o%frN2?~l1aGCHvDo_LD)7*Ki~=OA$YY*pl*q$PCR zXi~YhkOJPkaU%|?e_3Lw!%eDq@!=CF<>1x4RWbqbf{&(lCM=)*^o0oRif?qb(dI-` z^mbz@+%vmg{2UZ`J7gjyTkFb|xMe*HYMF~95g zY}BsYA$dtUt!nU4lv%k2@F$mnxyRkWeRVenmVoOC4U>N>J1NRE>=B`JQ0%(nlQPBc z9uhie3!nZf9wcp!hms6E_=e=*z{|k*ktjk1g~pg#dE3M%36>mp=L-70IF#D&t(gA| z@#=dygp3`k5F&J=03|qjUxvXQ=u%~#Uc+SvqfQAZcG)uPNURfECMOU>(>iVxxZS?a zcK;JAi-s!8%)MUx7Fz8aa`v>se*#=%(e?KfNKYY`au=8KqZ*$DIDIw2+L>n&^3j9l zgZ1!Rjl3B^34fa{U&jq02rS#D(;G%Bfwc0UkYJ?<*r!AxG*Gqp_hWj0E*Loi^LY4~ z;fd(|dQ{8M(mAJCc7Eu_LokQ(QDC0Z%Gd9xQTA7EhDC$70I+Kb6|P-FknmdUGV95W z={hh-fmSy!iN;qwUv2u|WtL&pWKmZ2Vx_IwZf)FFYXm8y31EUl9pBxN8v-}xxwd~U zcu%vX84hJT{?yMASSFVmyuM0Hmv>*rQ@LEdiL3iSM=-=|efG~wVt2KMw<{Mlu%26# zIkA4l7qb0mXa&G_`dJfA%ROx_zMdrkI$u=}6#%8NBZ&IAXaBujLhNtk9}j2kljjsA zf{zdYGdsbpxLKe|Is5?YZ8acGk6;oIZDLi8W0bsC;Je|r`O7rc2oE|H6pvW2=fm-l zw8s6#04%%A>UiCK$0b9~W{CpcfvOSbx`7@SQ{Z@Sjza!?4}yP#e_1W%b+roSk@`&4a?~|LqIprt67?~ zcU6Au<4EqRK`&f=*=+7LH?x3qP^3`vQw19EpY_+uOFyVkcJi8oXn`4!-tu`sFLM`&;I3FXd>V<>$B!D6Mme`sD01knbc_1oQI`uvOgRAC8&=L zdVu54P{1>Tu;qgvsIg^_xuk36i{I~Oea*|wtFNfj7szIa3!#KyIz2W^$WPP?yg`ueBdmxiSGo?VjL(Df&xF!HNavp zrilBfrsybi^CY%eT|kxT@~qQ{bcWg*wgfwK{R-=eI;EyIcfM98e`hq96&BU6aq6Cv z(oWaIQRIMxE?Rm9n3QKmm2D^9C1D|bA~6Mm52>L;U?jiV13*Ou|2*%X-3k8w&o2ER z)L<1~mf9Wq%a3-q1YYZl8YFO1eC@3+n2xK>w&BGXLVbAlzv$sg*Ot;ow|ds|X}30f zrE$HEW>m08GP_i};w8@!y7?e{d52?ei|U7nX{mzP%=+NZMUI`aKN~k_ zt&VO{wJ@EK*W_Ebs$1B&CN|H;Os1Q`ZF(Min?A(h9106%Cu;Btn7v>tr~;!PT~`N% z8$zs=mvM3K1NAmr#B2GHrJJytkm)jos^y3u;+}v>KVszhQ{Xzr_r4llr7u@++qR$@ zD$79zN#j4fs)AvqNFtJB6{iKyw_v`F| z+N%?At@q<0#xH?j=}6<$wjX8K`>qM7gOjt4at4w9)2zK8kIc@UJn7@(?&|LD6D3mAL$86B&$A4U2Np-da4!JiMtp9v7_SQ8} zW>@@}AZ)mwmx*JyomubS_uDNvl%$WEFJ$+ewdcrJ?}k9~0EyxYbEcXRQs-X5Vh~Kr zM@A*sCU3p`$5!m}Uxfr_!1mvn=dV=%`Wx}^zv_xN^pAN6&uKeO{o~NIIi*&X^@&G{ z$Y?|g`ag}yJ4`O;&}ihu*g}(r$3l3!6UWQkURV10S(jU%`EvG4Z$&K1ptz{0&BE2y z+dIltzqBw;(8=xF@LLEb_LIHv`I11_**u*`S$MQaGV57*(_Q|8u7AX86S(J~=iJy+cr=kFN1$I;TuX`o zhi@NBaoBST$W)&J%=Qq%BNnp8h9lmw$jXoC_ml56-tJ!}Ti@Azgv}Ewzerf^3yclg2 zY}5FMPX3?ST|-Cfqumn;z0Ln|z3^z|^!q}&^0VXKtSEd40a>AX@qYwBHi@Sf4*ca0 z?m7XvigO=A;i|^+L}9Jwn~V+IxX%%2N`fEiqJq!#!$Oi>B$muvUZ(FGzfe_EIFaWK zNkrb!eqUx{=LHjt9#&<^DD=WNZFzc}`Kevw>*k}HYl_pz7#Xxdq)}1+v>Qq%#wxh(Us5>K+SHbo*4W68zI)@w zsee{hYP(iDVgU#G1uDiHw1Q?tyRpi?6Ho9BM)&6Z1gO2ey(Pg;s|)yJpV(lzKcY9? zr(b^@E{sRb_rfIU*=~r^+z%*MxZwh53M`=OS`j+R?=GS(yA~gx)PS ztG)WtiDiwtSbkv^L3gLJTYpgkZO-j)A;9xK1OWdOs8?2r4aC>tL>5bMEW@_gOP<9) z6Wgkp)H|l+Q+vyfxsANaR~1PQy-W5!^Z^_yN#a?U9k3fKO*%Hm_Wye(o53fqJAY)Gs z@1L}7ag6qgKCn-ABlUXaRhBJR;Z}#t?H4@wW{zQ7LrcKl?ud*(2hit&7C)|f(l22T z;^Fg&8uYq+VxU0m6n{7f|0CQl@H3cXKMvmVcb8A-jIWLVjhs4ae^PBtRry)-ts4jB zZ8@sQ^%H}J$kzVwwic?aXO^4YOLJm7mf=3!5Otf$J(2Z$*_d}n|)W|+%+$5JgGVa#2#>~=(+`E zNi^wjsZv$y|9S^Us@F9<;lVS=A+^OF65|8kPC@#ea|(`&q9ifMJ@T&{qUplc3HOC&RYBK%CPYxAzVo zVlU}!42=lB-=I!k6B=FcZ(hpRP{Z|_sGzOollAbO9ko$WkLbV87lK?XhPUgvJ>0ny z&0e%z^p|L!gefK zN05`Ey?u6zu8Lbw41o5M7CH$0a&16Cu?`84GTgNd&LsZmut8>Y8~ zE&9X)WxmnOQ2Bp{%$UbN=p(h) zJMmc{19hn1N5x(c_vh_5f!RJghye$VYCyg;lRz7y?$z#J3hqh3Ui>iNXn@9iq?*kU7R zcn0IH`+auK3I9M_m=8<2kytK*|CcVPr56|P_5gPIpaQGNB7E+2&0S?12l62W_4s#5 zP^ie>jyyeH9~)=b^5WT!$YsfMvKgz1BPNRIN#l8=EX5KTm1o zkGbc!$p!ZBpVwn1^C~iWt+=nYPsg|+2RuQtN>)rU>0L5`B|t5T}#pC7LR z|L4597r&GIlY5Uc#N+j){ne6&zSgEWfQftPB_6$hM|!-GKjR6I3QtLchLm3$lz@G8 z6NP6=PPZIjrhx1UXYJ7N%ZEAv_1X!&GO?yAxc#hv;o+Tx#50V$cvH1=fS&#>k1wAX z_s(yK*Th<9My&Mwdh<=dVKO=(N*~^Y?n;M$tr5COm8*eHkUZR8$RXQB;1RfJS#Q1$ zH}9jCYDDDd0|k4Y!?IPfitYf(!pA|>-uT}oG(>_OP^(gpT&ykU{x8c-X=g1Ey43Q= zUJSdsmDUTse{*-5Ch1G=+!XhR^2n^PV?s97>FM&pUDeTt`TKMD>ZI}8h6|KccG(dq zi0{?bZ;N78`0bfOA!V>Kqc>OPLY-P&;942fvSvT)@W;?M;QMCZL&Jwyib;3Lu8uwVoBr$xx!^4T1}>o)XS4h$DX%nXI2|IKSdx&V zXLwZ7HCBi|I{I&ZX3ep;?8k8z_}3qX#(2bgQ^PK{izCv1=hxtRvj5m% zOrdoC0&ih2(U%tii{XnC{&dK}0n2mERLn0W8O2GtH+eaUsuB6?eX9LhS}!{+yo8;8 zd~$jr*4J2Wx^nCpEZUskMkdh{97;R%F`$K!BS;2p z{?#57C5>T?eY@4bXK}--ULx%g|sYWXYV* zMm*(z_Ic#r!1DmwAhbfo(JfSh1)XNt^LIHY!Llv`zKa9e-f2GxpfnX98%$lp+rE2 z)|9iL*!*dHpMafoj?Yx3Nd(aoY`}d(-SR*5pFk|h#@tM<_fz@l=KBw8&O{e+^u@0a zwGd8H1zuCI%W(;-0)}!1j>YMX!%OSBmICiRq8$(STY=%Z()q`s3}la6zHHXO9r8{? z3_tdC-c=gmfdCa*CXkTm$2Z}zvzi7EfI5gCa?{p#B|Z9qpO8KQ3r=q|(C^E#sFGC+ z$Q*KGWa9YblysZZ*|a_*yn99!3Sr9HWStUY^Dw z9A{rmm#-YcDdP7t9D-y3t5P~~sQiV(A;tfW1nZo9+?ix*xS#n_iKgF-y*oaMQf9_m z`|k}G^T^7tN9MKL`2^BwtqTEX3|Z!(P4DnUe4UqkzG`%+67AsTz)uQiK@~fd(Y@~T zWf+>MLVkke^E4^fk)OAi5m8C8lZ!l23AgK&NYg@lM)Q~b zrx?yy4kmCRG-K7VSJnpAf|@>0DI2;KG{id}*8-)>+QBOhQPk(Tdx4Am{wcy72eCk# zwczcR#hj^vedLkN zppXwA-iaaw$sayQCYu`YB%4TM75NTf!nGvT^PO-9@HDV?dwMwwro!jsc&)2-P-x<- z(Nox|8lGGFF)(~*t3_F9TAlqwg?B;_$?8*E8~uiVx?MEY?8O`md(}wi%h`E>oGA1P z#9rH=&uf(H?J;R{P;=iOBbWlqO5nfJRPG=gP8cQ3ES>P=j9I?U(WKOKvxmMAP?k+d*CM?Q@Y&eFRtYOgr{g>%&i9@u12g8ZP{1=n) zh6hb1-*lP8yNNw{e~PdRs~L0Y8MKSez~sxZFU4W+(0|U^$sgU5SHW$Ti0jKb@ErG) zmz6=7oY1tOo)t;4mKUt*z*bfLSNLBXh#sPi-J$ej;Zko%Zl`}Q|D~(`5K|P=mx8>6 z>U^4que)%C726$(?_Ao_&jOk{X43lv!}H#3aR%LVYAYPY?*%Mq<{yC7N$1NVk&hcO z`2&cNd%R^f5taCL0}py?cKQKF=PNXK4h#K+!E0sCgv5fs--A}ZXs@3ALWpj*K;ib3 zz(wT#IaDzm{tvOdDZBZO6T#8e78&<$GxDBnwjE*9#Mh05git2Gfr!(y{qe+22*>tN z#I#|X@sT3)(eosGd8MIeyuAep^KDimqNy?aD7Bhm_b)xVWQH&`GP->2^5%C{kaHXl zx_|EA=!ea7%{+ulB;Bd(?P})dy*^9LplH-r*tcQ0`6yT8C#w0(Sy3&UFUbDCM(BoF z=tf{GaQ2y$)U-TS2C3^p;GvAdbZ#xtgB_67eEGR>rJb*u1Qhi|k%ns%8i`#^H1N-c zPtkL)lrY2R3sDZbw`b0zKiOORqk)WM>N8hbCvDsa;v*(p!pGwbik0riG7eCqy_lza z)5VL!wGit0VOUq34?VLKv<%L$+8;|NIbS(+HlA9xFVp@x!_jtV{C=H&z^drMcr=&U zi)$U!u{wR$*-WCBLe@c!4$6o&yku`UvMUG^V0~+o<7pzPD#HbyAl!4-@HGNDo#0Nc zB*EFF5w1@F-ly9O= z6<14X4bIi$%Or$JEj&6rfr|RZ51mor2wH|WdNV(laz^HUmIHnDW<^SzfD$pMzR7|) zE6<@Y2xe=Mo$OBkVnu{;+Meediw6h5N#AgB9s)cHj5nNnPRcSWk)MC_jiL?D!5pM7 zw${V$#5ibZ`Xn6YCpGMLDmL-_Cj%P6CW?4ai^IpT90P@FllNJ+o*ut}{*fidQC)}$pqD$VAUdiG( z=AOq2v(srJ1boKp$&&$)Z5wcVqyjD=z{jT>=zlSd{Vw3H*6mZ;#AjfG7T*n5ELnOd zA(=d~RqQ)qX3+xjH9^rUZ(8OG#yUGuJ6ihlU6s)r3Q}1G`uRm}8SgwE!tM9^D>h0* z5XB+;c}w*0e4Wp^;);c&Nj?>^#9U+#pqTYtB)j=v^tl(z!n>x61nmDR%-lMZ5*xMQ zb`dJY;D@3qQJ+{cZH`~%S(5QoZxtzY0(RcfwOrRd1&-nB)^|ttn{p)ht_u`xRAr#b zkYpUk?*G|m{M<%Bru$K@YsRFBO})rd!>4@t%96EjQb6_z3KIKm8Wad6U5tMuaE9O* z#*43rlo;@&+RFMnX%h#~mWK#pOmt$q35}5v{x;!@lp5HsO5zmF+?OBAMuOa}EE(PN z;{$7c?2uP;=k}}b%!?lRb1Hn%PWrdNlbhW29`5G(lXFK!r=&5lfmCodgLge6MjBOC z?qn2wMOTdAP~PomBEN4$Bo$#Q&}~~KPx2KFr^ctG+)+~^nVAh_tJqzt>$YyN6ydmR z)}(sSBx!k@=*`MCQX?@jGC=7;l9@V;?iFZd_WShZ?eVMGslv&>)VMuJ zH_6)o7VXAHeJ9Ir4;NTRL+k=ETou_okwxD8d0cZ**F}8ku7=Fj@(b;-wX1mL^sv*K z3&JN;!VYT`RNs%Zd~DkD;M>}Bto%nXo-mv9AujIQ^$RMm?(sj3Ce+KXW5p!0r&&iV zH&L~Jzs-vs-tkHH{91>PSN%KsBmNC-ys~9-ZqcT8 zueT64AMUtNBxsKgjI*uKV!tM@dF;h1<2uq`3D%rgD}T4|pPu3xe7wECk`*0&^f2oq z7h`x|WWS)11Zn|~{pp>|wQ$e|o$n?8ZlayurSJdJhRF9(MdT*wC<%V7)yG}@bRabr z^_<(lsa8fv=={F}!@VyrT3)=qFNBIqHlv`sftJ`kw||BMTw0pMyGvo&8*X^$DEt=C z^G$q!XhaqBj^}3T+~&7T{F=%=n*vgg160EROb^$1#IE=Z^bzZ|=F;0JeeftySmF4T zl$_Fvi?iki4H>6@yLGfoy}IXXuiu#Xdwf8sbrK7g>yrKE$=cu@%;-hN|AJvdbR|o6 zxEF-(v$zq|&!q&Yf@_oBMSFYq3N{DMc_*VH?Xdsc*f%Z{icF*TZitNuN!$0z z)Y_@1$6H>^4Gv_JE?&a%1ltwTU|1 z4TeeYzh<;9#^K#afNhf9*TgldcC$0mZ>_l9M?vDp(47%Lr8c_h7Yd5T;Pydo3b5$y z?M$zzS1Yr1Z(k)ZeW7p3$$jg0L^Sh93-j!!xQ3Mfc%^_LpV^Cv+?&urKb)w)czYFZ zgROeZ-=i&R%8hTI$Y1;$Zq0?(;k0@NrXQo?jYVRA)SLzpj7gYxas~O{*5;F1_VO8aRr?`4D>8 zdO$Q?f+G_<`?KZVy&`#Omi*!k?*(JGqi*jwK0m+sT7tDCFWF7I2F$)XV2rq~G;-)m z)e&MBJY5Izy4(htl;-7v{@MB*opqPUS=FP*u;cXVp5slKKfXhVer|a`JAsssPW6H@ zXf$K1SGRoZ{A?xp-{LxX@%qb0_FI#&OK%5?+377To5|GWYiZO(ruetOA(jf$4~Z3! z@3+@|AvZBy~P<}jhr+|TH3c|jhCm$#u#3+=Z-$5PSo-}Z=4T=t5Xo#!9XC3aLR1JRypv{(G~0@;ypcY-)Yl_XlEVDAnh zIlzm8U`0ua=bpb<9lpe}Xwh#LpC#j>;$`G#hy#qiUvff3NswoO&xyx!B_@U-D&0M6 zBiw8247*1hG`{HVeOhF}b{KT~z>m|J)U82u9r5pG-=B9Q0+kIQ-gr_}h`cTJYf`O( zdJmkJFPxd5t;=#LJm%?km*jRTiQ;>tJFes)nD_#4J!T z(fIhA?ZZ4?^5VyfMF{0JqUDI%%yZaW>a1Q#T!4~>zh~Vn`bVx}X zZcGnNrKPLpw@r4%kju&*#@tz75eowhRaTC2?a4Fm0Vy4!*K>4(^M1|brX1^VpIWhr zWuVByVLE@cs>~zYBjvmU^!fvd+z%@D9YfqDUcc8EOX1g5XwL}sH-~^At05h2l-rgkTzRzr<`NK;M~@yEgW9rgf1%4Z{@T)dEoW><}=4Cl2(-sGRC?N-z|{!050CZ2I) z7CJU1Vsqu5vVP>>cQoY|v2%AtGDhSMQ!jpK;VDDN8A#s--H}cWWfnT5KO8^j{kp9W z9{j|#HxTZS<0yVUlRuk$u3TYhWV)RSgbnPt)h|XVhUkQ>-I()@ng|m4(svs$HL;Ol zNFcQcwC5`qaNYk1mkK3dQc_Z3{3H68&k#^5E%oMn{+E{yDM446bF*af!&6z_lzVks zcNQT-F{US~r(D1Q<`qMqz!_s{1)BN>3`zXUbknG`lE)afY$O6QWn^@lNf6l$-V2T>;%BaW`=r^ z68rNhnwZOozboQQJK!c`=M&dM!`pu|I{*#jZc6~y1KNsp1$dRW#-Hd5Rpy9tGX_3q z+QJ$(e$3rojx6MkRQt_VoDZBoAuFw;+yUnC*&dzC5yV~=Au_C zKLcHV`60)@y%JqPqR*j_K#Co*rjSZv@t(rJhV;m_5m>}A#8t}BkTD2{-QJ*y zqakr2bMh@N#?3)+`XIq^1XjLOSw|&-OvQ~B5=4=w@pA{vKX_ve z{J1=G-Slsu90EK!%yI6wvU~_`3hx{{%o0S%mWN`UE?MjQmnRxtflh;=^%6Aq4~8;u zD}ixktM2C~^+X&`yu$bw!-;|%?i(76m&?-NB9gGK`*Qxf@b;vi9d{@>W6;uLlp7pag; zPEl_?4CBEF?d?wI8SMT3r}CDy{oRmkzMyffIYk(jKpXx#M^B%stgL=p-DoX6!Sw2g zor5Tf2`I{0?$#9CeTpsS7N$y81=@&%5M(jf^?m#yG36Lz&TV5(mEA{a6mCj9BFL~5 z^2q<)ro}U|kN_n?+f3%CPpf^sFB7tASmG#yCtSWG>stcF=IPQhD_KRnS$oppfIP@( z#z9nNqB+q7%u9=U45|l6LjO|)XN7b#)5>tVDU5A~W0OSQJ;C)gbRxN+hD zlH~+;a_3{Im#W}f$4cZ(GCILWnTL;5*Fxha><=E8LSHHAfO*g09lIdudOKjKt zimghPE!`6eI4hhCLHspcr-#b$O>&kEe*9nf$|7~ z{|qB@ZlG|!=3Ds8kX@*``QhA|9)^SfWEub1jJ5Pq1Q!DGFu%-W`lGB9s911J5GcEj z#=&S~Zm%VQreEgK;_$6JKeD00jb0M#s}qUISh*t)DNrK)7sb-pjeR8y3a3KZx+EJq zZXM867!ERaBwIhACnFBQj_V+k=qJ3op#PL^))^tdnTb^3ik#{G{5K{R<%C2oK)^8^ zIZ|*QC(b*7525NbW>%Qv_&Z}H@K+Tw|c`xC+Ex1j!GT+bBpCdb7 zX}n%=<-Esyi1((vYL^M=)a4TuZnyIlDQB95u1VmH@~VyPr2f7nxkE6@qyI4HK8jRX zASoWG-!=#j=)pMbssCVX`3uAY%s(+b0P<@#L!(t5oWq=6B=g8j{~#-Dr{BE2!EC;# z-c4>*`n~UB{LGu#S;Q|8K~KC{=z`%T216BWNv_WZbc>NogPJ%l?{7@H*MpULrt?ml0I0Yks9 z=AP#7t~BQF4)KDB9W8rfPv2HJyVXZ;4|a;c1}?7r^_wo@OW=9G+Buur!W12x+?$x( zW|Tga3;fj$*!G8_Fg6*w|vf7Fz2Vbr~+2V6$(Z`ROkY5v4^Mz{SsYC&1Dl__+& z26@2a^djSkxA`X`l2TVhC3m7ygo$Z^$@W=f;yz(wA^DmpLtPEL%nHqB_QjA-$+Cm~ zy*>E1(qziILeG8WE*fp^ho+>y=DNXWeibsZ!W<@cP@fl};T`r_;hYzaHSj70|H+Li zwGrTdxgXN>2VrtB
      fsYv^)BozG0COL5{^IM*3wFD=_Z;V}9(S&m$413B0D_?Fy z*UN7#zYSYMJb3qo^OVMh?Bm5fEhD7 z7ah&b-*w(n{gr|0jJt+ZHiFAr}HKOhSXb&hMwNioG8%i%|QyQ}8 zZu2VQ&;C2Te0`q0{pM(kv+kk0Z*Ws7sqMI7q;6_qepokOOS*;-CFSZ4AOqNAwlXWPdoU>Sed6)I(0#i za2a~{5d4I-c|yyr;6^fa_q*Z}h)zinH^x|3X;2uR;?~lrI%NFq;xJn4`07IEFqyl1 zi_w`Vw;aDvxcEMIT~MR>Po=>Qy>r@Pi@d#4#8>Gn(i6#t$WSzyHfDE#6UBV=lygKf z{JO0dE$}qk8U9xZi=>J@13koG0Yn_4xAz%BMN?Exzoo>eSB%xRF5U(7S#k6b&mmW~ z(>^X78h?&j*>av`(RnRBI%HvZaU@TL5*GtOmN2P^N1y^s%scj&@{pTUq2ru zI%a|9`5H8`T*U6$v37uC^%TC(3fza7JyH&k%l}mZlUk)MgNqNQve|V-I|sQ;nK%9h zpCkAsYW^n%3np$ET~yGq%sdU+!mk^P*DO*N!(_B5;Bws=`r#vR1>|`JwAfWhU9|Hf ze%V+k4|N#fuiH8q&2WaD>wYWV_ww<4&G%w@^>*qRfn~$@t6yDTL?9xVrd5@8*soD= zEOV&yOr>k1-lON9wEM_UO=zsqV_FjM58eNXdRGuRAALCgx|6%<<6(#kxCisVtKFmi z@qF{NFLuo(EVyA_pxky){clC zdr$nkBR#*5Kr}V#kK2>3vehQty&m&J)Mo;c*ni+r8sYW`O}} z3$l0OuirgN)E5lRh{ryM#_q9Zrf#rVZ=XDs-?y<-`w%RuH5f`CUNLN3{kpp%t~8!LELGi#(UQb*8OOJTyyI>=NjLF<+mFPRs z{BTj^hP5D4=v)Yk)xxF+pQvjI5G-(ek>A8OB5&-Ec5xy(O`Xj#HF^6sf;V3Z^i~i{ zlfH$uJf`dNuHf3btUMmTckQhDtT`lW^TFwrxxw_e;m3KRiLCRsp!NZJsNdz9S9$6B zVe5gmZkpS?A2|c~Q@fx2vVK9uQ+nm()93G0C7uxbC#{NI>I*i~ z!1D|V5?9dsNJV~otlFIqrI*n`T~oIaNp5_Jk(%$XLBWyuR~z}`C|iC`16qVctDB9K zm8ZXP9MI;(U(9-gwUnV6 zkK#s?26>NS9myzT4nTogxh%i-3#Fu^_NSkd(~Ck9=p$2i1c^kis~ag&v9ie$bgzmCFGO?H6+O6#@-uwB!Gp>oSvL9Mz<;&M|f^a?Ao^w~^`loqa$GEA8=_*UqQ z3VZsczXYZT?-0Hf1N|RI*W%CA|Nqa~eT;30xz8o3T;`JdWiCazr9MSe<{BlI6d}&s z%e9nCly%ijsfcpfP^g4bE~ShVD$Ff&+kWT!`v>;eIgj&xzh2MF>-F68{=M(@x4YzQ zY|BG5Zq*#tN_yPzukDVPYU7fl&-X6(;dxAl+*;J*+sU8Al0>b;x!2@B+8_V);D$12 z42Vkco7w8@@0gMUmnMf@Kdsr%VOv%8TRupprQVQKTQ8GEJsdb>9!1P|VdS~$ER^M( zur&bGg&U(euK=$DAc%;Kl$p{WjX$uzAkZm!&Vc>+Ai(hJ$<;y!7g)l_^hYAv- z1R;GisMZREvobh_X)Wg{%1VL86>A0Z%mJ%TW!fR9@qb+@a6cut{Krvt&zibk>M=~ zW`-$hNj{W@)i&aebILp^Ca8CG?lgrqN#z3|}3mEi?luy-vqG2pZUf+dWhf ziv0)DxX1cLofG4D_SY0?a7+xOrUD3IPVNHnMMJz|bg!Jd5OYJO=X&`MDCb6MtQZbg zY!JaM(7c)|GZpIuBA&h;$qhN>R2|BHC@TYwmk@jm1I)z2NJDNbExn@g%O zplqn}irBNpQ}uEFcjsOKAkZw`$p87mSlhO;sEKj&$6}5tod5jsO@~w2qj@LZwZ|NF z(O!%;-n&n>|Bp7+9%;n=jw42&~Y|VV4>X@<~m45RmW$&8gXfVs1MMH7n=8y9;QD6sKYq^5hw1LW#PV(WKZnn~&_S z>ZdXu)98^{D<*qmXWLvLSMPH2Yg})TWXh+LdWmNpk&oE>%xo^aXq5odz!k>{YwI$c zvkd77BymBWadp<-MUUNTYsdX%RSbea?JpM)^cyY~Cfu%(e3ggIvsD*Zp7rq^9!GL{ z9%q-V2|!W04C1Q(z;vsESmsea*VR?xfQ(*UT_~H$;DS`4d}m8{u=4ef^9m#f`VXf* zMm|#Is*R7Q?KbqDjziazVoE^6dEdovkN9|6N%T z4sT)TA{2Q>cz*j|VmQf?yH?o&xE2>uIi~TCX0gUio3cB0x);?(#MjH zget^VBOaY1;Ts6uHT&$%#}Xg(KYESh9eoNa{3JdHzQ4o@Of9xdY-%gAxtyfY%lTgB z4#dYJ5uvY1v&aDPAs1?<5f_QQc!|-%)$6l+DTCC-KHNlo7$eg|;|3B~?&x3658t*L zB5ujEPqyUaX$gAE_9Q zSPlE8KRP^|KCx!*NteW&0<_1v#cmm8C!!KBW!p-7rmj1aI6th-8pH@(Q~LOapERv3 z+%B-)AT6It8XBBH96>FXrxmlS#34h9>!PSF&p#+L~`?PHKPC6N1a!;%wxiwp#j*D>481h1+Jt zoyEDnwtf{Wtv^{hXRM^hn~chbi*`^9=HnHBTuacKr|+G0D(i_+q1G}(hnS4tC}=Of zsLZF?@7$|xq=8nF!(`gypkXB`yNQ&o?vN}3(7w@rAKCP7L0jR`$EP!wnSk~RR*YJ% zE>tGREJa}}5_H?^{gNA4RjT_J#GP2~gl5^gm!5Wf2+i#{ck5B&S(-%K_wA#HNZml* z{!q;=-_hn;6&Fs|$y4d~`S$=-MOM^7KwFD>zKyq8+D^PZr+45wp=TV*s5^B=2!ATWvdxTlUwWtDS*L(<{Z}37aO)$A@6dZ%fM>OyjZpfSpII^=q3AA%z+n!K9MgxIV{?3Foxn?57pcCxCqiE z|6TN91yGk>86iE77xRc@5}3yC-cDFJ_ns6I_2b7|;0dTDD3!)pohIyL(`*s+IKk_Q zm>ksT)?|n{dF=8CahA9t|FOKNusv#Ab<-116-|K%Dt#b6D>!&*qc)H#>fkTMN4kl?J0;L;}k8WCG1|0N?t)pd8vw3=f zDp$rF=?@TFk;eP!wy0Fu;yaLU2~sa%nV7$!}GV|dNTWmA)HNyNe7ddlyQ%f5wZ~O; z>MQ+XCJ&9|8PM8E#XmeynW)&*?=Mh$jUiPHc6DIFJOqHO6EUsk%k4m#kqT&K+~C| zffq%y-Ri1qho!_4mp;U#?I!a*rtPo!BL>JZ+|*Cql89iVTzNJHbv&qvX*;b`Sv$Zj z4~CB1MekoIz1he}=Z;)xj*lU~!--%zb#CgLh*6)0N!uCHy1l1&%3@eM!uH-B9+b5kp;Mmn>JF+7-1@8+UMln zxMhIg6}@80?#H*22akw_uW!u<>=L z0Iy&^k?e;NY#Mk=V3tv+>9ge@^t{#hojS?9H?5m?;1RYj9#Uf6EE#F^(L~MESiw!) z!*?kuH#6bQ8LJJex4Yl#$6u}pne_i)Vac6nj||{>5*ChJyg%!GFi=AgVWc1~{pI>+ z@o%?rnMytGz_Jaq419TrGt;PuM0ZF4_;iR)2k8zD``(cn_il#?!H z4J!Jm#_-XXUu&FOxk+R#Usz9DONx-t+@GB|yuq=3CJAQ(wNG03I63`?@)Z1ew(7qt zEnwpgsci*!3l;XzS)4*i43~k4P48p9=yL7Wv$OBhd9~d`;(L9}@|R8hdRvhPe`fHz za{#q9-uY40IgSU+zJ8}paNa42p8ieo%LmlMiMW-`7^!ii@~1;0DO`XmJOH}a(@&qBH2$6zy!WU4@3m1@WHQ~~c3L$Ja2mSVAWA5+!LRIB}=Q)9;h`mj(%1XBH;w zgRpErRske4MRril&h1>$K}0!aziv+sw2>EQU68CZXah$4g5OYiIw$FD*hz|FS>9<% zR}u96e3yucJ+R`}k9_=eWi?-#lnEvSg#IbIh;rnQ3nS^8UA`h)|2j0+_aM6m_P3NF zLQ+g9c!owyb2fy4ay&P=a^w-GPtB@0$I9a68I5H-+V_##MMM5(Mu=N99sT4`1x6}3 zcj%TyKeGP~iO;wwr%=n9pGX}{w`L9se~li$JufcqcL!Gfitg_gecQ@kI#{pz1=&s| zX^^}{Yy!PeqS`g1=Y%tcIN6*n_YRlzE=sxJ1j+in(Ir`3YDYHCj+~Tnup6)GiG>{D z)6R4Tj^KqMBgaq26_Q}|quhZwL(&!&)juejIeZduR_}1w?g()_LV*GFjz7VK#&OP@ zrx$H^{!@QiH1$62A9na@X2OBB9UK>u^IA9hR@7vSKI+}&TNL!)`;!4{ff~_XUVFV_ z_Hq%IfwspjQ~%(x5!Obs&kW+3gu-bS(Etp85U9$aA;x|%ZRC7*va?><8&Q7-9M?^` zqOa>CNcf@8kSxxHWhV1?%5&cq-VS>%s6I}LURmhvI^xf0P}IWmE{1SP@K$#HClYsp z2kZRf@Uxl2n|scvr8ax?7Af}Ye8H$3%GjA5n&H?Uqh1d_sk>(wZwk}|oU zwe%)l_FzIE_Fzw;oN=Mi3?FRzPAXS*Ea39xqRWM{8*~$*wrpgDH5xnDk^kBLn>(QK z#zt^Xkr{so!!siS8Gp|>`-kSpoXPf!K)ZIb3_K|e^{qa3`VPU7*XQ7kGoDyh5cu5-Pl08SzRgy6uncIaVYzZQp-9 z-#YFY>J(i_0pruwJ`C$tR3_EjrG7TggK|vdnjP3qT)l6YW;Zh~N2v8DRZXKBVEaQG zrnN5Ci;q*N&3<`#scm|K9<>ti%XX3xN)zu$;aPqSBvh50;w7laGd5FsEiJ0OjMVDG zgCw=oB0Eizj9fEv&)n9Bw48k*NDe;othdhg40`k3g7Urq7 znxXXc?QI-zM_Y-#pnt0i^Q^L+Y)k%L)*!lrK>EsXq>n(Ctv6MXo3>IW4$?Z;R1|S-$_3hN`(^D^okL? zl+`3N4^?U2P)9nVQfuns4r5K+LzNNJ&$GB5fzTtY7A7`Rn|%nqUu@!W;1@NXNxj5J zSD4la@w9t^6MI4uPsPx@5uwd_6e@Rk=wUI! z!`(~i>%Mcw{OLn1x*7zrYHzE`3RdIbti_GX!DkK-m9kde@i&=oJ2(i|a7oj6;m=Ic zeYrbDQ_xsi*Jd#KII}_g5&jOe%^kf%>F;U9W`)ds@7#-T*3_UwAa`_#7$q1oMrxuS zKA4xP3WjbaEByy#KWz2Hmch}vowR=aMq$<^)SX2sKF3RS=4?6t@!lUI8H8oQfL|3# z4$fu)%^cNmfJKysL?=wM2B8lYYEdF&mpAI~g^~tjG}J?)GashTye0a%p-v6YIuL~$7(SKiO&$9*pWGJZ)H^}G z+uYnqRaZ?=eAS(@twUm!!b|mD-f0rw;#F*Wfc zqT2xPsF(R))jy^KGQ8tkql-4KI3|A4Bw+Z^Xj*EL-w)1j_NeOfCe7ha=YP%F!BBFF zdG2xcY`GzfI_<%oQacEph8PSXpGLDmfAZ$)xq$U|k<&B()SVsCHVXcby*Qv&g6c#F>3TB&x@Uzr3EF@Tb;~=ol(IV}jIwGL6H`XkqCd5V_r;~c}fQof3{$Jz3Ycx8h+59Qm zJ?9Hpi^5HrU003T8^!?<*pTQG=6_ZW4~u?btkw)>-aXy& z7n5qSOd!)-)6acjVBlYIAz-x8-r`s*OdB zG2J7opZon1KD>OU!ix+$@MSpo-_MDE8PiD}9r|BTM}kS2gq}b~ZmPdB)^P1-fF`X3 z)@4YgJUs>QwC9%ln3hj_DXwSLRNB^8d)ZskW&U-!Y%q*H-2lQC+D<}KVit5ZY1p>nUzu5|gb zgOMo5H`7}D>!bxxv*l;oqvoN;uQ9-imR4KabK0*Q&*Os#eI0?0ckHxezFh>o*zXJy zsr=5!Xb59^#cUs%)7EsjK0w`*ww|5zO(FfZ5q&!+Y`aZ6h(?a&9c+wSHP-Y`QV)csABw(?tk+p;MI;@NO~!tjb)Zn( z!3RGGRj(7z|KvUst(dMUO^cq`4MN zS&kmZKHHUm`_p#NySAf+*vEU~cAc+nq4GsLlYripGqaJ>WO^H?%l(^ZDj{9AjFBvy zer)^cqa-el-JOpn#r#L6W3V%o-(N&aW>JO*QUk74KGBtwzt{EcKx4c160`m>r=54P5TfhZbiY*eB^WG7d`>XTA(Db{v&FND{nJlki%>KRm7K=F4 z*Q|ygeJL|aHXqf2x`&5WvZd&ol7kk#cgpG)RlA(#EO6bi|FoTFs#S0Nj&a538qxm; z2+ot!gLqoOP|Y{tmNE~=92#B1aUVw8h^vy*ibYJo=;xNCw*uoP0^2O0WPa8m-|i*phC-YWso{t+IR{qUlV^oL{2DR%Y{-gZ}cbwlu%RUI&G~rXbHOPt*kvm6YY1 z8!l%Qxo;kf_sN+}!R_Q=2o2HW(~UaXE!e23Df6qV7jk<3p7j>=(h`Cod+;{$^qC%G zRM1-K>4v;XDpZcNE5;*GD#qKjUx!y zv4nbdc^Bf@sN4dONuU+|P4&c>O=!b-?mM~=Q+Lm=m6R;c*nSZ09t2%>1zy9Zb*95d z!tNP_5t6Vy|2|cUS=yqyn_dPKUM?)UykQ1u)LAQ+c;RSs*m?qz>$X&0g}m~0*!@T9 znSbhK*I9Rg0uatl0)W+{z&f;iRfDehL&fVtM1c4b$G45LWQDr%~)rxErRyjMeyziE{3=01!?20IMUxIw7@FrgFWU$MW`s2OSt&j{Jb|CJ3V zlM$Kq+$kn-m%uD~%Z-y!jv}RkeV=R6%yE{Jf2iHSStVCZnVJ`ad$~`o<~UY-&eH(z zbH0MPge~Zb#d#dp`?2k`3?PFMl)Ae)=!T+QFWGs6TEJf?*@17yS6;+peGX$!+PP6= zviG(cjHDRLruJ{9Ca0K5T?+FHOcm8E3~nINlt*QqZ8sT~om&AaUW{D5deXROr}{PF z&P~}?J#3!*!SJ?UF(q?~npGiMQMIqOd(CIbGYVHmD99i|f$~T`_)A9^CZ|-NyK@Iu z;Y2f!K-x{xOA#2u=fQOVW&S)Rw`Kbg$O18R+lKBB9oNEP_%p}PB^wxVZyh4-OzWyw z@lNI4_-LB-E3aY?)5lf=p5Hg#PAxl)z@Zq8sx)rbk+A0B6G*F6HJl@`T3@_+)ncQ4 zyR3g4B6ap~tj^o@u{=_oIwDywnKBS6y>GSiH+Fm~CRB=Hn{i$8gSA;N&>be3-D+Eu zN1|~zni5b1O_HNb+3U4^~9$MaGMFcBQ-pG;AmrShZ&`X2dNW z31MecK$_p-Jq?i~d0|Aj3?ZRNHtgkMKCVxB14261y{y`hzLJCALECL=7jj^I zAI9uh8BIlat8l!q@QFs->le2-UK760U-`Nz>&=t-t5HvTQ_7D(WWll8K5m5}H+wg? zu#Q~ypzTr16WNPqNGpXBP$f`I5Wb;Kf)kAiMd#OEmH5Rb+uhj0f1VFslq}P~;33eEukLoiC9L&i zc}AYAAu5PY9P|p}_eV^WJJ=`yZS1_F5$9DVp=X6*FUR1aUMYl|_ZVsm#>JQYyri9O zNP!P)O+x>mhAPj{E@E|EsP5?L=A%pv`q89W^_M%72g7bwo&J5Ou8$@=Yzb~8-6530 zb{DqH!957y=5~J#&8cL#Jp2}B7z1m^QKBIg=kdM!NE_^J)`S_4kJYmxKYz+gXpF=A z43{IKx;-ayKm{}0U+q>wf4>uUP%x26XNb%yn{_2hj!}EHh^&uQ*-MWBF+-6CZtRIF zf`QHAz=iYd_jN)I<#_{UU9fcX>f>qbG5h;3Z=P(E(7o)BxvR@(!OK5cJt*RR!e1cY z#119Y#J8bt^u-}UBh*jq!x+)WAnOR}0Vlh!REk=XSfbES>oj-Yi4B_FB~WPkHLR5P z;lq29ckvg~x<6`}RhCz%Ses8#c;ESFPE!fI;f#eT%PRU3@l;-J?vSrF@vE}v$4F$S z8ufll%!!kAFUij~kOcV~K0B{&eSL=D0^yK9I8WT&6KP@(;MaaBkw3KvIPy<8 z3kNU*5yj<#!vh3ePBxXHxAjkRpM1w@?!g3Qvmbnq5D1I$4Z?Z1@?sp}`?6f8^>e>C z_F;{eT`bOC#!i7<+BF}Z{(k@(leh{4FeU?Dbk9rr);Y{2mLq3?$<%-za_)rYASar&f98l{j$}9`c^+aQtBDj zl?={zmOwRePv1_$fP>;!vm#oR0lF_mAtwpPmdK5p7%E+D`^ zZ&|LFT=#Fu;csd)V&IVN4Succ)J*YkVam%hxyBv1RUc5n<0aX*N^ntp^(TS>u zcYdidhmEL>0y0k;2^p-#V+3d5q=$BQ*KhIKe`hv{D|b7A2RyW_H8sprxS(3(9o3P% zJ?t=v?UBvz!^9KXJb;b(5DRwVvp2N6IJ1h$Bt+8nr30omvL(Z3?o_03@2+fZ!Oe62 zp-|NX+7D9IZ370>AHRQAKO~IC z;M|y#YbOS9%R%ZJ;lIh36nrSCFyfA%uuEE6=ZL2M_pq5H1% z=_XG1>#N-@-d?#rV?2x~>2>2BIFOokpZn|_0+%Fyo)=jn@I!&E^6ceHnuvC)!AEdj zE7MObw-flEpb*T{P(xO4Va6n2D2CijF(rTKBjO#Qr+L_25u;0rt=e)25$O>C1#d~F zqID9VMGsqPtI3((QdA3T^cKVixBl1^B^Dd$h#st8GWQzEg!MxgMb(B8gC{>rTsXGM zh;{Nu z&u}+6+|{RdjW}RPG7SAyq@PXU2~}Uz89|s>Y8lvvGU94ogQ-_|XmXk3z zhnqx89{Q}A?!R@FWMI!9%N@8ss0$~FMoe~2d!pI8;ztSI?a7Uf^+pROaxZZ9ULPvX zO6~Vogr$8a8tRy(5rrgWzUs++7#Y*mMJ)Ll_t21KLU-W+OUo2Ux6&Kp~?d^ z9ArYc({d@r){dUu)M^j+zyZi14V*S*VxIqA3b#=$6WullpRp~7U0O{vZsmNH)WQ8- zZp@tqVa??gZcHD23ZRB>%LUT#=_bV$@&0V-rW55R-#c?>zD!UrUatyn5lV`+q&Vdg z)Ub{OB`MLo)ZQkeY!*Lkr>xzCx|Aeh{6F*wKQ)pbR-r?w5~`b3{V_~1#x1O@nx>cXeJa))We&+y?fA&9#0_bp^5Kproc82das8B=8J*LrC!T&B z+H)9l?UGym)%{5Gj(F-mK^ku0?E&0%lIxC%%3Sal3sG1k4jbKN9ZYztrgv;FhBfX6 z)+}XNNyl)!lZIU4c5r!OwVfC-O4keZ?7s_|JlnpMg#Jvb+cWr?m@^|VlN1Gs9H2<| zwWO0mO^b5i#HYyTJ%rt^v~#HZ!C`_q8>TS@A*Jy9SL%`CY9l+x?2W2vTx1JyM%kOf zE(QOYP6GMzY`tXHG}Ladk<+16*WjO$!+k#5TlnDZd|-Ex^}{~ZC8>OTuL2zbR0ed@;HQ9=6^sQUc9=y;O&*>a>T>w z4O>5>a1BwBs6D%3qS9+I#Wd;2+3~Gflxb)i`PAtyZ~qW+bFfHLq#p!jnTbj_-(sP< z0}O@1-v~5-dr(hSym&SYriB+YJ}pl(|A0nZDuHR@ zk1`wIzzgmN3fBC6cqDM2$a~?ubCg`TW-BN3p9q^3`@TVp0S~+lkdGDR}kl z2&6~AY$wIYz+-XSxFGTs$j(9-%P=hsxKwEpp1Q5M1$Za0(wehXTcp~#g zVt!EA$60Q~zd1drNqf%`_}*+V14b!dQ|IC6FECE%w9^}_i+p$^SHOAjNW4=3MCku6t4T7b;YOGI+~#VlIN7wx*WJb*XI0!KjZHu|vZZ`QI@M?eoo z?hZ8AqRZ@>V*Mv~1?*G8n~qo^gA}v7kyaP(B4%<+7*dEXmnChda!#Vhx%oRuut5FPXeA8f zNkdn9WO{2(0GJ($rm|y6i1BT9gp!sAt}YXz(nF&|$VXwb!uMUj2|Y{P=gV%nX$O3A zXE`O?>9)myo}`&7JIAi4J8OD<^Mbw!a)E^_IbujJMgKSgrGtIx2XG*A>?XIq2Aylb zJw#UI-3o?;N+0dyk^AovLoAKx1sOdJZjX}9R2d~oP6)m)R8be}mpFWi4L!bXTNu`O zwbora zm*Y`ysCcDHYYRs$ju9bVCxs@ji4WRu$r~KJyR1*fY(st-8CGw?lpz|N|EO*`J+bP6 z=2ACoc+$PCxnxHww-ZFsGg}oNnloQ1V{#4Y06ifV6)MX*W(_{1$Ra{bN46*{HnsM6AUauBIDmE;s6KFS*I%TO)A-F|&hR9R5SeN~ZT$-OJ z)04UXs?d@Jv(oZZ9y~_|cRS&8@0cwjfuV&OLkc0ZQv2V`MW^g*iVUQ$W;aA2a9DBYL}38WSPW zUv9_i#f2&?7)V;^xH2I8Ry7;K?oCh@Mkrf@{56y^{;$#MCUXDUJAqE|ZwgW=+#fxb zSpuGZO`)1Ld-y|#k^fL|bL)>Vw8tM_M&i7U&${;my4aJX86^j8! zp9ugDC!W4S-poiIi8PzHpVk)8bg7&S9!OF78g+8?W8*z(r&BXIAQLV;z-1sxtEEih z)vl*@PJ;4dNms>M2uv!o$qKu+LCUUWA4%?{VI?Ko=jn6NvJSKEOW z93Q2rn$Uiv+FGv+mxY5ftd*IeV6_QF5x3VF9RU+$7bdLpnV8Tx z9_$xt-A9tL`>o}$!P}8{pMmkd%t$$-g)GAYI-3W55N{w+KAip@r>4tKAzB^)&($^` zM&HTffHkRA{8pz4p`y`hn0}6QNOB&zK!e}aIC4I08+h%>@Ail1xfiS`Kg6GK%VReQG=;Rd zx+o_a;_s~KhkIIzT8C7s^O7ZTY~N%Dq1V0*pXoy`d z7VV>dgC^+$R2f|qqon?DQ5DcYpxhQ^2+eMdNT z_TYDL%|OSoo6lnm;)rijhVxL|e*+}ipE$-a$`=SXX9C>EKQk5l5r~c~=6C#|(lKQ8 zG3q1qcYizssd#-rts9Wps_Id0p=jAHUo{Zr8p=9J-pYQM%yZM6hOCiFpPo$|0>OoT`jvw+u4nBq zvDt@%5JTi#K+#MbBg-O-w7zz3&xgP``2jLr8s#9UU0!+B)y}nHN|522t?S9!03}Wz^vvow+y7MDbd}4Rz<_53=?moDrG3 z?FvwWz~gZA`SVQ!jHjLwnkC?GPu`VBi5sDRDrMT#585avAP1)u2-On=+ni!oV#sk$)P#kXS8 zaAR?ZS+79pCu0wu=;|Y7EY?#k1!~9 zx|tn7ebB1=d~2k=g|8vyoC-!!vJuoN#7UzDV&to{WkM!g2Z5e!z+)5IVKqEqNOYFl zZ`;<=xAc>mVvG)&!ve9A^}KnaQ;ci5fiLx(Oi%fC*pbg_3a&>!?U;(>0!edvG}Rqq z2~Y$sbbhx3X28(dzvn3UcZpEt=fgOH$i|NTQQ#X<*dhMs&^zRLw*sMROp7P9?@G!B zM>`}oK@2IgmIY9MAq%J9>919I^|}^U_cfTx`_(gk`S^)G{r_yz@vR z{wZT3M5%vs8A=L|LRtL=`I?RPsO)t63!2PSn#ja*Z z6vAvGg&~y}u!3haeqmgc=S{<-B8Q?S^%}&KwzY(cXCJsr7G_K=75YP?NF8dj8IW*t zc&*8>%E3>t{XVLq=GWp%yv4fsX6iW=!TQ4CeGZVwE!!>p%sm$pU&#hY?m?xfRaFFh zQ3O0>Ke2AAGlQ_NJitZSvbWqcAc8k&hnq_4=??Y}rmP>FROsMQm5{g3$FiUN*02e6 zY!AgZ-OeS8+s$ z<`eEo0*f}zXUq@cZ5FdGA@9_rFSaQ ztWg*k8yiRrJZ#?9>jwL7Ya_mKI1R-Lhv{@~vo?@SiaK%fd{S8DW<*PWdb@|F)RppK zEjn8d8oE>vsjrWCDO+q#ZDL9m51xoxk>ApBFdNj{uG5ocg3bDsgr-J(nCGxsc;ggC zyc(w1U*53=kN{Mu3mR*dh7+@o-_f|_TI>xA8mMYS=Owjn8d4a7J+l7)GnUk08=UlS z)YJZJ1^(cK2dULqx)D$Pe1Z%flVhk_0Q)V8?ezFS*wC{-0!C7>IL2HW6du(ODeihe! z6|4(>A~Wjq93)D4Wxdb?H}OsalaGTFC9?sOZ8-u>otO>J;Lq{fCICE+w0bD$zlqBf zW9HH532qjc&?^ilKts1Gjo!?)Do>jykh+qoz*`&*4?OncTtYB>j&&hsZ2i|PD^GIG zFsDb?U4A76>YEDTp${j1Ol~?#r)i*k!|_EQJ741To;g+O31Gp#!XUdzS$693~NVq2Qo6gZM!wX*Q_FO4>j*x zc%zN{WS!rK!h~A)2-Ods>;OEG@MtF`HKMleuC^Go@#pLLDsq`H;Z98-T+RxCw#&T6 znbEH~tAT;P1T%4LXaa5JbV}HDJL6`(`!(aWnWKMc&c?*g$61}7G7KmRQwJg2?rI#s z*d1Dvh`e5^WhS=~?sr3^phqQlJxS4fm+7^zaQ@aKqv^MORgvdpcU&m$S;+M4sjLWldwx8T2bMG};@Cx3S$ zq)PCX{DIhANyV^-SYTGsLu8Sj8JKx$>*^!;cl=h9730@=C+ zI4ITqQXKeS3yJ^no?3;fo)Kug_kHIwVHlDMPG@*O^!Q(c=ObqUPMWNQy-9mw z;e2E~G9TZR2M@>LEvfn%Kpaj6WtF5t=Kt%sUf`v?XnEi+9?mrP$(7<#M7?SZLu98U zMewZ9cK*e;XWY~Wu*G1P$ah@_Gw?Ymj&XtBAnwB_YUac0-xWN2Tl3#1n^>DRZ6l=P z;I%Qa;^E!bv8XEZHelT4-F_cmJSa_}-fEGt(_^g6m-H$0xA`^2eDUOZ>&WJT&ESvl z?m+!oIc7lTDeicexg-vV9sl>~df=wp>x~L-+jG9Rmu00fbTJw9s;AM&#vW472M%+f zKO)wD0Qh5_f6x`(wS7A&0BXQTX@(}%4YhG5iw0=rOA5I9>rc?%{Sb$_$G#vcDw#gV(xH#Y zudc-a#x`zl;?8%vdil4)Q_a92^5i)m2Ayk3j3laBoZe=Eil$OVhxcTII|j<>JTBb| zxa^deD=hirl&0VpFKx{N6$D0b`7P`5l(=>8dkuG5^QAvTgB=5PmWD{jIj zL8;5SM%>{xGv4Y|4?1`)e{uN9lXFG!a<2@y_-;TRm>GHzk1*$bE|xTulSG`wDiMI3 zwLcn+ggHBt&EwruD+&|yUi1LvDl8{k+##nEAjV?MLiGYZZf69r7T2<)>y`5n|8pZu z8IWXIikf2YVu3%#gdSH=0vY~Uv)B<+?1UMBufgSn$)+Fed7Kiz18=&=9aLTM1n>gb zRe>G-=rbSpqj~8Ivi`Q&4!daXnyd`zAgg!GmY{g{+-#&y3s55T7k9-CJj)t~yb5mT z4>Bdp22wQs9Z`ZpFcgbxmlw?!HE64^of+{9N(W66iLYi_PRJIfUYGPi49Hvs`brBX z-U2aRp|T77h-1iT7jCC0E>>b79ISP-ScAt$rO|;$Ab)9?w_pC4AQa3-Hkk{5t?jb$ z+rNKLr%cszHoXREso&h;!fY}|* z9vc0Xts^&HBE$V!*{Kw+HZ~g7bF&-;N-excVd+)KJDZLT`Ze!WRr^xYKAt30x%e*G zK&5!cSKt9=6z*KRP~J1a3`skF7L@>P!imyPksYy+^(zryR%wBgKy>e?hf35{K6<3r znUeb+sP)E`JX^IH~}s zid42UIHdl2KKyzH>&%?sj`^Cwli<#R{I@(lk8V!h*IOogD{=&ckv($s{c;GeQDdHi4 z^o;ZpDa_ECm_X%YpiZDh^m!Y@6b!-*^)hi3-nHk?9ik*3J$gh%j>+>ZS;Q;Pb{_yj zHWcRum{meN8YUrH9=j+5g|eU!)YZ{FJXp)z+L(K(Pob7uIz6*^IF!hmDYtf&7eKbp+WS=-$#4|#I&WnM;=Ozrq@qs_cMB(xi;gI_!GiI@UK)M)sliaSM} z`d`FL7zr|4{8HzYnOS8hE!^KZ6Yg}Tc>YNDH5BHrWwRah7dfV+qYut}*+{6ev*e{a z#7Z2}g+P&m2Fx8+$5UZ4l5FMQdf`;s z!Y1R&8Z_5brJ;QQYwui3dm1~WtIs}AH??~l6=NwQsmNhOQ-z}GU=kcqjxHucmeVcQYt>w^}ZP=Ck~Q36IR z5||H=+(_A9?@&NiR;Xp2F=wub^Ki_cfO9X-nam6IIp$Mp`sPfuHOWa;UU~%A@otE( z)eW2?PhYwU?hz*d0w(Ggq>k`&bA5fjguM;P4B-Yaf~GUBag~}HM32|+JumwA>SXaz z*pfmSM1@G8_Awf;o5CQz$$1U#hXDoWMikhhVFzvM0UI?s5Soh7c|O2v^f$&5FH}6j zum$uz`#QJz1cB{`Vtq7QvvLPF%51ak1_bYIn6K#VE-?lpTFzkxH17T5zDq@yWDbe8 zpyBWR<#ZD9?@VolWu*ea^X}D(rNmfKaGq`EGSKd-(wAz;J$WN}$j*|{eT^ivcd0f2 z;p-I;SL63t=4-4e(#~K9PmiX(U`^b5+(;8{)NMW>PR#>O+KA_WWMheSR0=E)Pm96HH9ibVYhTohV{C+{W;)8o@OAQd|ms(y`lxErF&E)_5fEG01e(dtzXA(Xj5 z6CAI7O0WmK3)_uT&tRc;GVpGf!G@Q&swta!R^e~9rEDMtWX zMo(|-62-qHGFu{&aY&S8?BnsVea~ACO28y26GeH`EVhUL?9re1zkiiFw=^T1|AkQo zR2;c9(L0}Yhc*2dJt!PWTaI-ZM67|robzHj0xx)P;06l%>y5JFZhce1BPs$?uaPoir_W=U^0Zx+b0=$*U0xpUN zzaTBweyG%)oXH@z^^pbWQYu=W*P1bCz3H*mtnFlqn`E{!yjyzl?N-Gcdj_UhIrjXg z(z8B7yCAH$^z4OI`L!Qp>Rx(_Z7y5@Gj*$Ag%GGXx)Ja9zl~z~wgluDHIxmd18qKx zh@@GSACpDliI$^YXiZUv)<$1|_xHqgOnotlO$a2ZbRn>p* zKx|0*v$hy1+PDt79m}bJh{oi$ZOxg`eW>A+f#3-|S_@v0huiY0&g%u&5N#&*#$p^A zGSzdK9^V0g)0olu#v@E_D8khQw)c!eZ;GwWtTK!<&k3Azaflm2I|97Wa<xNOQd42d49oJ{lA>vQUoXI)BFOGsR&9Vg%8|8f8$IE+AT*pGSIXWhr9m&qd9oDWA9J#dM_AIwYeVC@!|_{{!lZH z+0q5fP{d;E=XyRnYqpd<9J$$k8j6Yaw%2h0D{!`LDt`Il4lBU!oSNKZ`c zt03?OLS3dOZKU7*^A}&L0@`g_E|2Ap%eKX2*g-&2K?iV_P#6rk1J8}PTP#u0DqO4g z!X5;YG|(w4Oov{iTHz6N^5lL76ya3jJ)M{h1 z%Tkc|x(hx$*$u2k^coGfY~C->E)=YdX#%MW6wlucpNI(cEW-EruP~(!I)> zmD^r(yb_luY@=VAq0Xn9pIte32Y954S}+N90KO4+`|vECL8B)s-o3MdcHT%hh645G zf^>QNJe`C@X*T9J5cO)r0AQTITxQi5dU9g*5o*Rv# zLfbnD!D6`mT#iSQW3P`dFvJw$HPzR8CwnD@7M7~Elu~c6b^Y7hyiu+}uz`n{c5lOm zv7T+EO5!Htqo)2~?m3mM?H}a8OQ)8FEZB6y7@)0^JNx zQJFQT?KK0deW{yH&q$6DQOzp7h9D6$WGi z-qlnBzf`?5RSnQ-3!EG_6Z5u8S`U8<^!gl=ADc~D&7r8T&>=^M zqjPUZC@QItl!;uCRc^EW-rwJUu)X)*^Ywf^&LrOtCJ1sXLMByi<=9=qyM5M#h7i{8 z9kjyJ5A1Wr~V@B2|j0HOMGm_v-j4PZ5<+M4%Sn~nxFGM|eKDBN0yk@n5j)RW=q^z`!$ zEAjQT=cfP5{qVzoNfa*+tj9LrV(m-|jFHI-flJ9zDU5Zv=f{`l_@B?M8IsT%QR;dK zGFy=E=w0>o0CVq+3a=*BHE{l$Fn2j9q$C{u>!_VTyO>orp(n~yqNTUyA%|qh)ne?M zDJEwe*2W-sx3pRc_m_zSbyAU24neNu*tKg)-%z+VtU2Vo8PNFn{Wuc4Is>4k7NqSd zF~Wx=Mve>X#!sO8u?ml_)!+kRe;H&Lv9xoZ`DGq!}3c99XW;pFd1EjOCGL@w}v;5VZ! zyKcHcOZdcx?sGXE*@8U#j91U*)T+|I#4 z6iJ4>k*rWC$IewPFENz-Q4Q19<@l_-f_o!-}YNS_a(}ypM=eDyO>x*8NkpRF#ONponnw@&tTWi zNFZfsw$+n@$so}yKu;pzai0oVjWn|M*da=o=g0SY?jrjM15xaIx4EM%p+nN^Z#%O) z5RB96_$lMJVZm~3nc~HT@~FLpLSMc>!zPK91)|*UudG&{@g8`qE-vg|x2}T(<`ao^ zD-f)t9EGikrEfC^GmQ)Nz`bi8e`a>bxIS{SU$gts!%=1yR4EInN&FRHRNb2icYws@-p}Wx?Pnf0 z7XNctmQ$L5!wbPTYK-uktfrO2U)o}HwjpNQ$1AbFaR`%I@dvlWZ;n?-9bToT4vFqE;uvSM8d^C%U!xn>B(u7ksxb zjac?!3N8$fR*CheI3IV$VKbHsr(YUuq389hcN|%Z`BZ*nB0cvBONV+=kOwf5yWGL5 zcTc3OCO9a0Mwb*Het~hHKPC+(Kaqbof|vwvs>LvSz9rJhZm9yw}na zC@Uu>|Jq#sgsA4l3}ZhA)pCF>?IT-8IoOK@3Px&{PR!qIQ+Z#YT24?E@pB=3{LoDl z;ZB{M=8bBxM$f6Di&qbsxboE_O5YWDuZ)eCtA4S&0`}L|Rr;0Q&Dh>;N1LGuXk?to zol^3nXzJLvYlN8R`O9B|IBWHZ%JL3lDrH-s`xcbWtC$Co^i%9G>BLi(98Z26RL2|x zvHcBHW0>ND?lg`xg{g+EDnf0U2qk)Bv3Vo%=*K@nh+-nD>h2KzU97l_3DPAKqd35u z3qx{(dZiHf_l1iFz~2`%0O>kw7W+!LdTWbklofvE%W7`5;$ z%~tq9MeTBlOIn<3FW@ zu2fNX-y38hLHs&K8Ys5aPz_`+SmsYcIx|hr3Z?OFa)RE^g?L-pBM!fWc?-cYCjZN9 zhyV(2X;>A=9i*X>6V0DLEIZeR0c}G(7`To#3`Ek;<4|+S9f`)n+K9e4eA^H_ue1HF zW5rD+;hVv0cO*JxnEpqOoLiqBT9_|V42Sn-WS?}fG{DR(5e(@6tT5Ywcsbfp2O98N z!sjU+2=IGeqb6#r6BmlI%4$Y{`oTYJE#SF*t(0=f5L*T(8`Ws7JY0GKJvl1F$ZeKC zAlZ_1=AG}#U*KPx`frpKaA@sE5UIW&#IdbuFK6H4C#qD%`*esBS`1kM50ieF<}t3h zij@N_g(7Q)6=YuXngZ6J2W4h`mex#U{usDv7mXK1VWRl&gEyWk1nLe^QXfUa6*yE! zV3qv11js6I=*6U?NL%&e`ujH@$L||_|Nj@_eu($yjREgDhXY$`+}|R^ELmQx$Dc*j zMZ{g}&8?4-YtT+{u*Hpu%$xjWJ)v_I zSPooYXw@PzAhqlkgofD*SyA#BNXR2hcn`b*yeZFpZP6}XC8ZRI2zQp^b{}#Jup@Cj zLk>#H(e5Q}v|o?`Zff*Ge5eNL@kP6Avdr4po2!~`)wBR@TLsgvF^{Cox!EYxevCz4 zVijWi&PkcwRu{N;VyJDZX2|XqdtHtz7}p3~Z8a$zU!6r4Hf%#x0E@S_GS-*G8IwL! zfk+*4!#AgSZPb^VLO!_J1*KAtCgWYhxjOj1&4?t*b?&FV^Z?u-Q`3MDg^L>-Vlirx zPQIG)c8DKJRM@|m>$(QZp6Srd+`2@Kl8t#0(i|fkc5ZLz#KvYL!`C9Asb2+ zMmvJ0(1o+CT5lQ3C_hFXa_wzBk=EKMoE=gC6kYLyr_sB^dBHnD@fz>{ZQkH4yi(kua|%$5E}D}Rx_2<$N3~0{ z^KrKt5c6Hk*39{~D_nPsLC|#uruniP9g~HjTD?^quIR&UENxNq<@{lZ`*PeqRuS&N z^c9rG{&{~cgsm;B294-i$*N(jY5f=}XV?Q&Hr+x;mHy(y1%gu6My_q=Q=Bb&xjiWc zArJgoFR>C7I+%-(|(iXITA_Q!|R}ydv zAhJ_MOD7)VJbvLmo%FfgQro|aEm^%88NHqy+Xf6738MW^e{>+^2xFcUDA66hyI1iw zOEs5)e39hFuivAL@rU#6(~{oXEqxK{t_J$(5yhA@$+3h5wXiK9)I01B*R){b(oCR7 zGh6=W__{2Y5kQ38G!qGxZ~xgJfTIa&T5yQeMQ`N!BqvcmU@+$8cI6Vfx(@%JvXuaQ zXj0uK3Fc{r_BGY>?$596+wk*WM_vK*Y_3+#<2G#PF3kSJ#DM8sEq$SHCzfl{P%3d* z621>*N)@6_K9XXU4jF;RQF5j6127?^^Q**KrW4T4n0fMP9kMAAy7@RllTI&dpaEU> z|LN=E+l&}Lg2z$NoPsJ-J8>~f-+J$68wGYxQ5X%lsmjKnzeWrPO&;3W?PbG~bI20U z-W3hnuujAI63z{-Ci!64g}K*+OjrtEUj7>6S`ib<#kK)ik+JEApoqcuTJ$8q4f8>s zrh1|;)P}VdXa=_Lfp>!znaqUWwn_mkHB`pdVG)FV9VzGds1I-xeO}E^S`EK?_3CCi zQ#R#zD(**!VC_3#iyn2b9>Yw$Rf;~vtfkf~y<02o6-&~2f>mWZuu>2hGcae<=EVd! zLZe86?GWevwgxM4dAEPd^^$oU{w{kX?#NQXKfN}C*uCsZ3rU72KuD6T9Lb92tQfh^ zI`lXz@dXEK?5PqswcNdfmPFj}`oex}-6E~<>bX)J!wmeSgG`0Ulz1V}YruviPTIoH93}XBKVb?d z*$FdMzLgD|dYFGvS<+OMJ79|8wz5y9wgIyr?G`tYZh?;Sp?U|l?N(3!nMmKT^2a^0 zVm&{2<@cxFr`J;h-KFmBhW^O7(r~i*9E@_tG&rx1xv9m1MBr@s?~0cB1G$x>1+0MC zkNC8QzXx9pWG$&~MGh~t{*8HCa=9c=P5GAE{cSBg=|?z2Q^qP7ky5(_Y^F_A?*&jQ zQdtLapHM)fPxMJ!PO{ghD=~vd)VeHkp5^Ou`llK9C6HyWxJv}th3$JAum_s&_V$MMBergU}G)u zTCNm@XK04ARR?}-^D6im%G4(S#JAHoM}jxa1Wz3Qsnj+{Ck88*crlNXaxM88;v~p* ztgcGWn+(5d%_}$t7zQoOX?7LAf81Tad%5ZEk33-XJ@6vYYrjc(_d9WrXJd6_!W8c65-@$57SZDA9nu$1bPN+mj48I;Z| zz!{LXs+xfdnP>14^l<0p3O7H(-dCkRkVCE>Inpgw9OtMnm!H*RQrSf(pUUsP4%0A( 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ⅅhj}#1d90X>UO*O1p?{W66T9x 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 + +namespace gui::img { + +enum class ImgID { + Yoshi +}; + +struct Img { + unsigned int texture_id; + std::size_t width; + std::size_t height; +}; + +std::string getImgFilepath(ImgID img); + +} \ No newline at end of file diff --git a/include/client/gui/img/loader.hpp b/include/client/gui/img/loader.hpp new file mode 100644 index 00000000..033b22cc --- /dev/null +++ b/include/client/gui/img/loader.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "client/gui/img/img.hpp" + +#include + +namespace gui::img { + +class Loader { +public: + Loader() = default; + + bool init(); + +private: + std::unordered_map img_map; + + void _loadImg(); +}; + +} diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp new file mode 100644 index 00000000..696c8c9b --- /dev/null +++ b/include/client/gui/widget/staticimg.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "client/gui/widget/widget.hpp" +#include "client/gui/img/loader.hpp" +#include "client/gui/img/img.hpp" + +#include + +namespace gui::widget { + +class StaticImg : public Widget { +public: + using Ptr = std::unique_ptr; + + template + static Ptr make(Params&&... params) { + return std::make_unique(std::forward(params)...); + } + + StaticImg(glm::vec2 origin, gui::img::Img img, std::shared_ptr loader); + + void render(GLuint shader) override; + +private: + std::shared_ptr loader; + +}; + +} diff --git a/include/client/gui/widget/type.hpp b/include/client/gui/widget/type.hpp index 6dc210b8..a1a3cb12 100644 --- a/include/client/gui/widget/type.hpp +++ b/include/client/gui/widget/type.hpp @@ -3,7 +3,7 @@ namespace gui::widget { enum class Type { - DynText, Flexbox + DynText, Flexbox, StaticImg }; } diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 4cd3decb..c5bdb2ec 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -15,13 +15,17 @@ set(FILES util.cpp lobbyfinder.cpp + gui/imgs/img.cpp + gui/imgs/loader.cpp gui/font/font.cpp gui/font/loader.cpp gui/widget/widget.cpp gui/widget/dyntext.cpp gui/widget/flexbox.cpp + gui/widget/staticimg.cpp gui/gui.cpp + ../../dependencies/stb/stb_image.cpp ) # OpenGL @@ -29,6 +33,7 @@ set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL REQUIRED) add_library(${LIB_NAME} STATIC ${FILES}) +target_include_directories(${LIB_NAME} PRIVATE ../../dependencies/stb) # include stb image loading header target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) target_link_libraries(${LIB_NAME} @@ -46,6 +51,7 @@ target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static freetype) add_executable(${TARGET_NAME} main.cpp) +target_include_directories(${TARGET_NAME} PRIVATE ../../dependencies/stb) # include stb image loading header target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") target_link_libraries(${TARGET_NAME} PRIVATE game_shared_lib ${LIB_NAME}) diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index 62103d91..a04ab64f 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -8,9 +8,9 @@ namespace gui::font { std::string getFilepath(Font font) { auto dir = getRepoRoot() / "fonts"; switch (font) { - case Font::MENU: return (dir / "Lato-Regular.ttf").string(); + case Font::MENU: return (dir / "AncientModernTales-a7Po.ttf").string(); default: - case Font::TEXT: return (dir / "Lato-Regular.ttf").string(); + case Font::TEXT: return (dir / "AncientModernTales-a7Po.ttf").string(); } } diff --git a/src/client/gui/imgs/img.cpp b/src/client/gui/imgs/img.cpp new file mode 100644 index 00000000..437ecc15 --- /dev/null +++ b/src/client/gui/imgs/img.cpp @@ -0,0 +1,15 @@ +#include "client/gui/img/img.hpp" + +#include + +namespace gui::img { + +std::string getImgFilepath(ImgID img) { + auto img_root = getRepoRoot() / "imgs"; + switch (img) { + default: + case ImgID::Yoshi: return (img_root / "Yoshi.png").string(); + } +} + +} diff --git a/src/client/gui/imgs/loader.cpp b/src/client/gui/imgs/loader.cpp new file mode 100644 index 00000000..507b47de --- /dev/null +++ b/src/client/gui/imgs/loader.cpp @@ -0,0 +1,9 @@ +#include "client/gui/img/loader.hpp" + +namespace gui::img { + +bool Loader::init() { + +} + +} diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp new file mode 100644 index 00000000..5b7f107e --- /dev/null +++ b/src/client/gui/widget/staticimg.cpp @@ -0,0 +1,9 @@ +#include "client/gui/gui.hpp" + +namespace gui::widget { + +// StaticImg::StaticImg() + +// } + +} From c73f6d185123dfd92d416d467912c919de630ac1 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 30 Apr 2024 23:15:59 -0700 Subject: [PATCH 27/92] broken img loading --- include/client/gui/gui.hpp | 3 ++ include/client/gui/img/img.hpp | 11 +++-- include/client/gui/img/loader.hpp | 4 +- include/client/gui/widget/staticimg.hpp | 7 +-- src/client/gui/gui.cpp | 6 +++ src/client/gui/imgs/loader.cpp | 59 ++++++++++++++++++++++- src/client/gui/widget/staticimg.cpp | 62 ++++++++++++++++++++++++- 7 files changed, 142 insertions(+), 10 deletions(-) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 837af95d..89debd46 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -12,6 +12,8 @@ #include "client/gui/widget/staticimg.hpp" #include "client/gui/font/font.hpp" #include "client/gui/font/loader.hpp" +#include "client/gui/img/img.hpp" +#include "client/gui/img/loader.hpp" #include #include @@ -40,6 +42,7 @@ class GUI { GLuint text_shader; std::shared_ptr fonts; + img::Loader images; }; using namespace gui; diff --git a/include/client/gui/img/img.hpp b/include/client/gui/img/img.hpp index 88677d70..e5d13e97 100644 --- a/include/client/gui/img/img.hpp +++ b/include/client/gui/img/img.hpp @@ -1,7 +1,9 @@ #pragma once #include "shared/utilities/root_path.hpp" +#include "client/core.hpp" +#include #include namespace gui::img { @@ -10,10 +12,13 @@ enum class ImgID { Yoshi }; +#define GET_ALL_IMG_IDS() \ + {ImgID::Yoshi} + struct Img { - unsigned int texture_id; - std::size_t width; - std::size_t height; + GLuint texture_id; + int width; + int height; }; std::string getImgFilepath(ImgID img); diff --git a/include/client/gui/img/loader.hpp b/include/client/gui/img/loader.hpp index 033b22cc..42e02f98 100644 --- a/include/client/gui/img/loader.hpp +++ b/include/client/gui/img/loader.hpp @@ -12,10 +12,12 @@ class Loader { bool init(); + const Img& getImg(ImgID img_id) const; + private: std::unordered_map img_map; - void _loadImg(); + bool _loadImg(ImgID id); }; } diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp index 696c8c9b..8e088ddb 100644 --- a/include/client/gui/widget/staticimg.hpp +++ b/include/client/gui/widget/staticimg.hpp @@ -17,13 +17,14 @@ class StaticImg : public Widget { return std::make_unique(std::forward(params)...); } - StaticImg(glm::vec2 origin, gui::img::Img img, std::shared_ptr loader); + StaticImg(glm::vec2 origin, gui::img::Img img); + StaticImg(gui::img::Img img); void render(GLuint shader) override; private: - std::shared_ptr loader; - + gui::img::Img img; + GLuint VBO, VAO, EBO; }; } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index dd17d3d0..41f79f7e 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -23,12 +23,17 @@ bool GUI::init(GLuint text_shader) return false; } + if (!this->images.init()) { + return false; + } + this->text_shader = text_shader; auto title = widget::DynText::make("Arcana", this->fonts); title->addOnClick([](){std::cout << "Clickie click on title\n";}); auto option = widget::DynText::make("Start Game", this->fonts); option->addOnClick([](){std::cout << "click on option\n";}); + auto img = widget::StaticImg::make(this->images.getImg(img::ImgID::Yoshi)); auto flexbox = widget::Flexbox::make( glm::vec2(0.0f, 0.0f), @@ -38,6 +43,7 @@ bool GUI::init(GLuint text_shader) }); flexbox->push(std::move(title)); flexbox->push(std::move(option)); + flexbox->push(std::move(img)); this->addWidget(std::move(flexbox), 0.0f, 0.0f); diff --git a/src/client/gui/imgs/loader.cpp b/src/client/gui/imgs/loader.cpp index 507b47de..b904bfb7 100644 --- a/src/client/gui/imgs/loader.cpp +++ b/src/client/gui/imgs/loader.cpp @@ -1,9 +1,66 @@ #include "client/gui/img/loader.hpp" +#include "client/core.hpp" + +#include + +#include "stb_image.h" namespace gui::img { bool Loader::init() { - + std::cout << "Loading images...\n"; + + for (auto img_id : GET_ALL_IMG_IDS()) { + if (!this->_loadImg(img_id)) { + return false; + } + } + + std::cout << "Loaded images\n"; + return true; +} + +// reference: https://www.reddit.com/r/opengl/comments/57d21g/displaying_an_image_with_stb/ +bool Loader::_loadImg(ImgID img_id) { + auto path = getImgFilepath(img_id); + std::cout << "Loading " << path << "...\n"; + + int width, height, channels; + + unsigned char* img_data = stbi_load(path.c_str(), &width, &height, &channels, 4); + + GLuint texture_id; + if (img_data == 0 || width == 0 || height == 0) { + std::cerr << "Error loading " << path << "! " << img_data << + ", " << width << ", " << height << "\n" << std::endl; + return false; + } + + glGenTextures(1, &texture_id); + glBindTexture(GL_TEXTURE_2D, texture_id); + + //set up some vars for OpenGL texturizing + GLenum image_format = GL_RGBA; + GLint internal_format = GL_RGBA; + GLint level = 0; + //store the texture data for OpenGL use + glTexImage2D(GL_TEXTURE_2D, level, internal_format, width, height, + 0, image_format, GL_UNSIGNED_BYTE, img_data); + + stbi_image_free(img_data); + + glBindTexture(GL_TEXTURE_2D, 0); + + this->img_map.insert({img_id, Img { + .texture_id = texture_id, + .width = width, + .height = height + }}); + return true; +} + +const Img& Loader::getImg(ImgID img_id) const { + return this->img_map.at(img_id); } } diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 5b7f107e..3cda02aa 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -2,8 +2,66 @@ namespace gui::widget { -// StaticImg::StaticImg() +StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): + Widget(Type::StaticImg, origin) +{ + // reference: https://learnopengl.com/code_viewer.php?code=getting-started/textures -// } + // Set up vertex data (and buffer(s)) and attribute pointers + GLfloat vertices[] = { + // Positions // Colors // Texture Coords + 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // Top Right + 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // Bottom Right + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // Bottom Left + -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // Top Left + }; + GLuint indices[] = { // Note that we start from 0! + 0, 1, 3, // First Triangle + 1, 2, 3 // Second Triangle + }; + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(VAO); + + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // Position attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0); + glEnableVertexAttribArray(0); + // Color attribute + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat))); + glEnableVertexAttribArray(1); + // TexCoord attribute + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat))); + glEnableVertexAttribArray(2); + + glBindVertexArray(0); // Unbind VAO + + this->width = img.width; + this->height = img.height; +} + +StaticImg::StaticImg(gui::img::Img img): + StaticImg({0.0f, 0.0f}, img) {} + +void StaticImg::render(GLuint shader) { + glUseProgram(shader); + + // Bind Texture + glBindTexture(GL_TEXTURE_2D, this->img.texture_id); + // Draw container + glBindVertexArray(VAO); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + + glUseProgram(0); +} } From 718e8615ee4483493e4e908bba78056b715d8afa Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 00:42:58 -0700 Subject: [PATCH 28/92] fix center and add way to center in entire screen --- include/client/client.hpp | 4 ++-- include/client/gui/widget/flexbox.hpp | 9 +++++++-- src/client/gui/gui.cpp | 5 +++-- src/client/gui/widget/flexbox.cpp | 24 +++++++++++++----------- src/client/gui/widget/widget.cpp | 1 + 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index db379cd6..005720ce 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -22,8 +22,8 @@ #include "shared/network/session.hpp" #include "shared/utilities/config.hpp" -#define WINDOW_WIDTH 1280 -#define WINDOW_HEIGHT 960 +#define WINDOW_WIDTH 1920 +#define WINDOW_HEIGHT 1080 using namespace boost::asio::ip; diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index aad51abb..53c675a2 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -16,9 +16,14 @@ class Flexbox : public Widget { AlignItems alignment; }; - static Ptr make(glm::vec2 origin, Options options); - static Ptr make(glm::vec2 origin); + template + static Ptr make(Params&&... params) { + return std::make_unique(std::forward(params)...); + } + + Flexbox(glm::vec2 origin, glm::vec2 size, Options options); + Flexbox(glm::vec2 origin, glm::vec2 size); Flexbox(glm::vec2 origin, Options options); explicit Flexbox(glm::vec2 origin); diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 41f79f7e..d74f1e84 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -33,17 +33,18 @@ bool GUI::init(GLuint text_shader) title->addOnClick([](){std::cout << "Clickie click on title\n";}); auto option = widget::DynText::make("Start Game", this->fonts); option->addOnClick([](){std::cout << "click on option\n";}); - auto img = widget::StaticImg::make(this->images.getImg(img::ImgID::Yoshi)); + // auto img = widget::StaticImg::make(this->images.getImg(img::ImgID::Yoshi)); auto flexbox = widget::Flexbox::make( glm::vec2(0.0f, 0.0f), + glm::vec2(WINDOW_WIDTH, 0.0f), widget::Flexbox::Options { .direction { widget::JustifyContent::VERTICAL }, .alignment { widget::AlignItems::CENTER }, }); flexbox->push(std::move(title)); flexbox->push(std::move(option)); - flexbox->push(std::move(img)); + // flexbox->push(std::move(img)); this->addWidget(std::move(flexbox), 0.0f, 0.0f); diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index d236cacb..b3e76e02 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -8,24 +8,25 @@ namespace gui::widget { -Flexbox::Ptr Flexbox::make(glm::vec2 origin, Flexbox::Options options) { - return std::make_unique(origin, options); -} - -Flexbox::Ptr Flexbox::make(glm::vec2 origin) { - return std::make_unique(origin); +Flexbox::Flexbox(glm::vec2 origin, glm::vec2 size, Flexbox::Options options): + Widget(Type::Flexbox, origin) +{ + this->width = size.x; + this->height = size.y; } Flexbox::Flexbox(glm::vec2 origin, Flexbox::Options options): - Widget(Type::Flexbox, origin), - options(options) {} + Flexbox(origin, {0.0f, 0.0f}, options) {} -Flexbox::Flexbox(glm::vec2 origin): - Flexbox(origin, Flexbox::Options { +Flexbox::Flexbox(glm::vec2 origin, glm::vec2 size): + Flexbox(origin, size, Flexbox::Options { .direction = JustifyContent::HORIZONTAL, .alignment = AlignItems::LEFT, }) {} +Flexbox::Flexbox(glm::vec2 origin): + Flexbox(origin, {0.0f, 0.0f}) {} + void Flexbox::doClick(float x, float y) { Widget::doClick(x, y); for (auto& widget : this->widgets) { @@ -71,6 +72,8 @@ void Flexbox::push(Widget::Ptr&& widget) { widget->setOrigin(new_origin); } + this->widgets.push_back(std::move(widget)); + if (this->options.alignment == AlignItems::CENTER) { if (this->options.direction == JustifyContent::HORIZONTAL) { std::cerr << "Note: center alignment not yet implemented for horizontal justify. Doing nothing\n"; @@ -85,7 +88,6 @@ void Flexbox::push(Widget::Ptr&& widget) { std::cerr << "Note: right alignment not yet implemented. Doing nothing\n"; } // else it is align left, which is default behavior and requires no more messing - this->widgets.push_back(std::move(widget)); } void Flexbox::render(GLuint shader) { diff --git a/src/client/gui/widget/widget.cpp b/src/client/gui/widget/widget.cpp index fa3e3a92..e7bfd5b1 100644 --- a/src/client/gui/widget/widget.cpp +++ b/src/client/gui/widget/widget.cpp @@ -1,4 +1,5 @@ #include "client/gui/widget/widget.hpp" +#include "client/client.hpp" namespace gui::widget { From ef164ab311dba0a38462d35609a89d0b0f633ac7 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 01:18:50 -0700 Subject: [PATCH 29/92] attempt to render lobby menu BUT NOTHING IS F---- WORKING RIGHT!!! --- config.json | 4 ++-- include/client/client.hpp | 2 ++ include/client/gui/gui.hpp | 10 ++++++-- src/client/client.cpp | 45 +++++++++++++++++++++++++++++++++--- src/client/gui/font/font.cpp | 2 +- src/client/gui/gui.cpp | 40 ++++++++++++++------------------ src/client/main.cpp | 1 - 7 files changed, 72 insertions(+), 32 deletions(-) diff --git a/config.json b/config.json index 0d903310..e1b4a7f0 100644 --- a/config.json +++ b/config.json @@ -8,11 +8,11 @@ }, "server": { "lobby_name": "My Test Lobby", - "lobby_broadcast": false, + "lobby_broadcast": true, "max_players": 1 }, "client": { "default_name": "Player", - "lobby_discovery": false + "lobby_discovery": true } } \ No newline at end of file diff --git a/include/client/client.hpp b/include/client/client.hpp index 005720ce..f8c5c7d6 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -82,6 +82,8 @@ class Client { tcp::resolver resolver; tcp::socket socket; + LobbyFinder lobby_finder; + /// @brief Generate endpoints the client can connect to basic_resolver_results endpoints; std::shared_ptr session; diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 89debd46..cec5ff56 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -28,14 +28,20 @@ class GUI { bool init(GLuint text_shader); - void render(); + void beginFrame(); + void renderFrame(); + void endFrame(); - WidgetHandle addWidget(widget::Widget::Ptr&& widget, float x, float y); + WidgetHandle addWidget(widget::Widget::Ptr&& widget); std::unique_ptr removeWidget(WidgetHandle handle); void handleClick(float x, float y); void handleHover(float x, float y); + void clearAll(); + + std::shared_ptr getFonts(); + private: WidgetHandle next_handle {0}; std::unordered_map widgets; diff --git a/src/client/client.cpp b/src/client/client.cpp index 9e5b3db1..f719ab63 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -9,7 +9,9 @@ #include #include +#include +#include "client/gui/gui.hpp" #include "shared/game/event.hpp" #include "shared/network/constants.hpp" #include "shared/network/packet.hpp" @@ -40,9 +42,14 @@ Client::Client(boost::asio::io_context& io_context, GameConfig config): config(config), gameState(GamePhase::TITLE_SCREEN, config), session(nullptr), - gui() + gui(), + lobby_finder(io_context, config) { cam = new Camera(); + + if (config.client.lobby_discovery) { + lobby_finder.startSearching(); + } } void Client::connectAndListen(std::string ip_addr) { @@ -130,14 +137,46 @@ void Client::displayCallback() { /* Render here */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - this->gui.render(); + this->gui.beginFrame(); if (this->gameState.phase == GamePhase::TITLE_SCREEN) { - + auto flexbox = gui::widget::Flexbox::make( + glm::vec2(0.0f, 100.0f), + glm::vec2(WINDOW_WIDTH, 0.0f), + gui::widget::Flexbox::Options { + .direction = gui::widget::JustifyContent::VERTICAL, + .alignment = gui::widget::AlignItems::CENTER, + }); + + for (const auto& [ip, packet]: this->lobby_finder.getFoundLobbies()) { + auto entry = gui::widget::DynText::make(ip.address().to_string(), + this->gui.getFonts(), gui::widget::DynText::Options { + .font = gui::font::Font::MENU, + .font_size = gui::font::FontSizePx::SMALL, + .color = gui::font::getRGB(gui::font::FontColor::BLACK), + .scale = 1.0f + }); + auto entry2 = gui::widget::DynText::make(ip.address().to_string(), + this->gui.getFonts(), gui::widget::DynText::Options { + .font = gui::font::Font::MENU, + .font_size = gui::font::FontSizePx::SMALL, + .color = gui::font::getRGB(gui::font::FontColor::BLACK), + .scale = 1.0f + }); + entry->addOnClick([ip](){ + std::cout << "Clicked on " << ip.address().to_string() << "\n"; + }); + flexbox->push(std::move(entry)); + flexbox->push(std::move(entry2)); + this->gui.addWidget(std::move(flexbox)); + } } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); } + this->gui.renderFrame(); + this->gui.endFrame(); + /* Poll for and process events */ glfwPollEvents(); glfwSwapBuffers(window); diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index a04ab64f..97fd428d 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -10,7 +10,7 @@ std::string getFilepath(Font font) { switch (font) { case Font::MENU: return (dir / "AncientModernTales-a7Po.ttf").string(); default: - case Font::TEXT: return (dir / "AncientModernTales-a7Po.ttf").string(); + case Font::TEXT: return (dir / "Lato-Regular.ttf").string(); } } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index d74f1e84..6b3275c8 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -29,30 +29,16 @@ bool GUI::init(GLuint text_shader) this->text_shader = text_shader; - auto title = widget::DynText::make("Arcana", this->fonts); - title->addOnClick([](){std::cout << "Clickie click on title\n";}); - auto option = widget::DynText::make("Start Game", this->fonts); - option->addOnClick([](){std::cout << "click on option\n";}); - // auto img = widget::StaticImg::make(this->images.getImg(img::ImgID::Yoshi)); - - auto flexbox = widget::Flexbox::make( - glm::vec2(0.0f, 0.0f), - glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction { widget::JustifyContent::VERTICAL }, - .alignment { widget::AlignItems::CENTER }, - }); - flexbox->push(std::move(title)); - flexbox->push(std::move(option)); - // flexbox->push(std::move(img)); - - this->addWidget(std::move(flexbox), 0.0f, 0.0f); - std::cout << "Initialized GUI\n"; return true; } -void GUI::render() { +void GUI::beginFrame() { + +} + + +void GUI::renderFrame() { // for text rendering glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -67,9 +53,13 @@ void GUI::render() { glDisable(GL_BLEND); } -WidgetHandle GUI::addWidget(widget::Widget::Ptr&& widget, float x, float y) { +void GUI::endFrame() { + std::unordered_map empty; + std::swap(this->widgets, empty); +} + +WidgetHandle GUI::addWidget(widget::Widget::Ptr&& widget) { WidgetHandle handle = this->next_handle++; - const auto& [width, height] = widget->getSize(); this->widgets.insert({handle, std::move(widget)}); return handle; } @@ -100,4 +90,8 @@ void GUI::handleHover(float x, float y) { } } -} \ No newline at end of file +std::shared_ptr GUI::getFonts() { + return this->fonts; +} + +} diff --git a/src/client/main.cpp b/src/client/main.cpp index e9597a5e..2e1003dd 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -51,7 +51,6 @@ int main(int argc, char* argv[]) { auto config = GameConfig::parse(argc, argv); boost::asio::io_context context; - LobbyFinder lobby_finder(context, config); Client client(context, config); // if (config.client.lobby_discovery) { // lobby_finder.startSearching(); From bf72365401b471589dfc393b715d9ff0fc7655c3 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 12:06:21 -0700 Subject: [PATCH 30/92] render lobby screen, but it seg faults with more than 1 --- include/client/gui/font/font.hpp | 4 +-- src/client/client.cpp | 56 ++++++++++++++++++++----------- src/client/gui/gui.cpp | 5 ++- src/client/gui/widget/flexbox.cpp | 2 +- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index f04b9465..796ac3ac 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -14,8 +14,8 @@ enum class Font { enum FontSizePx { SMALL = 64, - MEDIUM = 128, - LARGE = 256 + MEDIUM = 96, + LARGE = 128 }; enum class FontColor { diff --git a/src/client/client.cpp b/src/client/client.cpp index f719ab63..608b5f28 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -36,6 +36,8 @@ bool Client::is_left_mouse_down = false; float Client::mouse_xpos = 0.0f; float Client::mouse_ypos = 0.0f; +using namespace gui; + Client::Client(boost::asio::io_context& io_context, GameConfig config): resolver(io_context), socket(io_context), @@ -140,35 +142,49 @@ void Client::displayCallback() { this->gui.beginFrame(); if (this->gameState.phase == GamePhase::TITLE_SCREEN) { - auto flexbox = gui::widget::Flexbox::make( - glm::vec2(0.0f, 100.0f), + auto title_flex = widget::Flexbox::make( + glm::vec2(0.0f, WINDOW_HEIGHT - font::FontSizePx::LARGE), glm::vec2(WINDOW_WIDTH, 0.0f), - gui::widget::Flexbox::Options { - .direction = gui::widget::JustifyContent::VERTICAL, - .alignment = gui::widget::AlignItems::CENTER, + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, + }); + auto title = widget::DynText::make( + "Lobbies", + this->gui.getFonts(), + widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::LARGE, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + }); + title_flex->push(std::move(title)); + this->gui.addWidget(std::move(title_flex)); + + auto lobbies_flex = widget::Flexbox::make( + glm::vec2(0.0f, WINDOW_HEIGHT / 2.0f), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, }); for (const auto& [ip, packet]: this->lobby_finder.getFoundLobbies()) { - auto entry = gui::widget::DynText::make(ip.address().to_string(), - this->gui.getFonts(), gui::widget::DynText::Options { - .font = gui::font::Font::MENU, - .font_size = gui::font::FontSizePx::SMALL, - .color = gui::font::getRGB(gui::font::FontColor::BLACK), - .scale = 1.0f - }); - auto entry2 = gui::widget::DynText::make(ip.address().to_string(), - this->gui.getFonts(), gui::widget::DynText::Options { - .font = gui::font::Font::MENU, - .font_size = gui::font::FontSizePx::SMALL, - .color = gui::font::getRGB(gui::font::FontColor::BLACK), + std::stringstream ss; + ss << packet.lobby_name << " " << packet.slots_taken << "/" << packet.slots_avail + packet.slots_taken; + + auto entry = widget::DynText::make(ss.str(), + this->gui.getFonts(), widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::SMALL, + .color = font::getRGB(font::FontColor::BLACK), .scale = 1.0f }); entry->addOnClick([ip](){ std::cout << "Clicked on " << ip.address().to_string() << "\n"; }); - flexbox->push(std::move(entry)); - flexbox->push(std::move(entry2)); - this->gui.addWidget(std::move(flexbox)); + lobbies_flex->push(std::move(entry)); + this->gui.addWidget(std::move(lobbies_flex)); } } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 6b3275c8..60c4ba69 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -34,7 +34,8 @@ bool GUI::init(GLuint text_shader) } void GUI::beginFrame() { - + std::unordered_map empty; + std::swap(this->widgets, empty); } @@ -54,8 +55,6 @@ void GUI::renderFrame() { } void GUI::endFrame() { - std::unordered_map empty; - std::swap(this->widgets, empty); } WidgetHandle GUI::addWidget(widget::Widget::Ptr&& widget) { diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index b3e76e02..45e97cbb 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -9,7 +9,7 @@ namespace gui::widget { Flexbox::Flexbox(glm::vec2 origin, glm::vec2 size, Flexbox::Options options): - Widget(Type::Flexbox, origin) + Widget(Type::Flexbox, origin), options(options) { this->width = size.x; this->height = size.y; From 3f86eaeee9e81b5bc651c6b6a2c70218b01ee929 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 14:01:30 -0700 Subject: [PATCH 31/92] working lobby screen with dummy data --- include/client/client.hpp | 10 ++++++++-- include/client/gui/widget/flexbox.hpp | 1 + src/client/client.cpp | 12 ++++++++++-- src/client/gui/widget/flexbox.cpp | 9 +++++---- src/client/lobbyfinder.cpp | 13 ++++++++++++- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index f8c5c7d6..8ae7f22f 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -22,8 +22,14 @@ #include "shared/network/session.hpp" #include "shared/utilities/config.hpp" -#define WINDOW_WIDTH 1920 -#define WINDOW_HEIGHT 1080 +#define WINDOW_WIDTH 1500 +#define WINDOW_HEIGHT 1000 + +// position something a "frac" of the way across the screen +// e.g. WIDTH_FRAC(4) -> a fourth of the way from the left +// HEIGHT_FRAC(3) -> a third of the way from the bottom +#define FRAC_WINDOW_WIDTH(num, denom) WINDOW_WIDTH * static_cast(num) / static_cast(denom) +#define FRAC_WINDOW_HEIGHT(num, denom) WINDOW_HEIGHT * static_cast(num) / static_cast(denom) using namespace boost::asio::ip; diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index 53c675a2..12d9755d 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -14,6 +14,7 @@ class Flexbox : public Widget { struct Options { JustifyContent direction; AlignItems alignment; + float padding; }; diff --git a/src/client/client.cpp b/src/client/client.cpp index 608b5f28..29402404 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -148,6 +148,7 @@ void Client::displayCallback() { widget::Flexbox::Options { .direction = widget::JustifyContent::VERTICAL, .alignment = widget::AlignItems::CENTER, + .padding = 0.0f, }); auto title = widget::DynText::make( "Lobbies", @@ -162,11 +163,12 @@ void Client::displayCallback() { this->gui.addWidget(std::move(title_flex)); auto lobbies_flex = widget::Flexbox::make( - glm::vec2(0.0f, WINDOW_HEIGHT / 2.0f), + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), glm::vec2(WINDOW_WIDTH, 0.0f), widget::Flexbox::Options { .direction = widget::JustifyContent::VERTICAL, .alignment = widget::AlignItems::CENTER, + .padding = 10.0f, }); for (const auto& [ip, packet]: this->lobby_finder.getFoundLobbies()) { @@ -183,9 +185,13 @@ void Client::displayCallback() { entry->addOnClick([ip](){ std::cout << "Clicked on " << ip.address().to_string() << "\n"; }); + entry->addOnHover([](){ + std::cout << "hover\n"; + }); lobbies_flex->push(std::move(entry)); - this->gui.addWidget(std::move(lobbies_flex)); } + + this->gui.addWidget(std::move(lobbies_flex)); } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); } @@ -205,6 +211,8 @@ void Client::idleCallback(boost::asio::io_context& context) { is_left_mouse_down = false; } + this->gui.handleHover(mouse_xpos, mouse_ypos); + std::optional movement = glm::vec3(0.0f); if(is_held_right) diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index 45e97cbb..a0a0f2b6 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -22,6 +22,7 @@ Flexbox::Flexbox(glm::vec2 origin, glm::vec2 size): Flexbox(origin, size, Flexbox::Options { .direction = JustifyContent::HORIZONTAL, .alignment = AlignItems::LEFT, + .padding = 0 }) {} Flexbox::Flexbox(glm::vec2 origin): @@ -61,14 +62,14 @@ void Flexbox::push(Widget::Ptr&& widget) { } if (this->options.direction == JustifyContent::HORIZONTAL) { - this->width += new_width; + this->width += new_width + this->options.padding; this->height = std::max(this->height, new_height); - glm::vec2 new_origin(prev_origin.x + prev_width, prev_origin.y); + glm::vec2 new_origin(prev_origin.x + prev_width + this->options.padding, prev_origin.y); widget->setOrigin(new_origin); } else if (this->options.direction == JustifyContent::VERTICAL) { - this->height += new_height; + this->height += new_height + this->options.padding; this->width = std::max(this->width, new_width); - glm::vec2 new_origin(prev_origin.x, prev_origin.y + prev_height); + glm::vec2 new_origin(prev_origin.x, prev_origin.y + prev_height + this->options.padding); widget->setOrigin(new_origin); } diff --git a/src/client/lobbyfinder.cpp b/src/client/lobbyfinder.cpp index 0207afc5..71e6b469 100644 --- a/src/client/lobbyfinder.cpp +++ b/src/client/lobbyfinder.cpp @@ -16,7 +16,18 @@ LobbyFinder::LobbyFinder(boost::asio::io_context& io_context, const GameConfig& keep_searching(false), lobby_info_buf() { - + this->lobbies_avail.insert(std::make_pair(boost::asio::ip::udp::endpoint(boost::asio::ip::address::from_string("100.80.1.2"), 9999), + ServerLobbyBroadcastPacket{ + .lobby_name = "Test 1", + .slots_taken = 0, + .slots_avail = 1 + })); + this->lobbies_avail.insert(std::make_pair(boost::asio::ip::udp::endpoint(boost::asio::ip::address::from_string("100.80.1.3"), 9999), + ServerLobbyBroadcastPacket{ + .lobby_name = "Test 2", + .slots_taken = 0, + .slots_avail = 1 + })); } LobbyFinder::~LobbyFinder() { From 5c0a54e0769d507f77dc3fbde7a21ddc2c14c81e Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 15:06:39 -0700 Subject: [PATCH 32/92] add hover handling --- include/client/gui/gui.hpp | 24 ++++++++++++++++++----- include/client/gui/widget/dyntext.hpp | 1 + include/client/gui/widget/flexbox.hpp | 4 ++++ include/client/gui/widget/widget.hpp | 15 ++++++++++++-- src/client/client.cpp | 20 ++++++++++--------- src/client/gui/gui.cpp | 8 ++++---- src/client/gui/widget/dyntext.cpp | 4 ++++ src/client/gui/widget/flexbox.cpp | 26 +++++++++++++++++++++++++ src/client/gui/widget/widget.cpp | 28 ++++++++++++++++++++++++--- 9 files changed, 107 insertions(+), 23 deletions(-) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index cec5ff56..967e24ee 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -15,12 +15,12 @@ #include "client/gui/img/img.hpp" #include "client/gui/img/loader.hpp" +#include #include #include namespace gui { -using WidgetHandle = std::size_t; class GUI { public: @@ -32,8 +32,22 @@ class GUI { void renderFrame(); void endFrame(); - WidgetHandle addWidget(widget::Widget::Ptr&& widget); - std::unique_ptr removeWidget(WidgetHandle handle); + widget::Handle addWidget(widget::Widget::Ptr&& widget); + std::unique_ptr removeWidget(widget::Handle handle); + + // template + widget::Widget* borrowWidget(widget::Handle handle) { + for (const auto& [_, widget] : this->widgets) { + if (widget->hasHandle(handle)) { + return (widget->borrow(handle)); + } + } + + std::cerr << "GUI ERROR: attempting to borrowWidget from GUI\n" + << "with invalid handle. This should never happen\n" + << "and means we are doing something very very bad." << std::endl; + std::exit(1); + } void handleClick(float x, float y); void handleHover(float x, float y); @@ -43,8 +57,8 @@ class GUI { std::shared_ptr getFonts(); private: - WidgetHandle next_handle {0}; - std::unordered_map widgets; + widget::Handle next_handle {0}; + std::unordered_map widgets; GLuint text_shader; std::shared_ptr fonts; diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index ed462a64..5be812c0 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -33,6 +33,7 @@ class DynText : public Widget { void render(GLuint shader) override; + void changeColor(font::FontColor new_color); private: Options options; diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index 12d9755d..8d602c39 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -1,6 +1,7 @@ #pragma once #include "client/gui/widget/widget.hpp" +#include "client/gui/gui.hpp" #include #include @@ -34,6 +35,9 @@ class Flexbox : public Widget { void push(Widget::Ptr&& widget); void render(GLuint shader) override; + + Widget* borrow(Handle handle) override; + bool hasHandle(Handle handle) const override; private: Options options; diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index 0a6af975..29023687 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -5,14 +5,18 @@ #include "client/gui/widget/options.hpp" #include +#include #include #include #include namespace gui::widget { -using Callback = std::function; +class GUI; + +using Handle = std::size_t; using CallbackHandle = std::size_t; +using Callback = std::function; class Widget { public: @@ -21,7 +25,7 @@ class Widget { explicit Widget(Type type, glm::vec2 origin); void setOrigin(glm::vec2 origin); - const glm::vec2& getOrigin() const; + [[nodiscard]] const glm::vec2& getOrigin() const; CallbackHandle addOnClick(Callback callback); CallbackHandle addOnHover(Callback callback); @@ -38,7 +42,14 @@ class Widget { [[nodiscard]] std::pair getSize() const; + [[nodiscard]] Handle getHandle() const; + [[nodiscard]] virtual bool hasHandle(Handle handle) const; + [[nodiscard]] virtual Widget* borrow(Handle handle); + protected: + Handle handle; + static std::size_t num_widgets; + Type type; glm::vec2 origin; std::size_t width {0}; diff --git a/src/client/client.cpp b/src/client/client.cpp index 29402404..96350fc9 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -182,11 +182,12 @@ void Client::displayCallback() { .color = font::getRGB(font::FontColor::BLACK), .scale = 1.0f }); - entry->addOnClick([ip](){ + entry->addOnClick([ip, this](widget::Handle handle){ std::cout << "Clicked on " << ip.address().to_string() << "\n"; }); - entry->addOnHover([](){ - std::cout << "hover\n"; + entry->addOnHover([this](widget::Handle handle){ + auto widget = dynamic_cast(this->gui.borrowWidget(handle)); + widget->changeColor(font::FontColor::BLUE); }); lobbies_flex->push(std::move(entry)); } @@ -196,6 +197,13 @@ void Client::displayCallback() { this->draw(); } + if (is_left_mouse_down) { + this->gui.handleClick(mouse_xpos, mouse_ypos); + is_left_mouse_down = false; + } + + this->gui.handleHover(mouse_xpos, mouse_ypos); + this->gui.renderFrame(); this->gui.endFrame(); @@ -206,12 +214,6 @@ void Client::displayCallback() { // Handle any updates void Client::idleCallback(boost::asio::io_context& context) { - if (is_left_mouse_down) { - this->gui.handleClick(mouse_xpos, mouse_ypos); - is_left_mouse_down = false; - } - - this->gui.handleHover(mouse_xpos, mouse_ypos); std::optional movement = glm::vec3(0.0f); diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 60c4ba69..d36b32ce 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -34,7 +34,7 @@ bool GUI::init(GLuint text_shader) } void GUI::beginFrame() { - std::unordered_map empty; + std::unordered_map empty; std::swap(this->widgets, empty); } @@ -57,13 +57,13 @@ void GUI::renderFrame() { void GUI::endFrame() { } -WidgetHandle GUI::addWidget(widget::Widget::Ptr&& widget) { - WidgetHandle handle = this->next_handle++; +widget::Handle GUI::addWidget(widget::Widget::Ptr&& widget) { + widget::Handle handle = this->next_handle++; this->widgets.insert({handle, std::move(widget)}); return handle; } -std::unique_ptr GUI::removeWidget(WidgetHandle handle) { +std::unique_ptr GUI::removeWidget(widget::Handle handle) { auto widget = std::move(this->widgets.at(handle)); this->widgets.erase(handle); return widget; diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 66ef1c9e..aead2dc7 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -104,4 +104,8 @@ void DynText::render(GLuint shader) { glUseProgram(0); } +void DynText::changeColor(font::FontColor new_color) { + this->options.color = font::getRGB(new_color); +} + } \ No newline at end of file diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index a0a0f2b6..858c9d49 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -99,6 +99,32 @@ void Flexbox::render(GLuint shader) { } } +Widget* Flexbox::borrow(Handle handle) { + for (auto& widget : this->widgets) { + if (widget->getHandle() == handle) { + return widget.get(); + } + } + + if (handle != this->handle) { + std::cerr << "UI ERROR: Trying to borrow from Flexbox with invalid handle\n" + << "This should never happen, and this means that we are doing something\n" + << "very wrong." << std::endl; + std::exit(1); + } + + return this; +} + +bool Flexbox::hasHandle(Handle handle) const { + for (auto& widget : this->widgets) { + if (widget->getHandle() == handle) { + return true; + } + } + + return this->handle == handle; +} } diff --git a/src/client/gui/widget/widget.cpp b/src/client/gui/widget/widget.cpp index e7bfd5b1..41d66a81 100644 --- a/src/client/gui/widget/widget.cpp +++ b/src/client/gui/widget/widget.cpp @@ -1,12 +1,15 @@ #include "client/gui/widget/widget.hpp" #include "client/client.hpp" +#include "client/gui/gui.hpp" namespace gui::widget { +std::size_t Widget::num_widgets = 0; + Widget::Widget(Type type, glm::vec2 origin): type(type), origin(origin) { - + this->handle = num_widgets++; } void Widget::setOrigin(glm::vec2 origin) { @@ -41,7 +44,7 @@ void Widget::removeOnHover(CallbackHandle handle) { void Widget::doClick(float x, float y) { if (this->_doesIntersect(x, y)) { for (const auto& [_handle, callback] : this->on_clicks) { - callback(); + callback(handle); } } } @@ -49,7 +52,7 @@ void Widget::doClick(float x, float y) { void Widget::doHover(float x, float y) { if (this->_doesIntersect(x, y)) { for (const auto& [_handle, callback] : this->on_hovers) { - callback(); + callback(handle); } } } @@ -71,4 +74,23 @@ bool Widget::_doesIntersect(float x, float y) const { ); } +Handle Widget::getHandle() const { + return this->handle; +} + +bool Widget::hasHandle(Handle handle) const { + return this->handle == handle; +} + +Widget* Widget::borrow(Handle handle) { + if (this->handle != handle) { + std::cerr << "UI ERROR: Trying to borrow from Widget with invalid handle\n" + << "This should never happen, and this means that we are doing something\n" + << "very wrong." << std::endl; + std::exit(1); + } + + return this; +} + } From 798896ba9e56724398c3242d55d3d59bc352e6e4 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 15:14:13 -0700 Subject: [PATCH 33/92] change borrow to template function --- include/client/gui/gui.hpp | 6 +++--- src/client/client.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 967e24ee..b5d738c1 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -35,11 +35,11 @@ class GUI { widget::Handle addWidget(widget::Widget::Ptr&& widget); std::unique_ptr removeWidget(widget::Handle handle); - // template - widget::Widget* borrowWidget(widget::Handle handle) { + template + W* borrowWidget(widget::Handle handle) { for (const auto& [_, widget] : this->widgets) { if (widget->hasHandle(handle)) { - return (widget->borrow(handle)); + return dynamic_cast(widget->borrow(handle)); } } diff --git a/src/client/client.cpp b/src/client/client.cpp index 96350fc9..06cec907 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -186,7 +186,7 @@ void Client::displayCallback() { std::cout << "Clicked on " << ip.address().to_string() << "\n"; }); entry->addOnHover([this](widget::Handle handle){ - auto widget = dynamic_cast(this->gui.borrowWidget(handle)); + auto widget = this->gui.borrowWidget(handle); widget->changeColor(font::FontColor::BLUE); }); lobbies_flex->push(std::move(entry)); From d04c73980dea63d5815e3725d41170a8e801d833 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 16:33:56 -0700 Subject: [PATCH 34/92] create CenterText macro widget for easier use --- config.json | 6 +- include/client/client.hpp | 5 + include/client/gui/gui.hpp | 3 +- include/client/gui/widget/centertext.hpp | 20 ++++ include/client/gui/widget/flexbox.hpp | 4 +- src/client/CMakeLists.txt | 1 + src/client/client.cpp | 111 +++++++++++------------ src/client/gui/gui.cpp | 7 +- src/client/gui/widget/centertext.cpp | 35 +++++++ src/client/lobbyfinder.cpp | 12 --- 10 files changed, 125 insertions(+), 79 deletions(-) create mode 100644 include/client/gui/widget/centertext.hpp create mode 100644 src/client/gui/widget/centertext.cpp diff --git a/config.json b/config.json index e1b4a7f0..004bf7e9 100644 --- a/config.json +++ b/config.json @@ -7,12 +7,12 @@ "server_port": 2355 }, "server": { - "lobby_name": "My Test Lobby", + "lobby_name": "Tyler's Lobby", "lobby_broadcast": true, - "max_players": 1 + "max_players": 2 }, "client": { - "default_name": "Player", + "default_name": "Tyler", "lobby_discovery": true } } \ No newline at end of file diff --git a/include/client/client.hpp b/include/client/client.hpp index 8ae7f22f..8e45a358 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -38,6 +38,11 @@ class Client { public: // Callbacks void displayCallback(); + + // set up ui for a specific screen + void createLobbyFinderGUI(); + void createLobbyGUI(); + void idleCallback(boost::asio::io_context& context); // Bound window callbacks diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index b5d738c1..68782a79 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -10,6 +10,7 @@ #include "client/gui/widget/dyntext.hpp" #include "client/gui/widget/flexbox.hpp" #include "client/gui/widget/staticimg.hpp" +#include "client/gui/widget/centertext.hpp" #include "client/gui/font/font.hpp" #include "client/gui/font/loader.hpp" #include "client/gui/img/img.hpp" @@ -30,7 +31,7 @@ class GUI { void beginFrame(); void renderFrame(); - void endFrame(); + void endFrame(float mouse_xpos, float mouse_ypos, bool is_left_mouse_down); widget::Handle addWidget(widget::Widget::Ptr&& widget); std::unique_ptr removeWidget(widget::Handle handle); diff --git a/include/client/gui/widget/centertext.hpp b/include/client/gui/widget/centertext.hpp new file mode 100644 index 00000000..38a9afe5 --- /dev/null +++ b/include/client/gui/widget/centertext.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "client/gui/widget/flexbox.hpp" +#include "client/gui/widget/dyntext.hpp" + +namespace gui::widget { + +class CenterText { +public: + static Flexbox::Ptr make( + std::string text, + font::Font font, + font::FontSizePx size, + font::FontColor color, + std::shared_ptr fonts, + float y_pos + ); +}; + +} diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index 8d602c39..1381e4a4 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -1,7 +1,6 @@ #pragma once #include "client/gui/widget/widget.hpp" -#include "client/gui/gui.hpp" #include #include @@ -18,7 +17,6 @@ class Flexbox : public Widget { float padding; }; - template static Ptr make(Params&&... params) { return std::make_unique(std::forward(params)...); @@ -41,7 +39,7 @@ class Flexbox : public Widget { private: Options options; - std::vector> widgets; + std::vector widgets; }; diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index c5bdb2ec..7dc9b457 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -19,6 +19,7 @@ set(FILES gui/imgs/loader.cpp gui/font/font.cpp gui/font/loader.cpp + gui/widget/centertext.cpp gui/widget/widget.cpp gui/widget/dyntext.cpp gui/widget/flexbox.cpp diff --git a/src/client/client.cpp b/src/client/client.cpp index 06cec907..89e776c2 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -142,76 +142,69 @@ void Client::displayCallback() { this->gui.beginFrame(); if (this->gameState.phase == GamePhase::TITLE_SCREEN) { - auto title_flex = widget::Flexbox::make( - glm::vec2(0.0f, WINDOW_HEIGHT - font::FontSizePx::LARGE), - glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 0.0f, - }); - auto title = widget::DynText::make( - "Lobbies", - this->gui.getFonts(), - widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::LARGE, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - }); - title_flex->push(std::move(title)); - this->gui.addWidget(std::move(title_flex)); - - auto lobbies_flex = widget::Flexbox::make( - glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), - glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 10.0f, - }); - - for (const auto& [ip, packet]: this->lobby_finder.getFoundLobbies()) { - std::stringstream ss; - ss << packet.lobby_name << " " << packet.slots_taken << "/" << packet.slots_avail + packet.slots_taken; - - auto entry = widget::DynText::make(ss.str(), - this->gui.getFonts(), widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::SMALL, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - }); - entry->addOnClick([ip, this](widget::Handle handle){ - std::cout << "Clicked on " << ip.address().to_string() << "\n"; - }); - entry->addOnHover([this](widget::Handle handle){ - auto widget = this->gui.borrowWidget(handle); - widget->changeColor(font::FontColor::BLUE); - }); - lobbies_flex->push(std::move(entry)); - } - - this->gui.addWidget(std::move(lobbies_flex)); + this->createLobbyFinderGUI(); + } else if (this->gameState.phase == GamePhase::LOBBY) { + this->createLobbyGUI(); } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); } - if (is_left_mouse_down) { - this->gui.handleClick(mouse_xpos, mouse_ypos); - is_left_mouse_down = false; - } - - this->gui.handleHover(mouse_xpos, mouse_ypos); - + this->gui.endFrame(mouse_xpos, mouse_ypos, is_left_mouse_down); this->gui.renderFrame(); - this->gui.endFrame(); /* Poll for and process events */ glfwPollEvents(); glfwSwapBuffers(window); } +void Client::createLobbyFinderGUI() { + this->gui.addWidget(widget::CenterText::make( + "Lobbies", + font::Font::MENU, + font::FontSizePx::LARGE, + font::FontColor::BLACK, + this->gui.getFonts(), + WINDOW_HEIGHT - font::FontSizePx::LARGE + )); + + auto lobbies_flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, + .padding = 10.0f, + }); + + for (const auto& [ip, packet]: this->lobby_finder.getFoundLobbies()) { + std::stringstream ss; + ss << packet.lobby_name << " " << packet.slots_taken << "/" << packet.slots_avail + packet.slots_taken; + + auto entry = widget::DynText::make(ss.str(), + this->gui.getFonts(), widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::SMALL, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + }); + entry->addOnClick([ip, this](widget::Handle handle){ + std::cout << "Connecting to " << ip.address() << " ...\n"; + this->connectAndListen(ip.address().to_string()); + }); + entry->addOnHover([this](widget::Handle handle){ + auto widget = this->gui.borrowWidget(handle); + widget->changeColor(font::FontColor::BLUE); + }); + lobbies_flex->push(std::move(entry)); + } + + this->gui.addWidget(std::move(lobbies_flex)); +} + +void Client::createLobbyGUI() { + // auto title; +} + // Handle any updates void Client::idleCallback(boost::asio::io_context& context) { diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index d36b32ce..6da315d1 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -54,7 +54,12 @@ void GUI::renderFrame() { glDisable(GL_BLEND); } -void GUI::endFrame() { +void GUI::endFrame(float mouse_xpos, float mouse_ypos, bool is_left_mouse_down) { + if (is_left_mouse_down) { + this->handleClick(mouse_xpos, mouse_ypos); + is_left_mouse_down = false; + } + this->handleHover(mouse_xpos, mouse_ypos); } widget::Handle GUI::addWidget(widget::Widget::Ptr&& widget) { diff --git a/src/client/gui/widget/centertext.cpp b/src/client/gui/widget/centertext.cpp new file mode 100644 index 00000000..4a192f78 --- /dev/null +++ b/src/client/gui/widget/centertext.cpp @@ -0,0 +1,35 @@ +#include "client/gui/widget/centertext.hpp" +#include "client/client.hpp" + +namespace gui::widget { + +Flexbox::Ptr CenterText::make( + std::string text, + font::Font font, + font::FontSizePx size, + font::FontColor color, + std::shared_ptr fonts, + float y_pos +) { + auto flex = widget::Flexbox::make( + glm::vec2(0.0f, y_pos), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, + .padding = 0.0f, + }); + auto title = widget::DynText::make( + text, + fonts, + widget::DynText::Options { + .font = font, + .font_size = size, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + }); + flex->push(std::move(title)); + return flex; +} + +} diff --git a/src/client/lobbyfinder.cpp b/src/client/lobbyfinder.cpp index 71e6b469..ce2e88c9 100644 --- a/src/client/lobbyfinder.cpp +++ b/src/client/lobbyfinder.cpp @@ -16,18 +16,6 @@ LobbyFinder::LobbyFinder(boost::asio::io_context& io_context, const GameConfig& keep_searching(false), lobby_info_buf() { - this->lobbies_avail.insert(std::make_pair(boost::asio::ip::udp::endpoint(boost::asio::ip::address::from_string("100.80.1.2"), 9999), - ServerLobbyBroadcastPacket{ - .lobby_name = "Test 1", - .slots_taken = 0, - .slots_avail = 1 - })); - this->lobbies_avail.insert(std::make_pair(boost::asio::ip::udp::endpoint(boost::asio::ip::address::from_string("100.80.1.3"), 9999), - ServerLobbyBroadcastPacket{ - .lobby_name = "Test 2", - .slots_taken = 0, - .slots_avail = 1 - })); } LobbyFinder::~LobbyFinder() { From f7948cc8d28b4f887b5bb57a19a4284a744c3f87 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 18:08:34 -0700 Subject: [PATCH 35/92] add text input widget --- include/client/client.hpp | 1 + include/client/gui/font/font.hpp | 3 +- include/client/gui/gui.hpp | 12 +++- include/client/gui/widget/textinput.hpp | 49 ++++++++++++++++ include/client/gui/widget/type.hpp | 2 +- include/client/util.hpp | 2 +- src/client/CMakeLists.txt | 1 + src/client/client.cpp | 33 ++++++++++- src/client/gui/font/font.cpp | 2 + src/client/gui/gui.cpp | 31 +++++++++- src/client/gui/widget/dyntext.cpp | 2 +- src/client/gui/widget/textinput.cpp | 76 +++++++++++++++++++++++++ src/client/main.cpp | 10 ++-- 13 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 include/client/gui/widget/textinput.hpp create mode 100644 src/client/gui/widget/textinput.cpp diff --git a/include/client/client.hpp b/include/client/client.hpp index 8e45a358..fe393405 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -49,6 +49,7 @@ class Client { static void keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods); static void mouseCallback(GLFWwindow* window, double xposIn, double yposIn); static void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods); + static void charCallback(GLFWwindow* window, unsigned int codepoint); // Getter / Setters GLFWwindow* getWindow() { return window; } diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index 796ac3ac..d4da741e 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -21,7 +21,8 @@ enum FontSizePx { enum class FontColor { BLACK, RED, - BLUE + BLUE, + GRAY }; std::string getFilepath(Font font); diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 68782a79..ac7500f8 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -11,6 +11,7 @@ #include "client/gui/widget/flexbox.hpp" #include "client/gui/widget/staticimg.hpp" #include "client/gui/widget/centertext.hpp" +#include "client/gui/widget/textinput.hpp" #include "client/gui/font/font.hpp" #include "client/gui/font/loader.hpp" #include "client/gui/img/img.hpp" @@ -31,11 +32,17 @@ class GUI { void beginFrame(); void renderFrame(); - void endFrame(float mouse_xpos, float mouse_ypos, bool is_left_mouse_down); + void endFrame(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down); widget::Handle addWidget(widget::Widget::Ptr&& widget); std::unique_ptr removeWidget(widget::Handle handle); + bool shouldCaptureKeystrokes() const; + void setCaptureKeystrokes(bool should_capture); + void captureKeystroke(char c); + void captureBackspace(); + std::string getCapturedKeyboardInput() const; + template W* borrowWidget(widget::Handle handle) { for (const auto& [_, widget] : this->widgets) { @@ -64,6 +71,9 @@ class GUI { std::shared_ptr fonts; img::Loader images; + + bool capture_keystrokes; + std::string keyboard_input; }; using namespace gui; diff --git a/include/client/gui/widget/textinput.hpp b/include/client/gui/widget/textinput.hpp new file mode 100644 index 00000000..3b69fdf5 --- /dev/null +++ b/include/client/gui/widget/textinput.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "client/gui/widget/widget.hpp" +#include "client/gui/widget/dyntext.hpp" +#include "client/gui/font/font.hpp" +#include "client/gui/font/loader.hpp" + +#include +#include + +namespace gui { + class GUI; +} + +namespace gui::widget { + +class TextInput : public Widget { +public: + using Ptr = std::unique_ptr; + + template + static Ptr make(Params&&... params) { + return std::make_unique(std::forward(params)...); + } + + TextInput(glm::vec2 origin, + std::string placeholder, + gui::GUI* gui, + std::shared_ptr fonts, + DynText::Options options); + + TextInput(glm::vec2 origin, + std::string placeholder, + gui::GUI* gui, + std::shared_ptr fonts); + + void render(GLuint shader) override; + + bool hasHandle(Handle handle) const override; + Widget* borrow(Handle handle) override; + void doClick(float x, float y) override; + void doHover(float x, float y) override; + +private: + widget::DynText::Ptr dyntext; + gui::GUI* gui; +}; + +} diff --git a/include/client/gui/widget/type.hpp b/include/client/gui/widget/type.hpp index a1a3cb12..efb920d7 100644 --- a/include/client/gui/widget/type.hpp +++ b/include/client/gui/widget/type.hpp @@ -3,7 +3,7 @@ namespace gui::widget { enum class Type { - DynText, Flexbox, StaticImg + DynText, Flexbox, StaticImg, TextInput }; } diff --git a/include/client/util.hpp b/include/client/util.hpp index 5ae6653d..ac1c0b4c 100644 --- a/include/client/util.hpp +++ b/include/client/util.hpp @@ -15,4 +15,4 @@ #include "client/core.hpp" -GLuint LoadShaders(const char* vertex_file_path, const char* fragment_file_path); +GLuint LoadShaders(const char* vertex_file_path, const char* fragment_file_path); \ No newline at end of file diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 7dc9b457..4cadb9ee 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -20,6 +20,7 @@ set(FILES gui/font/font.cpp gui/font/loader.cpp gui/widget/centertext.cpp + gui/widget/textinput.cpp gui/widget/widget.cpp gui/widget/dyntext.cpp gui/widget/flexbox.cpp diff --git a/src/client/client.cpp b/src/client/client.cpp index 89e776c2..93f591b8 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -199,6 +199,19 @@ void Client::createLobbyFinderGUI() { } this->gui.addWidget(std::move(lobbies_flex)); + + this->gui.addWidget(widget::TextInput::make( + glm::vec2(0.0f, 0.0f), + "Enter a name", + &this->gui, + this->gui.getFonts(), + widget::DynText::Options { + .font = font::Font::TEXT, + .font_size = font::FontSizePx::SMALL, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + } + )); } void Client::createLobbyGUI() { @@ -274,12 +287,22 @@ void Client::draw() { // callbacks - for Interaction void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) { - // Check for a key press. + Client* client = static_cast(glfwGetWindowUserPointer(window)); + + // Check for a key press. if (action == GLFW_PRESS) { switch (key) { case GLFW_KEY_ESCAPE: // Close the window. This causes the program to also terminate. - glfwSetWindowShouldClose(window, GL_TRUE); + client->gui.setCaptureKeystrokes(false); + break; + + case GLFW_KEY_TAB: + client->gui.setCaptureKeystrokes(true); + break; + + case GLFW_KEY_BACKSPACE: + client->gui.captureBackspace(); break; case GLFW_KEY_DOWN: @@ -373,4 +396,10 @@ void Client::mouseButtonCallback(GLFWwindow* window, int button, int action, int is_left_mouse_down = false; } } +} + +void Client::charCallback(GLFWwindow* window, unsigned int codepoint) { + Client* client = static_cast(glfwGetWindowUserPointer(window)); + + client->gui.captureKeystroke(static_cast(codepoint)); } \ No newline at end of file diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index 97fd428d..a1631c2b 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -20,6 +20,8 @@ glm::vec3 getRGB(FontColor color) { return {1.0f, 0.0f, 0.0f}; case FontColor::BLUE: return {0.0f, 0.0f, 1.0f}; + case FontColor::GRAY: + return {0.5f, 0.5f, 0.5f}; default: case FontColor::BLACK: return {0.0f, 0.0f, 0.0f}; diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 6da315d1..6685f177 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -18,6 +18,7 @@ bool GUI::init(GLuint text_shader) std::cout << "Initializing GUI...\n"; this->fonts = std::make_shared(); + this->capture_keystrokes = false; if (!this->fonts->init()) { return false; @@ -54,7 +55,7 @@ void GUI::renderFrame() { glDisable(GL_BLEND); } -void GUI::endFrame(float mouse_xpos, float mouse_ypos, bool is_left_mouse_down) { +void GUI::endFrame(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down) { if (is_left_mouse_down) { this->handleClick(mouse_xpos, mouse_ypos); is_left_mouse_down = false; @@ -74,6 +75,34 @@ std::unique_ptr GUI::removeWidget(widget::Handle handle) { return widget; } +void GUI::captureBackspace() { + if (this->shouldCaptureKeystrokes()) { + if (!this->keyboard_input.empty()) { + this->keyboard_input.pop_back(); + } + } +} + +void GUI::captureKeystroke(char c) { + if (this->shouldCaptureKeystrokes()) { + if (c >= 32 && c <= 126) { // meaningful character + this->keyboard_input.push_back(c); + } + } +} + +std::string GUI::getCapturedKeyboardInput() const { + return this->keyboard_input.c_str(); +} + +bool GUI::shouldCaptureKeystrokes() const { + return this->capture_keystrokes; +} + +void GUI::setCaptureKeystrokes(bool should_capture) { + this->capture_keystrokes = should_capture; +} + // TODO: reduce copied code between these two functions void GUI::handleClick(float x, float y) { diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index aead2dc7..75969da7 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -41,7 +41,7 @@ DynText::DynText(glm::vec2 origin, std::string text, DynText::DynText(glm::vec2 origin, std::string text, std::shared_ptr fonts): DynText(origin, text, fonts, DynText::Options { - .font {font::Font::TEXT}, + .font {font::Font::MENU}, .font_size {font::FontSizePx::MEDIUM}, .color {font::getRGB(font::FontColor::BLACK)}, .scale {1.0}, diff --git a/src/client/gui/widget/textinput.cpp b/src/client/gui/widget/textinput.cpp new file mode 100644 index 00000000..455abd4f --- /dev/null +++ b/src/client/gui/widget/textinput.cpp @@ -0,0 +1,76 @@ +#include "client/gui/widget/textinput.hpp" +#include "client/core.hpp" +#include "client/gui/gui.hpp" +#include + +namespace gui::widget { + +TextInput::TextInput(glm::vec2 origin, + std::string placeholder, + gui::GUI* gui, + std::shared_ptr fonts, + DynText::Options options): + Widget(Type::TextInput, origin) +{ + std::string text_to_display; + font::FontColor color_to_display; + std::string captured_input = gui->getCapturedKeyboardInput(); + if (captured_input.size() == 0) { + text_to_display = placeholder; + options.color = font::getRGB(font::FontColor::GRAY); + } else { + text_to_display = captured_input; + } + + this->dyntext = DynText::make(origin, text_to_display, fonts, options); + this->dyntext->addOnClick([gui](widget::Handle handle) { + gui->setCaptureKeystrokes(true); + }); +} + +TextInput::TextInput(glm::vec2 origin, + std::string placeholder, + gui::GUI* gui, + std::shared_ptr fonts): + TextInput(origin, placeholder, gui, fonts, DynText::Options { + .font {font::Font::TEXT}, + .font_size {font::FontSizePx::MEDIUM}, + .color {font::getRGB(font::FontColor::BLACK)}, + .scale {1.0}, + }) {} + +void TextInput::render(GLuint shader) { + this->dyntext->render(shader); +} + +bool TextInput::hasHandle(Handle handle) const { + return handle == this->dyntext->getHandle() || + this->handle == handle; +} + +Widget* TextInput::borrow(Handle handle) { + if (this->handle == handle) { + return this; + } + + if (this->dyntext->getHandle() == handle) { + return this->dyntext.get(); + } + + std::cerr << "UI ERROR: Trying to borrow from Text Input with invalid handle\n" + << "This should never happen, and this means that we are doing something\n" + << "very wrong." << std::endl; + std::exit(1); +} + +void TextInput::doClick(float x, float y) { + Widget::doClick(x, y); + this->dyntext->doClick(x, y); +} + +void TextInput::doHover(float x, float y) { + Widget::doHover(x, y); + this->dyntext->doHover(x, y); +} + +} \ No newline at end of file diff --git a/src/client/main.cpp b/src/client/main.cpp index 2e1003dd..95aedfc8 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -28,6 +28,8 @@ void set_callbacks(GLFWwindow* window, Client* client) { // Set the mouse and cursor callbacks glfwSetMouseButtonCallback(window, Client::mouseButtonCallback); glfwSetCursorPosCallback(window, Client::mouseCallback); + + glfwSetCharCallback(window, Client::charCallback); } void set_opengl_settings(GLFWwindow* window) { @@ -52,12 +54,6 @@ int main(int argc, char* argv[]) auto config = GameConfig::parse(argc, argv); boost::asio::io_context context; Client client(context, config); - // if (config.client.lobby_discovery) { - // lobby_finder.startSearching(); - // } - // } else { - // client.connectAndListen(config.network.server_ip); - // } if (client.init() == -1) { exit(EXIT_FAILURE); @@ -66,6 +62,8 @@ int main(int argc, char* argv[]) GLFWwindow* window = client.getWindow(); if (!window) exit(EXIT_FAILURE); + glfwSetWindowUserPointer(window, &client); + // Setup callbacks. set_callbacks(window, &client); // Setup OpenGL settings. From 27c91f1b52cde1c872ae445301a12d926fdbbeb9 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 18:43:02 -0700 Subject: [PATCH 36/92] scuffed caret behavior for text input :alien: --- include/client/client.hpp | 4 ++++ include/client/gui/widget/textinput.hpp | 7 +++++++ include/shared/utilities/time.hpp | 3 +++ src/client/client.cpp | 9 +++++++++ src/client/gui/widget/textinput.cpp | 13 +++++++++++++ src/shared/CMakeLists.txt | 1 + src/shared/utilities/time.cpp | 9 +++++++++ 7 files changed, 46 insertions(+) create mode 100644 include/shared/utilities/time.hpp create mode 100644 src/shared/utilities/time.cpp diff --git a/include/client/client.hpp b/include/client/client.hpp index fe393405..b2d58e98 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -50,6 +51,7 @@ class Client { static void mouseCallback(GLFWwindow* window, double xposIn, double yposIn); static void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods); static void charCallback(GLFWwindow* window, unsigned int codepoint); + static time_t getTimeOfLastKeystroke(); // Getter / Setters GLFWwindow* getWindow() { return window; } @@ -89,6 +91,8 @@ class Client { static float mouse_xpos; static float mouse_ypos; + static time_t time_of_last_keystroke; + SharedGameState gameState; GameConfig config; tcp::resolver resolver; diff --git a/include/client/gui/widget/textinput.hpp b/include/client/gui/widget/textinput.hpp index 3b69fdf5..fbfc337f 100644 --- a/include/client/gui/widget/textinput.hpp +++ b/include/client/gui/widget/textinput.hpp @@ -14,6 +14,11 @@ namespace gui { namespace gui::widget { +/** + * Widget to capture text input. + * NOTE: with the current implementation, there can only be one of these active in the GUI + * at one time. If there are multiple, they will both affect each other. + */ class TextInput : public Widget { public: using Ptr = std::unique_ptr; @@ -42,6 +47,8 @@ class TextInput : public Widget { void doHover(float x, float y) override; private: + static std::string prev_input; + widget::DynText::Ptr dyntext; gui::GUI* gui; }; diff --git a/include/shared/utilities/time.hpp b/include/shared/utilities/time.hpp new file mode 100644 index 00000000..fc13a7b2 --- /dev/null +++ b/include/shared/utilities/time.hpp @@ -0,0 +1,3 @@ +#pragma once + +long getMsSinceEpoch(); \ No newline at end of file diff --git a/src/client/client.cpp b/src/client/client.cpp index 93f591b8..1d524bfc 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -17,6 +17,7 @@ #include "shared/network/packet.hpp" #include "shared/utilities/config.hpp" #include "shared/utilities/root_path.hpp" +#include "shared/utilities/time.hpp" using namespace boost::asio::ip; using namespace std::chrono_literals; @@ -36,6 +37,8 @@ bool Client::is_left_mouse_down = false; float Client::mouse_xpos = 0.0f; float Client::mouse_ypos = 0.0f; +time_t Client::time_of_last_keystroke = 0; + using namespace gui; Client::Client(boost::asio::io_context& io_context, GameConfig config): @@ -303,6 +306,7 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, case GLFW_KEY_BACKSPACE: client->gui.captureBackspace(); + Client::time_of_last_keystroke = getMsSinceEpoch(); break; case GLFW_KEY_DOWN: @@ -402,4 +406,9 @@ void Client::charCallback(GLFWwindow* window, unsigned int codepoint) { Client* client = static_cast(glfwGetWindowUserPointer(window)); client->gui.captureKeystroke(static_cast(codepoint)); + Client::time_of_last_keystroke = getMsSinceEpoch(); +} + +time_t Client::getTimeOfLastKeystroke() { + return Client::time_of_last_keystroke; } \ No newline at end of file diff --git a/src/client/gui/widget/textinput.cpp b/src/client/gui/widget/textinput.cpp index 455abd4f..aab574e1 100644 --- a/src/client/gui/widget/textinput.cpp +++ b/src/client/gui/widget/textinput.cpp @@ -1,10 +1,16 @@ #include "client/gui/widget/textinput.hpp" #include "client/core.hpp" #include "client/gui/gui.hpp" +#include "client/client.hpp" +#include "shared/utilities/time.hpp" + #include +#include namespace gui::widget { +std::string TextInput::prev_input = ""; + TextInput::TextInput(glm::vec2 origin, std::string placeholder, gui::GUI* gui, @@ -22,6 +28,13 @@ TextInput::TextInput(glm::vec2 origin, text_to_display = captured_input; } + auto ms_since_epoch = getMsSinceEpoch(); + if (Client::getTimeOfLastKeystroke() + 1000 > ms_since_epoch || ms_since_epoch % 2000 > 1000) { + if (gui->shouldCaptureKeystrokes()) { + text_to_display += '|'; + } + } + this->dyntext = DynText::make(origin, text_to_display, fonts, options); this->dyntext->addOnClick([gui](widget::Handle handle) { gui->setCaptureKeystrokes(true); diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index 1ebd306c..f2cabe2a 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -9,6 +9,7 @@ set(FILES utilities/config.cpp utilities/rng.cpp utilities/root_path.cpp + utilities/time.cpp ) add_library(${LIB_NAME} STATIC ${FILES}) diff --git a/src/shared/utilities/time.cpp b/src/shared/utilities/time.cpp new file mode 100644 index 00000000..d78b8dd9 --- /dev/null +++ b/src/shared/utilities/time.cpp @@ -0,0 +1,9 @@ +#include "shared/utilities/time.hpp" + +#include + +long getMsSinceEpoch() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ).count(); +} From 6fc94e973aa70cdc38b039bfaf4bb62afb281d07 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 18:53:35 -0700 Subject: [PATCH 37/92] allow holding backspace for deletions --- src/client/client.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index 1d524bfc..b68ae219 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -204,7 +204,7 @@ void Client::createLobbyFinderGUI() { this->gui.addWidget(std::move(lobbies_flex)); this->gui.addWidget(widget::TextInput::make( - glm::vec2(0.0f, 0.0f), + glm::vec2(300.0f, 300.0f), "Enter a name", &this->gui, this->gui.getFonts(), @@ -296,7 +296,6 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, if (action == GLFW_PRESS) { switch (key) { case GLFW_KEY_ESCAPE: - // Close the window. This causes the program to also terminate. client->gui.setCaptureKeystrokes(false); break; @@ -384,6 +383,16 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, break; } } + + if (action == GLFW_REPEAT) { + if (key == GLFW_KEY_BACKSPACE) { + auto ms_since_epoch = getMsSinceEpoch(); + if (Client::time_of_last_keystroke + 100 < ms_since_epoch) { + Client::time_of_last_keystroke = ms_since_epoch; + client->gui.captureBackspace(); + } + } + } } void Client::mouseCallback(GLFWwindow* window, double xposIn, double yposIn) { @@ -394,7 +403,6 @@ void Client::mouseCallback(GLFWwindow* window, double xposIn, double yposIn) { void Client::mouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { if (button == GLFW_MOUSE_BUTTON_LEFT) { if (action == GLFW_PRESS) { - std::cout << mouse_xpos << ", " << mouse_ypos << "\n"; is_left_mouse_down = true; } else if (action == GLFW_RELEASE) { is_left_mouse_down = false; From f97c2d8998c4b587d84cc57ab6e9dc1a1c3f2c50 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Wed, 1 May 2024 19:03:41 -0700 Subject: [PATCH 38/92] remove lato because it doesn't fit fantasy theme --- fonts/Lato-Regular.ttf | Bin 75152 -> 0 bytes src/client/gui/font/font.cpp | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 fonts/Lato-Regular.ttf diff --git a/fonts/Lato-Regular.ttf b/fonts/Lato-Regular.ttf deleted file mode 100644 index bb2e8875a993f9c7d9e45d0eeffa839550cc6287..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75152 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 Date: Thu, 2 May 2024 10:12:26 -0700 Subject: [PATCH 39/92] fix lobby still broadcasting after server start --- src/client/client.cpp | 4 ++-- src/server/server.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index 3c40214a..fe440cf5 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -107,8 +107,6 @@ bool Client::init() { /* Make the window's context current */ glfwMakeContextCurrent(window); - // tell GLFW to capture our mouse - // glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); GLenum err = glewInit() ; if (GLEW_OK != err) { @@ -161,6 +159,8 @@ void Client::displayCallback() { } else if (this->gameState.phase == GamePhase::LOBBY) { this->createLobbyGUI(); } else if (this->gameState.phase == GamePhase::GAME) { + // tell GLFW to capture our mouse + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); this->draw(); } diff --git a/src/server/server.cpp b/src/server/server.cpp index 5fbcbc98..0c1e7030 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -152,6 +152,7 @@ std::chrono::milliseconds Server::doTick() { if (this->state.getLobbyPlayers().size() >= this->state.getLobbyMaxPlayers()) { this->state.setPhase(GamePhase::GAME); + this->lobby_broadcaster.stopBroadcasting(); } else { std::cout << "Only have " << this->state.getLobbyPlayers().size() << "/" << this->state.getLobbyMaxPlayers() << "\n"; } From cc0372d7660fb3f61cf1e1717bd92047de726773 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 10:36:39 -0700 Subject: [PATCH 40/92] in progress refactor --- include/client/client.hpp | 1 + include/client/gui/gui.hpp | 21 ++++++++- src/client/client.cpp | 62 +++---------------------- src/client/gui/gui.cpp | 92 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 114 insertions(+), 62 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index ddf78059..ca2569f9 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -79,6 +79,7 @@ class Client { GLFWwindow *window; GLuint cubeShaderProgram; + friend class gui::GUI; gui::GUI gui; Camera *cam; diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index ac7500f8..4f2f08a6 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -23,16 +23,25 @@ namespace gui { +enum class GUIState { + NONE, + TITLE_SCREEN, + LOBBY_BROWSER, + LOBBY, + GAME_HUD, + GAME_ESC_MENU +}; class GUI { public: - GUI(); + explicit GUI(Client* client); bool init(GLuint text_shader); void beginFrame(); + void layoutFrame(GUIState state); + void handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down); void renderFrame(); - void endFrame(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down); widget::Handle addWidget(widget::Widget::Ptr&& widget); std::unique_ptr removeWidget(widget::Handle handle); @@ -74,6 +83,14 @@ class GUI { bool capture_keystrokes; std::string keyboard_input; + + Client* client; + + void _layoutTitleScreen(); + void _layoutLobbyBrowser(); + void _layoutLobby(); + void _layoutGameHUD(); + void _layoutGameEscMenu() }; using namespace gui; diff --git a/src/client/client.cpp b/src/client/client.cpp index fe440cf5..f5c933d3 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -155,16 +155,18 @@ void Client::displayCallback() { this->gui.beginFrame(); if (this->gameState.phase == GamePhase::TITLE_SCREEN) { - this->createLobbyFinderGUI(); + this->gui.layoutFrame(gui::GUIState::LOBBY_BROWSER); } else if (this->gameState.phase == GamePhase::LOBBY) { - this->createLobbyGUI(); + this->gui.layoutFrame(gui::GUIState::LOBBY); } else if (this->gameState.phase == GamePhase::GAME) { + this->gui.layoutFrame(gui::GUIState::GAME_HUD); + // tell GLFW to capture our mouse glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); this->draw(); } - this->gui.endFrame(mouse_xpos, mouse_ypos, is_left_mouse_down); + this->gui.handleInputs(mouse_xpos, mouse_ypos, is_left_mouse_down); this->gui.renderFrame(); /* Poll for and process events */ @@ -173,60 +175,6 @@ void Client::displayCallback() { } void Client::createLobbyFinderGUI() { - this->gui.addWidget(widget::CenterText::make( - "Lobbies", - font::Font::MENU, - font::FontSizePx::LARGE, - font::FontColor::BLACK, - this->gui.getFonts(), - WINDOW_HEIGHT - font::FontSizePx::LARGE - )); - - auto lobbies_flex = widget::Flexbox::make( - glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), - glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 10.0f, - }); - - for (const auto& [ip, packet]: this->lobby_finder.getFoundLobbies()) { - std::stringstream ss; - ss << packet.lobby_name << " " << packet.slots_taken << "/" << packet.slots_avail + packet.slots_taken; - - auto entry = widget::DynText::make(ss.str(), - this->gui.getFonts(), widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::SMALL, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - }); - entry->addOnClick([ip, this](widget::Handle handle){ - std::cout << "Connecting to " << ip.address() << " ...\n"; - this->connectAndListen(ip.address().to_string()); - }); - entry->addOnHover([this](widget::Handle handle){ - auto widget = this->gui.borrowWidget(handle); - widget->changeColor(font::FontColor::BLUE); - }); - lobbies_flex->push(std::move(entry)); - } - - this->gui.addWidget(std::move(lobbies_flex)); - - this->gui.addWidget(widget::TextInput::make( - glm::vec2(300.0f, 300.0f), - "Enter a name", - &this->gui, - this->gui.getFonts(), - widget::DynText::Options { - .font = font::Font::TEXT, - .font_size = font::FontSizePx::SMALL, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - } - )); } void Client::createLobbyGUI() { diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 6685f177..dc4f8331 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -9,8 +9,8 @@ namespace gui { -GUI::GUI() { - +GUI::GUI(Client* client) { + this->client = client; } bool GUI::init(GLuint text_shader) @@ -39,6 +39,18 @@ void GUI::beginFrame() { std::swap(this->widgets, empty); } +void GUI::layoutFrame(GUIState state) { + switch (state) { + case GUIState::GAME_ESC_MENU: + break; + case GUIState::LOBBY_BROWSER: + break; + case GUIState::GAME_ESC_MENU: + break; + case GUIState::NONE: + break; + } +} void GUI::renderFrame() { // for text rendering @@ -55,7 +67,7 @@ void GUI::renderFrame() { glDisable(GL_BLEND); } -void GUI::endFrame(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down) { +void GUI::handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down) { if (is_left_mouse_down) { this->handleClick(mouse_xpos, mouse_ypos); is_left_mouse_down = false; @@ -127,4 +139,78 @@ std::shared_ptr GUI::getFonts() { return this->fonts; } + +void GUI::_layoutTitleScreen() { + +} + +void GUI::_layoutLobbyBrowser() { + this->addWidget(widget::CenterText::make( + "Lobbies", + font::Font::MENU, + font::FontSizePx::LARGE, + font::FontColor::BLACK, + this->fonts, + WINDOW_HEIGHT - font::FontSizePx::LARGE + )); + + auto lobbies_flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, + .padding = 10.0f, + }); + + for (const auto& [ip, packet]: client->lobby_finder.getFoundLobbies()) { + std::stringstream ss; + ss << packet.lobby_name << " " << packet.slots_taken << "/" << packet.slots_avail + packet.slots_taken; + + auto entry = widget::DynText::make(ss.str(), + this->fonts, widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::SMALL, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + }); + entry->addOnClick([ip, this](widget::Handle handle){ + std::cout << "Connecting to " << ip.address() << " ...\n"; + this->client->connectAndListen(ip.address().to_string()); + }); + entry->addOnHover([this](widget::Handle handle){ + auto widget = this->borrowWidget(handle); + widget->changeColor(font::FontColor::BLUE); + }); + lobbies_flex->push(std::move(entry)); + } + + this->addWidget(std::move(lobbies_flex)); + + this->addWidget(widget::TextInput::make( + glm::vec2(300.0f, 300.0f), + "Enter a name", + this, + fonts, + widget::DynText::Options { + .font = font::Font::TEXT, + .font_size = font::FontSizePx::SMALL, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + } + )); +} + +void GUI::_layoutLobby() { + +} + +void GUI::_layoutGameHUD() { + +} + +void GUI::_layoutGameEscMenu() { + +} + } From 7141e4a7f8c039419c9cd1614ed1cf621c669b87 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 11:49:37 -0700 Subject: [PATCH 41/92] refactor gui layout to be more in the gui class --- include/client/client.hpp | 5 +- include/client/gui/font/font.hpp | 3 +- include/client/gui/gui.hpp | 4 +- include/client/gui/widget/centertext.hpp | 2 +- src/client/client.cpp | 43 ++++---- src/client/gui/font/loader.cpp | 2 +- src/client/gui/gui.cpp | 132 ++++++++++++++++++----- src/client/gui/widget/centertext.cpp | 4 +- src/server/server.cpp | 3 +- 9 files changed, 143 insertions(+), 55 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index ca2569f9..995e3199 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -41,10 +41,6 @@ class Client { // Callbacks void displayCallback(); - // set up ui for a specific screen - void createLobbyFinderGUI(); - void createLobbyGUI(); - void idleCallback(boost::asio::io_context& context); void handleKeys(int eid, int keyType, bool keyHeld, bool *eventSent, glm::vec3 movement = glm::vec3(0.0f)); @@ -81,6 +77,7 @@ class Client { friend class gui::GUI; gui::GUI gui; + gui::GUIState gui_state; Camera *cam; // Flags diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index d4da741e..577d393b 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -15,7 +15,8 @@ enum class Font { enum FontSizePx { SMALL = 64, MEDIUM = 96, - LARGE = 128 + LARGE = 128, + BIG_YOSHI = 256 }; enum class FontColor { diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 4f2f08a6..5095e5d0 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -21,6 +21,8 @@ #include #include +class Client; + namespace gui { enum class GUIState { @@ -90,7 +92,7 @@ class GUI { void _layoutLobbyBrowser(); void _layoutLobby(); void _layoutGameHUD(); - void _layoutGameEscMenu() + void _layoutGameEscMenu(); }; using namespace gui; diff --git a/include/client/gui/widget/centertext.hpp b/include/client/gui/widget/centertext.hpp index 38a9afe5..f2283dba 100644 --- a/include/client/gui/widget/centertext.hpp +++ b/include/client/gui/widget/centertext.hpp @@ -7,7 +7,7 @@ namespace gui::widget { class CenterText { public: - static Flexbox::Ptr make( + static Widget::Ptr make( std::string text, font::Font font, font::FontSizePx size, diff --git a/src/client/client.cpp b/src/client/client.cpp index f5c933d3..d6ce8223 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -58,7 +58,8 @@ Client::Client(boost::asio::io_context& io_context, GameConfig config): config(config), gameState(GamePhase::TITLE_SCREEN, config), session(nullptr), - gui(), + gui(this), + gui_state(gui::GUIState::TITLE_SCREEN), lobby_finder(io_context, config) { cam = new Camera(); @@ -155,17 +156,13 @@ void Client::displayCallback() { this->gui.beginFrame(); if (this->gameState.phase == GamePhase::TITLE_SCREEN) { - this->gui.layoutFrame(gui::GUIState::LOBBY_BROWSER); + } else if (this->gameState.phase == GamePhase::LOBBY) { - this->gui.layoutFrame(gui::GUIState::LOBBY); } else if (this->gameState.phase == GamePhase::GAME) { - this->gui.layoutFrame(gui::GUIState::GAME_HUD); - - // tell GLFW to capture our mouse - glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); this->draw(); } + this->gui.layoutFrame(this->gui_state); this->gui.handleInputs(mouse_xpos, mouse_ypos, is_left_mouse_down); this->gui.renderFrame(); @@ -174,15 +171,16 @@ void Client::displayCallback() { glfwSwapBuffers(window); } -void Client::createLobbyFinderGUI() { -} - -void Client::createLobbyGUI() { - // auto title; -} - // Handle any updates void Client::idleCallback(boost::asio::io_context& context) { + if (this->session != nullptr) { + processServerInput(context); + } + + // If we aren't in the middle of the game then we shouldn't capture any movement info + // or send any movement related events + if (this->gui_state != GUIState::GAME_HUD) { return; } + std::optional jump = glm::vec3(0.0f); std::optional cam_movement = glm::vec3(0.0f); @@ -225,10 +223,6 @@ void Client::idleCallback(boost::asio::io_context& context) { sentCamMovement = cam_movement.value(); } } - - if (this->session != nullptr) { - processServerInput(context); - } } // Handles given key @@ -263,7 +257,13 @@ void Client::processServerInput(boost::asio::io_context& context) { for (Event event : this->session->getEvents()) { if (event.type == EventType::LoadGameState) { + GamePhase old_phase = this->gameState.phase; this->gameState = boost::get(event.data).state; + + // Change the UI to the game hud UI whenever we change into the GAME game phase + if (old_phase != GamePhase::GAME && this->gameState.phase == GamePhase::GAME) { + this->gui_state = GUIState::GAME_HUD; + } } } } @@ -304,6 +304,13 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, if (action == GLFW_PRESS) { switch (key) { case GLFW_KEY_ESCAPE: + if (client->gameState.phase == GamePhase::GAME) { + if (client->gui_state == GUIState::GAME_ESC_MENU) { + client->gui_state = GUIState::GAME_HUD; + } else if (client->gui_state == GUIState::GAME_HUD) { + client->gui_state = GUIState::GAME_ESC_MENU; + } + } client->gui.setCaptureKeystrokes(false); break; diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index e0158195..1e275242 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -56,7 +56,7 @@ bool Loader::_loadFont(Font font) { return false; } - for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE}) { + for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE, FontSizePx::BIG_YOSHI}) { FT_Set_Pixel_Sizes(face, 0, font_size); std::unordered_map characters; for (unsigned char c = 0; c < 128; c++) { diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index dc4f8331..20721a21 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -39,19 +39,6 @@ void GUI::beginFrame() { std::swap(this->widgets, empty); } -void GUI::layoutFrame(GUIState state) { - switch (state) { - case GUIState::GAME_ESC_MENU: - break; - case GUIState::LOBBY_BROWSER: - break; - case GUIState::GAME_ESC_MENU: - break; - case GUIState::NONE: - break; - } -} - void GUI::renderFrame() { // for text rendering glEnable(GL_BLEND); @@ -139,9 +126,73 @@ std::shared_ptr GUI::getFonts() { return this->fonts; } +void GUI::layoutFrame(GUIState state) { + switch (state) { + case GUIState::TITLE_SCREEN: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutTitleScreen(); + break; + case GUIState::GAME_ESC_MENU: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutGameEscMenu(); + break; + case GUIState::LOBBY_BROWSER: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutLobbyBrowser(); + break; + case GUIState::GAME_HUD: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + this->_layoutGameHUD(); + break; + case GUIState::LOBBY: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutLobby(); + break; + case GUIState::NONE: + break; + } +} + void GUI::_layoutTitleScreen() { + this->addWidget(widget::CenterText::make( + "Arcana", + font::Font::MENU, + font::FontSizePx::BIG_YOSHI, + font::FontColor::RED, + fonts, + FRAC_WINDOW_HEIGHT(2, 3) + )); + + auto start_text = widget::DynText::make( + "Start Game", + fonts, + widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::MEDIUM, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f, + } + ); + start_text->addOnHover([this](widget::Handle handle) { + auto widget = this->borrowWidget(handle); + widget->changeColor(font::FontColor::RED); + }); + start_text->addOnClick([this](widget::Handle handle) { + client->gui_state = GUIState::LOBBY_BROWSER; + }); + auto start_flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, + .padding = 0.0f, + } + ); + start_flex->push(std::move(start_text)); + this->addWidget(std::move(start_flex)); } void GUI::_layoutLobbyBrowser() { @@ -177,28 +228,29 @@ void GUI::_layoutLobbyBrowser() { entry->addOnClick([ip, this](widget::Handle handle){ std::cout << "Connecting to " << ip.address() << " ...\n"; this->client->connectAndListen(ip.address().to_string()); + this->client->gui_state = GUIState::LOBBY; }); entry->addOnHover([this](widget::Handle handle){ auto widget = this->borrowWidget(handle); - widget->changeColor(font::FontColor::BLUE); + widget->changeColor(font::FontColor::RED); }); lobbies_flex->push(std::move(entry)); } this->addWidget(std::move(lobbies_flex)); - this->addWidget(widget::TextInput::make( - glm::vec2(300.0f, 300.0f), - "Enter a name", - this, - fonts, - widget::DynText::Options { - .font = font::Font::TEXT, - .font_size = font::FontSizePx::SMALL, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - } - )); + // this->addWidget(widget::TextInput::make( + // glm::vec2(300.0f, 300.0f), + // "Enter a name", + // this, + // fonts, + // widget::DynText::Options { + // .font = font::Font::TEXT, + // .font_size = font::FontSizePx::SMALL, + // .color = font::getRGB(font::FontColor::BLACK), + // .scale = 1.0f + // } + // )); } void GUI::_layoutLobby() { @@ -210,7 +262,35 @@ void GUI::_layoutGameHUD() { } void GUI::_layoutGameEscMenu() { + auto exit_game_txt = widget::DynText::make( + "Exit Game", + fonts, + widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::MEDIUM, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f, + } + ); + exit_game_txt->addOnHover([this](widget::Handle handle) { + auto widget = this->borrowWidget(handle); + widget->changeColor(font::FontColor::RED); + }); + exit_game_txt->addOnClick([this](widget::Handle handle) { + glfwDestroyWindow(this->client->getWindow()); + }); + auto flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 2)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, + .padding = 0.0f, + } + ); + flex->push(std::move(exit_game_txt)); + this->addWidget(std::move(flex)); } } diff --git a/src/client/gui/widget/centertext.cpp b/src/client/gui/widget/centertext.cpp index 4a192f78..95c6d6f6 100644 --- a/src/client/gui/widget/centertext.cpp +++ b/src/client/gui/widget/centertext.cpp @@ -3,7 +3,7 @@ namespace gui::widget { -Flexbox::Ptr CenterText::make( +Widget::Ptr CenterText::make( std::string text, font::Font font, font::FontSizePx size, @@ -25,7 +25,7 @@ Flexbox::Ptr CenterText::make( widget::DynText::Options { .font = font, .font_size = size, - .color = font::getRGB(font::FontColor::BLACK), + .color = font::getRGB(color), .scale = 1.0f }); flex->push(std::move(title)); diff --git a/src/server/server.cpp b/src/server/server.cpp index 0c1e7030..af3e2dba 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -152,7 +152,8 @@ std::chrono::milliseconds Server::doTick() { if (this->state.getLobbyPlayers().size() >= this->state.getLobbyMaxPlayers()) { this->state.setPhase(GamePhase::GAME); - this->lobby_broadcaster.stopBroadcasting(); + // TODO: figure out how to selectively broadcast to only the players that were already in the lobby + // this->lobby_broadcaster.stopBroadcasting(); } else { std::cout << "Only have " << this->state.getLobbyPlayers().size() << "/" << this->state.getLobbyMaxPlayers() << "\n"; } From 6c40d5a7c528152aa434d760f1ed27b03e452d25 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 12:21:11 -0700 Subject: [PATCH 42/92] make lobby name serializable and sent to player --- include/client/client.hpp | 4 ++-- include/shared/game/sharedgamestate.hpp | 21 ++++++++++++++++---- include/shared/utilities/serialize_macro.hpp | 1 + src/client/gui/gui.cpp | 11 +++++++++- src/server/game/servergamestate.cpp | 2 ++ 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index 995e3199..bf869cba 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -28,8 +28,8 @@ #define WINDOW_HEIGHT 1000 // position something a "frac" of the way across the screen -// e.g. WIDTH_FRAC(4) -> a fourth of the way from the left -// HEIGHT_FRAC(3) -> a third of the way from the bottom +// e.g. WIDTH_FRAC(1, 4) -> a fourth of the way from the left +// HEIGHT_FRAC(2, 3) -> two thirds of the way from the bottom #define FRAC_WINDOW_WIDTH(num, denom) WINDOW_WIDTH * static_cast(num) / static_cast(denom) #define FRAC_WINDOW_HEIGHT(num, denom) WINDOW_HEIGHT * static_cast(num) / static_cast(denom) diff --git a/include/shared/game/sharedgamestate.hpp b/include/shared/game/sharedgamestate.hpp index f4f9a868..527e6c6b 100644 --- a/include/shared/game/sharedgamestate.hpp +++ b/include/shared/game/sharedgamestate.hpp @@ -1,5 +1,11 @@ #pragma once +#include +#include +#include +#include +#include + //#include "server/game/servergamestate.hpp" #include "shared/game/sharedobject.hpp" #include "shared/utilities/smartvector.hpp" @@ -7,9 +13,6 @@ #include "shared/utilities/config.hpp" #include "server/game/constants.hpp" -#include -#include -#include enum class GamePhase { TITLE_SCREEN, @@ -25,6 +28,11 @@ struct Lobby { // could eventually be EntityID -> Player (where Player derives from // Object)? + /** + * @brief name of the lobby as set by the server + */ + std::string name; + /** * @brief A hash table that maps from player's EntityID to their names. */ @@ -35,6 +43,10 @@ struct Lobby { */ int max_players; + DEF_SERIALIZE(Archive& ar, unsigned int version) { + ar & name & players & max_players; + } + // TODO: Add a player role listing? I.e., which player is playing which // character and which player is playing as the Dungeon Master? }; @@ -73,9 +85,10 @@ struct SharedGameState { this->timestep = FIRST_TIMESTEP; this->timestep_length = config.game.timestep_length_ms; this->lobby.max_players = config.server.max_players; + this->lobby.name = config.server.lobby_name; } DEF_SERIALIZE(Archive& ar, const unsigned int version) { - ar & phase& lobby.max_players & lobby.players & objects; + ar & phase & lobby & objects; } }; \ No newline at end of file diff --git a/include/shared/utilities/serialize_macro.hpp b/include/shared/utilities/serialize_macro.hpp index ddd269cc..9c9863f8 100644 --- a/include/shared/utilities/serialize_macro.hpp +++ b/include/shared/utilities/serialize_macro.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 20721a21..6465790e 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -5,6 +5,7 @@ #include #include "shared/utilities/rng.hpp" #include "client/client.hpp" +#include "shared/game/sharedgamestate.hpp" namespace gui { @@ -254,7 +255,15 @@ void GUI::_layoutLobbyBrowser() { } void GUI::_layoutLobby() { - + auto lobby_title = widget::CenterText::make( + this->client->gameState.lobby.name, + font::Font::MENU, + font::FontSizePx::LARGE, + font::FontColor::BLACK, + this->fonts, + WINDOW_HEIGHT - font::FontSizePx::LARGE + ); + this->addWidget(std::move(lobby_title)); } void GUI::_layoutGameHUD() { diff --git a/src/server/game/servergamestate.cpp b/src/server/game/servergamestate.cpp index e34e06a8..55916f07 100644 --- a/src/server/game/servergamestate.cpp +++ b/src/server/game/servergamestate.cpp @@ -8,6 +8,7 @@ ServerGameState::ServerGameState(GamePhase start_phase, GameConfig config) { this->timestep = FIRST_TIMESTEP; this->timestep_length = config.game.timestep_length_ms; this->lobby.max_players = config.server.max_players; + this->lobby.name = config.server.lobby_name; } ServerGameState::ServerGameState(GamePhase start_phase) { @@ -15,6 +16,7 @@ ServerGameState::ServerGameState(GamePhase start_phase) { this->timestep = FIRST_TIMESTEP; this->timestep_length = TIMESTEP_LEN; this->lobby.max_players = MAX_PLAYERS; + this->lobby.name = "Unnamed Lobby"; } ServerGameState::ServerGameState() : ServerGameState(GamePhase::LOBBY) {} From e65a9f552278617c60f2a015d09ac833b1948868 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 15:00:35 -0700 Subject: [PATCH 43/92] make lobby broadcast say accurate player info --- config.json | 2 +- include/server/game/servergamestate.hpp | 5 +++ src/client/gui/gui.cpp | 44 +++++++++++++++++++++++++ src/server/game/servergamestate.cpp | 4 +++ src/server/server.cpp | 6 ++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index b3e98048..004bf7e9 100644 --- a/config.json +++ b/config.json @@ -9,7 +9,7 @@ "server": { "lobby_name": "Tyler's Lobby", "lobby_broadcast": true, - "max_players": 1 + "max_players": 2 }, "client": { "default_name": "Tyler", diff --git a/include/server/game/servergamestate.hpp b/include/server/game/servergamestate.hpp index b49a030c..45069c76 100644 --- a/include/server/game/servergamestate.hpp +++ b/include/server/game/servergamestate.hpp @@ -118,6 +118,11 @@ class ServerGameState { * Getter for the mapping between entity ID and player name in the lobby */ const std::unordered_map& getLobbyPlayers() const; + + /** + * Getter for the lobby name + */ + const std::string& getLobbyName() const; /** * Returns how many max players can be in the lobby, based on the config option */ diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 6465790e..4d260f89 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -264,6 +264,50 @@ void GUI::_layoutLobby() { WINDOW_HEIGHT - font::FontSizePx::LARGE ); this->addWidget(std::move(lobby_title)); + std::stringstream ss; + ss << this->client->gameState.lobby.players.size() << " / " << this->client->gameState.lobby.max_players; + auto player_count = widget::CenterText::make( + ss.str(), + font::Font::MENU, + font::FontSizePx::MEDIUM, + font::FontColor::BLACK, + this->fonts, + WINDOW_HEIGHT - (2 * font::FontSizePx::LARGE) - 10.0f + ); + this->addWidget(std::move(player_count)); + + auto players_flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 5)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, + .padding = 10.0f + } + ); + for (const auto& [_eid, player_name] : this->client->gameState.lobby.players) { + players_flex->push(widget::DynText::make( + player_name, + this->fonts, + widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::MEDIUM, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + } + )); + } + this->addWidget(std::move(players_flex)); + + auto waiting_msg = widget::CenterText::make( + "Waiting for players...", + font::Font::MENU, + font::FontSizePx::MEDIUM, + font::FontColor::GRAY, + this->fonts, + 30.0f + ); + this->addWidget(std::move(waiting_msg)); } void GUI::_layoutGameHUD() { diff --git a/src/server/game/servergamestate.cpp b/src/server/game/servergamestate.cpp index 55916f07..950a9d33 100644 --- a/src/server/game/servergamestate.cpp +++ b/src/server/game/servergamestate.cpp @@ -196,6 +196,10 @@ const std::unordered_map& ServerGameState::getLobbyPlayer return this->lobby.players; } +const std::string& ServerGameState::getLobbyName() const { + return this->lobby.name; +} + int ServerGameState::getLobbyMaxPlayers() const { return this->lobby.max_players; } diff --git a/src/server/server.cpp b/src/server/server.cpp index af3e2dba..1b51092e 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -158,6 +158,12 @@ std::chrono::milliseconds Server::doTick() { std::cout << "Only have " << this->state.getLobbyPlayers().size() << "/" << this->state.getLobbyMaxPlayers() << "\n"; } + this->lobby_broadcaster.setLobbyInfo(ServerLobbyBroadcastPacket { + .lobby_name = this->state.getLobbyName(), + .slots_taken = static_cast(this->state.getLobbyPlayers().size()), + .slots_avail = this->state.getLobbyMaxPlayers() - static_cast(this->state.getLobbyPlayers().size()), + }); + sendUpdateToAllClients(Event(this->world_eid, EventType::LoadGameState, LoadGameStateEvent(this->state.generateSharedGameState()))); // Tell each client the current lobby status From 53ccd2d7de093d982f57e7a889d43a0c6709299b Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 15:13:54 -0700 Subject: [PATCH 44/92] minor refactor for lobby broadcast continued --- config.json | 2 +- include/server/game/servergamestate.hpp | 13 +------------ include/server/lobbybroadcaster.hpp | 2 +- include/shared/game/sharedgamestate.hpp | 5 +---- src/server/game/servergamestate.cpp | 12 ++---------- src/server/lobbybroadcaster.cpp | 9 +++++++-- src/server/server.cpp | 13 +++++-------- 7 files changed, 18 insertions(+), 38 deletions(-) diff --git a/config.json b/config.json index 004bf7e9..b3e98048 100644 --- a/config.json +++ b/config.json @@ -9,7 +9,7 @@ "server": { "lobby_name": "Tyler's Lobby", "lobby_broadcast": true, - "max_players": 2 + "max_players": 1 }, "client": { "default_name": "Tyler", diff --git a/include/server/game/servergamestate.hpp b/include/server/game/servergamestate.hpp index 45069c76..88eaa49a 100644 --- a/include/server/game/servergamestate.hpp +++ b/include/server/game/servergamestate.hpp @@ -114,19 +114,8 @@ class ServerGameState { * Removes a player from the lobby with the specified id. */ void removePlayerFromLobby(EntityID id); - /** - * Getter for the mapping between entity ID and player name in the lobby - */ - const std::unordered_map& getLobbyPlayers() const; - /** - * Getter for the lobby name - */ - const std::string& getLobbyName() const; - /** - * Returns how many max players can be in the lobby, based on the config option - */ - int getLobbyMaxPlayers() const; + const Lobby& getLobby() const; /* Debugger Methods */ diff --git a/include/server/lobbybroadcaster.hpp b/include/server/lobbybroadcaster.hpp index 3da9e9db..66b9e9bb 100644 --- a/include/server/lobbybroadcaster.hpp +++ b/include/server/lobbybroadcaster.hpp @@ -37,7 +37,7 @@ class LobbyBroadcaster { * Tell the broadcaster the current state of the lobby so it can broadcast * updated messages. */ - void setLobbyInfo(const ServerLobbyBroadcastPacket& bcast_info); + void setLobbyInfo(const Lobby& bcast_info); /** * Tells the LobbyBroadcaster to stop advertising that there is a lobby open. diff --git a/include/shared/game/sharedgamestate.hpp b/include/shared/game/sharedgamestate.hpp index 527e6c6b..1d710545 100644 --- a/include/shared/game/sharedgamestate.hpp +++ b/include/shared/game/sharedgamestate.hpp @@ -24,10 +24,6 @@ enum class GamePhase { * @brief Information about the current lobby of players. */ struct Lobby { - // TODO: Perhaps instead of a mapping from EntityID -> string, the mapping - // could eventually be EntityID -> Player (where Player derives from - // Object)? - /** * @brief name of the lobby as set by the server */ @@ -43,6 +39,7 @@ struct Lobby { */ int max_players; + DEF_SERIALIZE(Archive& ar, unsigned int version) { ar & name & players & max_players; } diff --git a/src/server/game/servergamestate.cpp b/src/server/game/servergamestate.cpp index 950a9d33..70632bd6 100644 --- a/src/server/game/servergamestate.cpp +++ b/src/server/game/servergamestate.cpp @@ -192,16 +192,8 @@ void ServerGameState::removePlayerFromLobby(EntityID id) { this->lobby.players.erase(id); } -const std::unordered_map& ServerGameState::getLobbyPlayers() const { - return this->lobby.players; -} - -const std::string& ServerGameState::getLobbyName() const { - return this->lobby.name; -} - -int ServerGameState::getLobbyMaxPlayers() const { - return this->lobby.max_players; +const Lobby& ServerGameState::getLobby() const { + return this->lobby; } std::string ServerGameState::to_string() { diff --git a/src/server/lobbybroadcaster.cpp b/src/server/lobbybroadcaster.cpp index 1ae10341..76da4cb1 100644 --- a/src/server/lobbybroadcaster.cpp +++ b/src/server/lobbybroadcaster.cpp @@ -5,6 +5,7 @@ #include #include +#include "shared/game/sharedgamestate.hpp" #include "shared/network/constants.hpp" #include "shared/network/packet.hpp" #include "shared/utilities/config.hpp" @@ -33,9 +34,13 @@ void LobbyBroadcaster::startBroadcasting(const ServerLobbyBroadcastPacket& bcast } } -void LobbyBroadcaster::setLobbyInfo(const ServerLobbyBroadcastPacket& bcast_info) { +void LobbyBroadcaster::setLobbyInfo(const Lobby& lobby_info) { std::unique_lock lock(this->mut); - this->bcast_info = bcast_info; + this->bcast_info = ServerLobbyBroadcastPacket { + .lobby_name = lobby_info.name, + .slots_taken = static_cast(lobby_info.players.size()), + .slots_avail = static_cast(lobby_info.max_players - lobby_info.players.size()) + }; } void LobbyBroadcaster::stopBroadcasting() { diff --git a/src/server/server.cpp b/src/server/server.cpp index 1b51092e..1362be7c 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -150,24 +150,21 @@ std::chrono::milliseconds Server::doTick() { } } - if (this->state.getLobbyPlayers().size() >= this->state.getLobbyMaxPlayers()) { + if (this->state.getLobby().players.size() >= this->state.getLobby().max_players) { this->state.setPhase(GamePhase::GAME); // TODO: figure out how to selectively broadcast to only the players that were already in the lobby // this->lobby_broadcaster.stopBroadcasting(); } else { - std::cout << "Only have " << this->state.getLobbyPlayers().size() << "/" << this->state.getLobbyMaxPlayers() << "\n"; + std::cout << "Only have " << this->state.getLobby().players.size() + << "/" << this->state.getLobby().max_players << "\n"; } - this->lobby_broadcaster.setLobbyInfo(ServerLobbyBroadcastPacket { - .lobby_name = this->state.getLobbyName(), - .slots_taken = static_cast(this->state.getLobbyPlayers().size()), - .slots_avail = this->state.getLobbyMaxPlayers() - static_cast(this->state.getLobbyPlayers().size()), - }); + this->lobby_broadcaster.setLobbyInfo(this->state.getLobby()); sendUpdateToAllClients(Event(this->world_eid, EventType::LoadGameState, LoadGameStateEvent(this->state.generateSharedGameState()))); // Tell each client the current lobby status - std::cout << "waiting for " << this->state.getLobbyMaxPlayers() << " players" << std::endl; + std::cout << "waiting for " << this->state.getLobby().max_players << " players" << std::endl; break; case GamePhase::GAME: { From 0c7adf7cc2190e769ea09a9d0300d7e0e46e292b Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 15:32:01 -0700 Subject: [PATCH 45/92] add keyboard input for player name --- config.json | 6 +++--- include/client/gui/gui.hpp | 1 + src/client/client.cpp | 7 ++++++- src/client/gui/gui.cpp | 28 ++++++++++++++++------------ src/client/gui/widget/textinput.cpp | 4 ++++ 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/config.json b/config.json index b3e98048..656af412 100644 --- a/config.json +++ b/config.json @@ -7,12 +7,12 @@ "server_port": 2355 }, "server": { - "lobby_name": "Tyler's Lobby", + "lobby_name": "Unnamed Lobby", "lobby_broadcast": true, - "max_players": 1 + "max_players": 2 }, "client": { - "default_name": "Tyler", + "default_name": "John Doe", "lobby_discovery": true } } \ No newline at end of file diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 5095e5d0..4e0da16a 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -52,6 +52,7 @@ class GUI { void setCaptureKeystrokes(bool should_capture); void captureKeystroke(char c); void captureBackspace(); + void clearCapturedKeyboardInput(); std::string getCapturedKeyboardInput() const; template diff --git a/src/client/client.cpp b/src/client/client.cpp index d6ce8223..2201825e 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -76,8 +76,13 @@ void Client::connectAndListen(std::string ip_addr) { this->session->connectTo(this->endpoints); + auto name = this->gui.getCapturedKeyboardInput(); + if (name == "") { + name = config.client.default_name; + } + auto packet = PackagedPacket::make_shared(PacketType::ClientDeclareInfo, - ClientDeclareInfoPacket { .player_name = config.client.default_name }); + ClientDeclareInfoPacket { .player_name = name }); this->session->sendPacketAsync(packet); diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 4d260f89..c7799f47 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -103,6 +103,10 @@ void GUI::setCaptureKeystrokes(bool should_capture) { this->capture_keystrokes = should_capture; } +void GUI::clearCapturedKeyboardInput() { + this->keyboard_input = ""; +} + // TODO: reduce copied code between these two functions void GUI::handleClick(float x, float y) { @@ -240,18 +244,18 @@ void GUI::_layoutLobbyBrowser() { this->addWidget(std::move(lobbies_flex)); - // this->addWidget(widget::TextInput::make( - // glm::vec2(300.0f, 300.0f), - // "Enter a name", - // this, - // fonts, - // widget::DynText::Options { - // .font = font::Font::TEXT, - // .font_size = font::FontSizePx::SMALL, - // .color = font::getRGB(font::FontColor::BLACK), - // .scale = 1.0f - // } - // )); + this->addWidget(widget::TextInput::make( + glm::vec2(FRAC_WINDOW_WIDTH(2, 5), FRAC_WINDOW_HEIGHT(1, 6)), + "Enter a name", + this, + fonts, + widget::DynText::Options { + .font = font::Font::TEXT, + .font_size = font::FontSizePx::SMALL, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + } + )); } void GUI::_layoutLobby() { diff --git a/src/client/gui/widget/textinput.cpp b/src/client/gui/widget/textinput.cpp index aab574e1..94ececec 100644 --- a/src/client/gui/widget/textinput.cpp +++ b/src/client/gui/widget/textinput.cpp @@ -39,6 +39,10 @@ TextInput::TextInput(glm::vec2 origin, this->dyntext->addOnClick([gui](widget::Handle handle) { gui->setCaptureKeystrokes(true); }); + + auto [width, height] = this->dyntext->getSize(); + this->width = width; + this->height = height; } TextInput::TextInput(glm::vec2 origin, From 93bbd1488b04672d98e750aa879e8695275c5c8d Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 15:45:25 -0700 Subject: [PATCH 46/92] add message in lobby browser if no lobby found --- src/client/gui/gui.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index c7799f47..6537ca86 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -242,6 +242,19 @@ void GUI::_layoutLobbyBrowser() { lobbies_flex->push(std::move(entry)); } + if (client->lobby_finder.getFoundLobbies().empty()) { + lobbies_flex->push(widget::DynText::make( + "No lobbies found...", + this->fonts, + widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::SMALL, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f + } + )); + } + this->addWidget(std::move(lobbies_flex)); this->addWidget(widget::TextInput::make( From 61b1574cc5392efd8fad09bbf41e63604461f0df Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 16:35:43 -0700 Subject: [PATCH 47/92] start documentation --- config.json | 2 +- include/client/gui/gui.hpp | 279 ++++++++++++++++++++++++++++++++++--- src/client/gui/gui.cpp | 38 +++-- 3 files changed, 277 insertions(+), 42 deletions(-) diff --git a/config.json b/config.json index 656af412..9d33b6fc 100644 --- a/config.json +++ b/config.json @@ -9,7 +9,7 @@ "server": { "lobby_name": "Unnamed Lobby", "lobby_broadcast": true, - "max_players": 2 + "max_players": 1 }, "client": { "default_name": "John Doe", diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 4e0da16a..d6f6f4fc 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -1,8 +1,5 @@ #pragma once -// #include "client/core.hpp" - - // Include all gui headers so everyone else just needs to include this file #include "client/gui/widget/options.hpp" #include "client/gui/widget/type.hpp" @@ -25,6 +22,11 @@ class Client; namespace gui { +/** + * Enumeration for all of the different "screens" that can be rendered by the GUI. The GUI class + * itself doesn't contain any internal state related to any specific state, but instead takes + * a GUIState as input to determine what should be rendered to the screen. + */ enum class GUIState { NONE, TITLE_SCREEN, @@ -34,27 +36,154 @@ enum class GUIState { GAME_ESC_MENU }; +/** + * Class which wraps around all of the GUI elements that should be rendered to the screen. + * + * The GUI can essentially be thought of as a collection of "Widgets" which all have "Handles" (or IDs). + * Each widget has all of the logic it needs to know its size, location, and how to render it. + * The GUI class acts as a container for all of these widgets and provides a helpful abstraction layer + * allowing the rest of the code to think in terms of "GUIState", since you can just tell the GUI + * to render a specific state, and it will do so. + * + * Here be Dragons + */ class GUI { public: + /// ========================================================================= + /// + /// These are the functions that need to be called to setup a GUI object. Doing anything + /// else before calling these will probably cause a crash. + /// + /** + * @brief Stores the client pointer as a data member. + * + * Constructor for a GUI. This really does nothing except store a pointer to the Client object + * so that the GUI functions can easily access Client data members, due to the friend relationship + * of Client -> GUI. + * + * @param client Pointer to the client object. Note that GUI is a friend class to client, so GUI can + * access private client data members for ease of use. + */ explicit GUI(Client* client); - + /** + * @brief Initializes all of the necessary file loading for all of the GUI elements. + * Currently this is mainly the image loading and the font loading. + * + * @param text_shader Shader to use for text rendering + */ bool init(GLuint text_shader); + /// ================================================================================ + /// ===================================================================== + /// + /// These series of functions are what you actually use to do the displaying and + /// rendering of the GUI. They should be called in the order they are listed in + /// this file. + /// + /** + * @brief Wipes all of the previous frame's state + * + * Function that should be called at the beginning of a frame. Essentially it wipes + * all of the previous frame's widget data. + */ void beginFrame(); + /** + * @brief Adds widgets to the layout depending on the specified GUI state. + * + * @param state Current State of the GUI that should be rendered. This essentially + * corresponds to a specific "screen" that should be displayed + */ void layoutFrame(GUIState state); + /** + * @brief Takes the current relevant input information and alters the GUI based on the + * how the inputs interact with all of the handlers assigned to the Widgets. + * + * NOTE: this must be called after adding all of the widgets to the screen, otherwise + * no event handling will work on those widgets added after calling this function. + * + * NOTE: currently this function takes a reference to the mouse down boolean because + * if it "captures" a mouse click then it sets that boolean to false, to prevent continued + * event triggers if the mouse is held down. In other words, we want "onClick" events + * to happen only on the initial click, not continually while the mouse is held down. + * + * NOTE: both of the x and y coordinates of the mouse passed into this function are in + * the GLFW coordinate space. In GLFW coordinates, the top left corner of the screen is + * (x=0,y=0). However, in the GUI world all of our coordinates are based off of + * (x=0,y=0) being in the bottom left corner. This is the only GUI function that takes + * GLFW coordinates, + * + * @param mouse_xpos x position of the mouse, in GLFW Coordinates + * @param mouse_ypos y position of the mouse, in GLFW Coordinates + * @param is_left_mouse_down reference to flag which stores whether or not the left mouse + * is down. Note: this is a reference so that if a click is captured it can toggle the + * mouse down flag to false, so that click doesn't get "double counted" in subsequent + * frames. + */ void handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down); + /** + * Renders the current state of the GUI to the screen, based on all of the widgets + * that have been added and any changes caused by inputs. + */ void renderFrame(); + /// ============================================================================== + /// ========================================================= + /// + /// These functions are concerned with adding, removing, and getting widgets to/from + /// the GUI. + /// + /** + * @brief Adds the specified widget to the GUI. + * + * NOTE: the widget that is being passed in is of the type Widget::Ptr, which is + * a typedef for a std::unique_ptr. To easily get a unique_ptr for a Widget, + * you can use the corresponding static `make` function provided by each widget + * implementation. + * + * @param widget Widget to add + * @returns A handle to the widget that was added, so it can be modifed/removed later + */ widget::Handle addWidget(widget::Widget::Ptr&& widget); - std::unique_ptr removeWidget(widget::Handle handle); - - bool shouldCaptureKeystrokes() const; - void setCaptureKeystrokes(bool should_capture); - void captureKeystroke(char c); - void captureBackspace(); - void clearCapturedKeyboardInput(); - std::string getCapturedKeyboardInput() const; - + /** + * @brief Removes the specified widget from the GUI + * + * @param handle Handle to the widget that you want to remove + * @returns The widget that you removed, now isolated from the GUI. + */ + widget::Widget::Ptr removeWidget(widget::Handle handle); + /** + * @brief Borrows the specified widget from the GUI. + * This is especially useful inside of callback functions for widgets as you will + * already have the handle needed to initiate a borrow. + * + * NOTE: This function essentially returns the raw pointer for the specified widget. + * Technically you could do unspeakable things to this pointer, but as we are all + * bound by societal conventions so shall you too not break this taboo. + * + * NOTE: You should not attempt to save this pointer elsewhere. This pointer can be thought + * of as a "temporary" reference to the Widget, which will go out of scope + * between the time of calling and the next frame. + * + * NOTE: The template argument you specify essentially performs a dynamic cast on the + * returned pointer. You should not make your template argument a pointer itself, since + * the function return type already adds a pointer to it. You should only specify a + * specific kind of Widget if you are confident in what kind of widget it is (i.e. in + * an event handler). Otherwise, it is safe to just specify a widget::Widget as the + * template argument + * + * Example use: + * // Precondition: We know that `handle` is a valid handle to a widget::DynText + * auto widget = gui.borrowWidget(handle); + * // widget is of type `widget::DynText*` + * + * If you mess up and pass in the wrong template argument, then the pointer will + * end up being nullptr. + * + * @tparam Type of the widget that you are trying to borrow. + * @param handle Handle to the widget you want to borrow + * @returns Pointer to the widget specified by the handle, casted to + * be a pointer of the specified template type. + */ template W* borrowWidget(widget::Handle handle) { for (const auto& [_, widget] : this->widgets) { @@ -68,13 +197,76 @@ class GUI { << "and means we are doing something very very bad." << std::endl; std::exit(1); } + /// ============================================================================== - void handleClick(float x, float y); - void handleHover(float x, float y); - - void clearAll(); + /// ============================================================== + /// + /// These functions are concerned with keyboard input for the TextInput widget. + /// Because of the way this is implemented, there can only be one TextInput widget + /// on the screen at one time. If there are more, they will end up sharing all of + /// the inputted text. + /// + /** + * @brief Checks to see if the GUI is currently capturing keyboard input. + * + * NOTE: This shouldn't be called as a precondition for `captureKeystroke` because + * `captureKeystroke` will internally also check whether or not the GUI is + * capturing keystrokes. + * + * @returns true if the GUI is capturing keyboard input, false otherwise + */ + bool shouldCaptureKeystrokes() const; + /** + * @brief Toggles whether or not the GUI should be capturing keyboard input + * + * @param should_capture Whether or not the GUI should be capturing keyboard input + */ + void setCaptureKeystrokes(bool should_capture); + /** + * @brief Captures a keystroke as an ASCII char. + * + * Takes a keystroke as captured by the client's GLFW code, and adds it into the GUI. + * Internally, this will check that the ASCII value is between [32, 126], which is the + * set of meaningful ASCII values that we care about. + * + * NOTE: this function internally checks whether or not the GUI is capturing keyboard + * input. If the GUI is not currently capturing keyboard input, then this will do + * nothing. This means that you don't need to check `shouldCaptureKeystrokes` before + * calling this function. + * + * NOTE: This function does not handle backspaces. To handle backspaces, the + * `captureBackspace` function should be used instead. + * + * @param c ASCII char to capture + */ + void captureKeystroke(char c); + /** + * @brief If the GUI is capturing backspaces, then records a backspace press. This deletes + * the most recently captured keystroke in the GUI. + */ + void captureBackspace(); + /** + * @brief Wipes all of the captured keyboard input from the internal state. + */ + void clearCapturedKeyboardInput(); + /** + * @brief Returns all of the captured keyboard input without clearing it. + * + * @returns all of the captured keyboard input as an std::string + */ + std::string getCapturedKeyboardInput() const; + /// ============================================================================== + /// ===================================================================== + /// + /// Getters for various private data members + /// + /** + * @brief Getter for the font loader + * @returns shared pointer to the font loader + */ std::shared_ptr getFonts(); + /// ============================================================================== private: widget::Handle next_handle {0}; @@ -89,13 +281,60 @@ class GUI { Client* client; + /// =========================================================== + /** + * @brief Performs a click on the specied coordinate in the GUI coordinate frame. + * + * This is what will call the click callback functions on the widgets. + * + * @param x x coordinate of click in GUI coordinates + * @param y y coordinate of click in GUI coordinates + */ + void _handleClick(float x, float y); + /** + * @brief Performs a mouse hover on the specied coordinate in the GUI coordinate frame. + * + * This is what will call the hover callback functions on the widgets. + * + * @param x x coordinate of click in GUI coordinates + * @param y y coordinate of click in GUI coordinates + */ + void _handleHover(float x, float y); + /// ============================================================================= + + /// ================================================================ + /// + /// These are all of the internal helper functions which render a specified GUIState + /// layout. These are where all of the widget manipulation should occur. + /// + /// NOTE: The widget manipulation functions are public, so you can also do further + /// widget manipulation outside of one of these functions. However, if you feel the + /// need to do this you should consider whether or not what you're doing makes more + /// sense to encode as a GUIState. The reason those functions are public are to do + /// more fine tuned GUI manipulation which may only make sense to do outside of these + /// preset "layouts". + /// + /** + * @brief + */ void _layoutTitleScreen(); + /** + * @brief + */ void _layoutLobbyBrowser(); + /** + * @brief + */ void _layoutLobby(); + /** + * @brief + */ void _layoutGameHUD(); + /** + * @brief + */ void _layoutGameEscMenu(); + /// ============================================================================= }; -using namespace gui; - -} \ No newline at end of file +} diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 6537ca86..d5a4a873 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -56,11 +56,13 @@ void GUI::renderFrame() { } void GUI::handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down) { + // convert to gui coords, where (0,0) is bottome left + mouse_ypos = WINDOW_HEIGHT - mouse_ypos; if (is_left_mouse_down) { - this->handleClick(mouse_xpos, mouse_ypos); + this->_handleClick(mouse_xpos, mouse_ypos); is_left_mouse_down = false; } - this->handleHover(mouse_xpos, mouse_ypos); + this->_handleHover(mouse_xpos, mouse_ypos); } widget::Handle GUI::addWidget(widget::Widget::Ptr&& widget) { @@ -69,7 +71,7 @@ widget::Handle GUI::addWidget(widget::Widget::Ptr&& widget) { return handle; } -std::unique_ptr GUI::removeWidget(widget::Handle handle) { +widget::Widget::Ptr GUI::removeWidget(widget::Handle handle) { auto widget = std::move(this->widgets.at(handle)); this->widgets.erase(handle); return widget; @@ -107,25 +109,7 @@ void GUI::clearCapturedKeyboardInput() { this->keyboard_input = ""; } -// TODO: reduce copied code between these two functions - -void GUI::handleClick(float x, float y) { - // convert to gui coords, where (0,0) is bottome left - y = WINDOW_HEIGHT - y; - - for (const auto& [_, widget] : this->widgets) { - widget->doClick(x, y); - } -} - -void GUI::handleHover(float x, float y) { - // convert to gui coords, where (0,0) is bottome left - y = WINDOW_HEIGHT - y; - for (const auto& [_, widget] : this->widgets) { - widget->doHover(x, y); - } -} std::shared_ptr GUI::getFonts() { return this->fonts; @@ -363,4 +347,16 @@ void GUI::_layoutGameEscMenu() { this->addWidget(std::move(flex)); } +void GUI::_handleClick(float x, float y) { + for (const auto& [_, widget] : this->widgets) { + widget->doClick(x, y); + } +} + +void GUI::_handleHover(float x, float y) { + for (const auto& [_, widget] : this->widgets) { + widget->doHover(x, y); + } +} + } From 9939ad368e91e6e2bc079237ef420ca2eb732237 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 16:42:46 -0700 Subject: [PATCH 48/92] continue documenting --- include/client/gui/font/font.hpp | 6 ++++++ include/client/gui/gui.hpp | 26 +++++++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index 577d393b..2d229972 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -7,6 +7,12 @@ namespace gui::font { +/** + * Abstract representation of the different fonts to use in our game + * + * NOTE: currently I haven't found a good font for "Text", so both of these + * map to the same font. + */ enum class Font { MENU, TEXT, diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index d6f6f4fc..96a9c721 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -315,23 +315,39 @@ class GUI { /// preset "layouts". /// /** - * @brief + * @brief Displays the title screen layout + * + * Transitions to the LobbyBrowser once "Start Game" is clicked. */ void _layoutTitleScreen(); /** - * @brief + * @brief Displays the lobby browser layout + * + * Allows the user to input a name. + * Transitions to the Lobby once a lobby is selected to join. */ void _layoutLobbyBrowser(); /** - * @brief + * @brief Displays the lobby layout + * + * Does not provide any GUI-initiated transitions, as it instead waits for the + * server to specify that the game has started with a LoadGameStateEvent + * + * Displays the lobby name and all of the players who are currently in the lobby. */ void _layoutLobby(); /** - * @brief + * @brief Displays the Game HUD layout + * + * TODO: this is not implemented yet */ void _layoutGameHUD(); /** - * @brief + * @brief Displays the menu which appears when the player presses Escape while playing + * + * Known bugs: + * BUG: The game stops rendering the game when this is being displayed + * BUG: Mouse movement is still tracked causing disorienting reorientation upon resuming */ void _layoutGameEscMenu(); /// ============================================================================= From 4cfc2abbdf1656cdeab5e3127a4f6ab065857a97 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Thu, 2 May 2024 17:28:18 -0700 Subject: [PATCH 49/92] continue documentation --- include/client/gui/font/font.hpp | 14 +++++++++++++- include/client/gui/font/loader.hpp | 26 ++++++++++++++++++++++++++ include/client/gui/img/img.hpp | 21 +++++++++++++++++---- include/client/gui/img/loader.hpp | 5 +++++ 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index 2d229972..81f86160 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -18,13 +18,19 @@ enum class Font { TEXT, }; +/** + * Preset sizes for fonts + */ enum FontSizePx { SMALL = 64, MEDIUM = 96, LARGE = 128, - BIG_YOSHI = 256 + BIG_YOSHI = 256 // https://www.google.com/search?q=big+yoshi&sca_esv=f28e476185e14b90&udm=2&biw=1504&bih=927&sxsrf=ACQVn082--gJOfKGRnYyCSUnjdkNWOUwIg%3A1714695676102&ei=_C00ZrvlBdDWkPIP3bGh6Ac&ved=0ahUKEwi706-Vm_CFAxVQK0QIHd1YCH0Q4dUDCBA&uact=5&oq=big+yoshi&gs_lp=Egxnd3Mtd2l6LXNlcnAiCWJpZyB5b3NoaTIEECMYJzIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABEj-BFDdAljSA3ACeACQAQCYATagAWaqAQEyuAEDyAEA-AEBmAIEoAJ5wgIKEAAYgAQYQxiKBZgDAIgGAZIHATSgB5MM&sclient=gws-wiz-serp }; +/** + * Preset colors for text + */ enum class FontColor { BLACK, RED, @@ -32,8 +38,14 @@ enum class FontColor { GRAY }; +/** + * Mappings from our specified abstract fonts to the file to load + */ std::string getFilepath(Font font); +/** + * Mapping from preset font colors to RGB values + */ glm::vec3 getRGB(FontColor color); } diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp index e17e99a0..fecdf1d4 100644 --- a/include/client/gui/font/loader.hpp +++ b/include/client/gui/font/loader.hpp @@ -13,6 +13,9 @@ namespace gui::font { // modified from https://learnopengl.com/In-Practice/Text-Rendering +/** + * Representation of a font character + */ struct Character { unsigned int texture_id; /// id handle for glyph texture glm::ivec2 size; /// size of glyph @@ -20,21 +23,44 @@ struct Character { unsigned int advance; /// offset to advance to next glyph }; +/** + * Hash function so we can provide a pair of Font and FontSize and get the char + * mappings for that font. + */ struct font_pair_hash { std::size_t operator()(const std::pair& p) const; }; +/** + * Handles loading all of the fonts we want to use, and provides an interface to get + * the Character information (e.g. opengl texture and sizing information). + */ class Loader { public: Loader() = default; + /** + * Initializes all of the font information for every font we want to use and stores + * it inside of the font map data member. + */ bool init(); + /** + * Loads the specified character with the specified font and size + * + * @param c ASCII char to load + * @param font Abstract font to use. NOTE: must be one of the preset values we provide! + * @param FontSizePx size of the font in pixels to load + * @returns the Character information for that glyph + */ [[nodiscard]] const Character& loadChar(char c, Font font, FontSizePx size) const; private: FT_Library ft; + /** + * Internal helper function to load a font. Called in `init()` + */ bool _loadFont(Font font); std::unordered_map< diff --git a/include/client/gui/img/img.hpp b/include/client/gui/img/img.hpp index e5d13e97..c7f27694 100644 --- a/include/client/gui/img/img.hpp +++ b/include/client/gui/img/img.hpp @@ -8,19 +8,32 @@ namespace gui::img { +/** + * Abstract representation of any image we would want to load in + * + * NOTE: if you add a new ID, then also add it into the below macro so that + * the image loader will actually load it. + */ enum class ImgID { Yoshi }; - #define GET_ALL_IMG_IDS() \ {ImgID::Yoshi} +/** + * Representation of a loaded image + */ struct Img { - GLuint texture_id; - int width; - int height; + GLuint texture_id; /// opengl texture id + int width; /// width in pixels + int height; /// height in pixels }; +/** + * Mapping from abstract image id to the filepath for that image + * + * @param img ID of the image to get the filepath of + */ std::string getImgFilepath(ImgID img); } \ No newline at end of file diff --git a/include/client/gui/img/loader.hpp b/include/client/gui/img/loader.hpp index 42e02f98..66875a65 100644 --- a/include/client/gui/img/loader.hpp +++ b/include/client/gui/img/loader.hpp @@ -6,6 +6,11 @@ namespace gui::img { +/** + * This class is supposed to load images. + * + * Unfortunately it doesn't work at all! Yipeeee! + */ class Loader { public: Loader() = default; From b7e3b4d94c7018e8264da2d32108c277ae6c3006 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 3 May 2024 13:45:31 -0700 Subject: [PATCH 50/92] move assets to assets folder, and continue documentation --- .../fonts}/AncientModernTales-a7Po.ttf | Bin {imgs => assets/imgs}/Yoshi.png | Bin include/client/gui/font/font.hpp | 2 +- include/client/gui/gui.hpp | 2 - include/client/gui/widget/widget.hpp | 118 ++++++++++++++++-- src/client/gui/font/font.cpp | 2 +- src/client/gui/font/loader.cpp | 2 +- src/client/gui/gui.cpp | 2 +- src/client/gui/imgs/img.cpp | 2 +- 9 files changed, 112 insertions(+), 18 deletions(-) rename {fonts => assets/fonts}/AncientModernTales-a7Po.ttf (100%) rename {imgs => assets/imgs}/Yoshi.png (100%) diff --git a/fonts/AncientModernTales-a7Po.ttf b/assets/fonts/AncientModernTales-a7Po.ttf similarity index 100% rename from fonts/AncientModernTales-a7Po.ttf rename to assets/fonts/AncientModernTales-a7Po.ttf diff --git a/imgs/Yoshi.png b/assets/imgs/Yoshi.png similarity index 100% rename from imgs/Yoshi.png rename to assets/imgs/Yoshi.png diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index 81f86160..84e003f6 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -25,7 +25,7 @@ enum FontSizePx { SMALL = 64, MEDIUM = 96, LARGE = 128, - BIG_YOSHI = 256 // https://www.google.com/search?q=big+yoshi&sca_esv=f28e476185e14b90&udm=2&biw=1504&bih=927&sxsrf=ACQVn082--gJOfKGRnYyCSUnjdkNWOUwIg%3A1714695676102&ei=_C00ZrvlBdDWkPIP3bGh6Ac&ved=0ahUKEwi706-Vm_CFAxVQK0QIHd1YCH0Q4dUDCBA&uact=5&oq=big+yoshi&gs_lp=Egxnd3Mtd2l6LXNlcnAiCWJpZyB5b3NoaTIEECMYJzIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABEj-BFDdAljSA3ACeACQAQCYATagAWaqAQEyuAEDyAEA-AEBmAIEoAJ5wgIKEAAYgAQYQxiKBZgDAIgGAZIHATSgB5MM&sclient=gws-wiz-serp + HUGE = 256 }; /** diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 96a9c721..afbee47d 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -44,8 +44,6 @@ enum class GUIState { * The GUI class acts as a container for all of these widgets and provides a helpful abstraction layer * allowing the rest of the code to think in terms of "GUIState", since you can just tell the GUI * to render a specific state, and it will do so. - * - * Here be Dragons */ class GUI { public: diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index 29023687..b3ad69bb 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -12,36 +12,132 @@ namespace gui::widget { -class GUI; - +/// @brief Type for a Widget Handle using Handle = std::size_t; +/// @brief Type for the handle for a specific callback function using CallbackHandle = std::size_t; +/// @brief Type for a onClick or onHover callback function. +/// The Handle parameter is the handle to the widget that the callback is +/// attached to. using Callback = std::function; +/** + * Abstract base class for any arbitrary widget. + * + * Any Widget implementation must override the following pure virtual functions: + * - render + * + * And certain widget implementations may also wish to override these virtual functions + * - doClick + * - doHover + * - hasHandle + * - borrow + * + * You can see the documentation for these functions for explanations of why you may or + * may not need to override this functions for a specific derived widget. + */ class Widget { public: + /// All widgets are passed around and manipulated as unique ptrs so this is a helpful + /// alias to reduce characters typed using Ptr = std::unique_ptr; + /// =============================================================================== + /// + /// These functions are necessary to setup and position a widget on the screen. + /// + /** + * @brief Sets the type and origin position of the widget, and assigns a new unique handle. + * + * @param type Type of the widget + * @param origin Origin position of the widget (bottom left x,y coordinate in the GUI + * coordinate frame) + */ explicit Widget(Type type, glm::vec2 origin); - + /** + * @brief Overrides the current origin position with the newly specified one. + * + * NOTE: This is helpful when you may not know the exact origin position of a widget at + * construction time (i.e. when inserting a widget inside of a flexbox, as the flexbox) + * then becomes responsible for positioning the widget). + * + * @param origin Bottom left (x,y) coordinate of the widget in the GUI coordinate frame. + */ void setOrigin(glm::vec2 origin); - [[nodiscard]] const glm::vec2& getOrigin() const; - + /// ====================================================================================== + + /// ===================================================================== + /// + /// These functions are used to register, remove, and activate action callbacks on widgets + /// + /** + * @brief Register a new onClick callback function for this widget. + * + * @param callback Callback function to run on click + * + * @returns handle to the callback function, which can be passed into removeOnClick to + * unregister. + */ CallbackHandle addOnClick(Callback callback); + /** + * @brief Register a new onHover callback function for this widget. + * + * @param callback Callback function to run on hover + * + * @returns handle to the callback function, which can be passed into removeOnHover to + * unregister. + */ CallbackHandle addOnHover(Callback callback); - + /** + * @brief Removes the onClick callback function associated with the given handle + * + * @param handle Handle to the onClick callback function you want to remove + */ void removeOnClick(CallbackHandle handle); + /** + * @brief Removes the onHover callback function associated with the given handle + * + * @param handle Handle to the onHover callback function you want to remove + */ void removeOnHover(CallbackHandle handle); - - virtual void render(GLuint shader) = 0; - + /** + * @brief Performs a click at the specified (x,y) position in the GUI coordinate frame. + * + * NOTE: This function's main role is to determine if a click at the specified (x,y) + * coordinate should trigger the onClick handlers for this widget. The default implementation + * of this checks to see if the (x,y) coordinate is within the bounds specified by `origin` + * and `width` and `height`, and if so calls all of the onClick handlers. + * + * NOTE: One reason for a derived class to override this function is if the widget itself + * contains other widgets, because in that case doClick will not be called on those "subwidgets" + * unless this function passes the call down the chain + * + * @param x x coordinate of the click in GUI coordinates + * @param y y coordinate of the click in GUI coordinates + */ virtual void doClick(float x, float y); + /** + * @brief Performs a hover action at the specified (x,y) position in the GUI coordinate frame + * + * NOTE: see the documentation for `doClick`, as everything said there also applies here, but just + * for the hover actions. + */ virtual void doHover(float x, float y); + /// ====================================================================================== + + /// ============================================================================= + /** + * Renders the widget to the screen using the specified shader + * + * @param shader Shader to use when rednering the widgete + */ + virtual void render(GLuint shader) = 0; + /// ====================================================================================== - [[nodiscard]] Type getType() const; + [[nodiscard]] Type getType() const; + [[nodiscard]] const glm::vec2& getOrigin() const; [[nodiscard]] std::pair getSize() const; - [[nodiscard]] Handle getHandle() const; [[nodiscard]] virtual bool hasHandle(Handle handle) const; [[nodiscard]] virtual Widget* borrow(Handle handle); diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index b7a31d59..29a67b38 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -6,7 +6,7 @@ namespace gui::font { std::string getFilepath(Font font) { - auto dir = getRepoRoot() / "fonts"; + auto dir = getRepoRoot() / "assets/fonts"; switch (font) { case Font::MENU: return (dir / "AncientModernTales-a7Po.ttf").string(); default: diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 1e275242..233df563 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -56,7 +56,7 @@ bool Loader::_loadFont(Font font) { return false; } - for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE, FontSizePx::BIG_YOSHI}) { + for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE, FontSizePx::HUGE}) { FT_Set_Pixel_Sizes(face, 0, font_size); std::unordered_map characters; for (unsigned char c = 0; c < 128; c++) { diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index d5a4a873..115cd148 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -147,7 +147,7 @@ void GUI::_layoutTitleScreen() { this->addWidget(widget::CenterText::make( "Arcana", font::Font::MENU, - font::FontSizePx::BIG_YOSHI, + font::FontSizePx::HUGE, font::FontColor::RED, fonts, FRAC_WINDOW_HEIGHT(2, 3) diff --git a/src/client/gui/imgs/img.cpp b/src/client/gui/imgs/img.cpp index 437ecc15..57cc2675 100644 --- a/src/client/gui/imgs/img.cpp +++ b/src/client/gui/imgs/img.cpp @@ -5,7 +5,7 @@ namespace gui::img { std::string getImgFilepath(ImgID img) { - auto img_root = getRepoRoot() / "imgs"; + auto img_root = getRepoRoot() / "assets/imgs"; switch (img) { default: case ImgID::Yoshi: return (img_root / "Yoshi.png").string(); From a02ccc3272e838f402acc3b38f0a02a0367a138d Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 3 May 2024 13:55:24 -0700 Subject: [PATCH 51/92] refactor how shaders are passed around in the gui land --- include/client/gui/gui.hpp | 9 +++++---- include/client/gui/widget/dyntext.hpp | 3 ++- include/client/gui/widget/flexbox.hpp | 2 +- include/client/gui/widget/staticimg.hpp | 3 ++- include/client/gui/widget/textinput.hpp | 2 +- include/client/gui/widget/widget.hpp | 4 +--- src/client/client.cpp | 7 ++++--- src/client/gui/gui.cpp | 8 +++++--- src/client/gui/widget/dyntext.cpp | 10 ++++++---- src/client/gui/widget/flexbox.cpp | 4 ++-- src/client/gui/widget/staticimg.cpp | 6 ++++-- src/client/gui/widget/textinput.cpp | 4 ++-- 12 files changed, 35 insertions(+), 27 deletions(-) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index afbee47d..864cd1aa 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -64,12 +64,14 @@ class GUI { */ explicit GUI(Client* client); /** - * @brief Initializes all of the necessary file loading for all of the GUI elements. - * Currently this is mainly the image loading and the font loading. + * @brief Initializes all of the necessary file loading for all of the GUI elements, and + * registers all of the static shader variables for each of the derived widget classes + * that need a shader. * * @param text_shader Shader to use for text rendering + * @param img_shader Shader to use for rendering images */ - bool init(GLuint text_shader); + bool init(GLuint text_shader, GLuint img_shader); /// ================================================================================ /// ===================================================================== @@ -269,7 +271,6 @@ class GUI { private: widget::Handle next_handle {0}; std::unordered_map widgets; - GLuint text_shader; std::shared_ptr fonts; img::Loader images; diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index 5be812c0..a2a70ee1 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -13,6 +13,7 @@ namespace gui::widget { class DynText : public Widget { public: using Ptr = std::unique_ptr; + static GLuint shader; struct Options { font::Font font {font::Font::TEXT}; @@ -31,7 +32,7 @@ class DynText : public Widget { DynText(std::string text, std::shared_ptr loader, Options options); DynText(std::string text, std::shared_ptr loader); - void render(GLuint shader) override; + void render() override; void changeColor(font::FontColor new_color); diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index 1381e4a4..63352b60 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -32,7 +32,7 @@ class Flexbox : public Widget { void push(Widget::Ptr&& widget); - void render(GLuint shader) override; + void render() override; Widget* borrow(Handle handle) override; bool hasHandle(Handle handle) const override; diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp index 8e088ddb..23edb967 100644 --- a/include/client/gui/widget/staticimg.hpp +++ b/include/client/gui/widget/staticimg.hpp @@ -11,6 +11,7 @@ namespace gui::widget { class StaticImg : public Widget { public: using Ptr = std::unique_ptr; + static GLuint shader; template static Ptr make(Params&&... params) { @@ -20,7 +21,7 @@ class StaticImg : public Widget { StaticImg(glm::vec2 origin, gui::img::Img img); StaticImg(gui::img::Img img); - void render(GLuint shader) override; + void render() override; private: gui::img::Img img; diff --git a/include/client/gui/widget/textinput.hpp b/include/client/gui/widget/textinput.hpp index fbfc337f..f7f951aa 100644 --- a/include/client/gui/widget/textinput.hpp +++ b/include/client/gui/widget/textinput.hpp @@ -39,7 +39,7 @@ class TextInput : public Widget { gui::GUI* gui, std::shared_ptr fonts); - void render(GLuint shader) override; + void render() override; bool hasHandle(Handle handle) const override; Widget* borrow(Handle handle) override; diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index b3ad69bb..1b5b6534 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -128,10 +128,8 @@ class Widget { /// ============================================================================= /** * Renders the widget to the screen using the specified shader - * - * @param shader Shader to use when rednering the widgete */ - virtual void render(GLuint shader) = 0; + virtual void render() = 0; /// ====================================================================================== diff --git a/src/client/client.cpp b/src/client/client.cpp index 2201825e..cce16054 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -130,13 +130,14 @@ bool Client::init() { if (!textShaderProgram) { std::cerr << "Failed to initialize text shader program" << std::endl; - return -1; + return false; } // Init GUI (e.g. load in all fonts) - if (!this->gui.init(textShaderProgram)) { + // TODO: pass in shader for image loading in second param + if (!this->gui.init(textShaderProgram, textShaderProgram)) { std::cerr << "GUI failed to init" << std::endl; - return -1; + return false; } this->cubeShaderProgram = loadCubeShaders(); diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 115cd148..9c29b4b0 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -14,7 +14,7 @@ GUI::GUI(Client* client) { this->client = client; } -bool GUI::init(GLuint text_shader) +bool GUI::init(GLuint text_shader, GLuint image_shader) { std::cout << "Initializing GUI...\n"; @@ -29,7 +29,9 @@ bool GUI::init(GLuint text_shader) return false; } - this->text_shader = text_shader; + // Need to register all of the necessary shaders for each widget + widget::DynText::shader = text_shader; + widget::StaticImg::shader = image_shader; std::cout << "Initialized GUI\n"; return true; @@ -48,7 +50,7 @@ void GUI::renderFrame() { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); for (auto& [handle, widget] : this->widgets) { - widget->render(this->text_shader); + widget->render(); } glDisable(GL_CULL_FACE); diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 75969da7..67d1eaf4 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -11,6 +11,8 @@ namespace gui::widget { +GLuint DynText::shader = 0; + DynText::DynText(glm::vec2 origin, std::string text, std::shared_ptr fonts, DynText::Options options): text(text), options(options), fonts(fonts), Widget(Type::DynText, origin) @@ -53,13 +55,13 @@ DynText::DynText(std::string text, std::shared_ptr fonts): DynText::DynText(std::string text, std::shared_ptr fonts, DynText::Options options): DynText({0.0f, 0.0f}, text, fonts, options) {} -void DynText::render(GLuint shader) { - glUseProgram(shader); +void DynText::render() { + glUseProgram(DynText::shader); // todo move to gui glm::mat4 projection = glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); - glUniformMatrix4fv(glGetUniformLocation(shader, "projection"), 1, false, reinterpret_cast(&projection)); - glUniform3f(glGetUniformLocation(shader, "textColor"), + glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&projection)); + glUniform3f(glGetUniformLocation(DynText::shader, "textColor"), this->options.color.x, this->options.color.y, this->options.color.z); glActiveTexture(GL_TEXTURE0); glBindVertexArray(VAO); diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index 858c9d49..860fba78 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -91,11 +91,11 @@ void Flexbox::push(Widget::Ptr&& widget) { } -void Flexbox::render(GLuint shader) { +void Flexbox::render() { // use x and y as origin coordinates, and render everything else based off of it for (const auto& widget : this->widgets) { - widget->render(shader); + widget->render(); } } diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 3cda02aa..a00c552c 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -2,6 +2,8 @@ namespace gui::widget { +GLuint StaticImg::shader = 0; + StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): Widget(Type::StaticImg, origin) { @@ -50,8 +52,8 @@ StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): StaticImg::StaticImg(gui::img::Img img): StaticImg({0.0f, 0.0f}, img) {} -void StaticImg::render(GLuint shader) { - glUseProgram(shader); +void StaticImg::render() { + glUseProgram(StaticImg::shader); // Bind Texture glBindTexture(GL_TEXTURE_2D, this->img.texture_id); diff --git a/src/client/gui/widget/textinput.cpp b/src/client/gui/widget/textinput.cpp index 94ececec..f8490655 100644 --- a/src/client/gui/widget/textinput.cpp +++ b/src/client/gui/widget/textinput.cpp @@ -56,8 +56,8 @@ TextInput::TextInput(glm::vec2 origin, .scale {1.0}, }) {} -void TextInput::render(GLuint shader) { - this->dyntext->render(shader); +void TextInput::render() { + this->dyntext->render(); } bool TextInput::hasHandle(Handle handle) const { From 227eb539ea7d28bc3cf4a90080eb9e5b7b2bf86c Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 3 May 2024 14:52:02 -0700 Subject: [PATCH 52/92] finish documentation and make unit tests compile --- include/client/gui/widget/centertext.hpp | 17 ++++++ include/client/gui/widget/dyntext.hpp | 10 +++ include/client/gui/widget/flexbox.hpp | 17 ++++++ include/client/gui/widget/staticimg.hpp | 14 +++++ include/client/gui/widget/textinput.hpp | 11 ++++ include/client/gui/widget/widget.hpp | 77 +++++++++++++++++++++++- src/client/CMakeLists.txt | 18 +----- src/client/gui/CMakeLists.txt | 41 +++++++++++++ src/client/tests/CMakeLists.txt | 7 ++- 9 files changed, 192 insertions(+), 20 deletions(-) create mode 100644 src/client/gui/CMakeLists.txt diff --git a/include/client/gui/widget/centertext.hpp b/include/client/gui/widget/centertext.hpp index f2283dba..8e041f13 100644 --- a/include/client/gui/widget/centertext.hpp +++ b/include/client/gui/widget/centertext.hpp @@ -5,8 +5,25 @@ namespace gui::widget { +/** + * This widget is a special kind of "Macro Widget" which essentially acts as a wrapper around a more + * complex internal structure to make a piece of text that is centered horizontally across the + * screen. + */ class CenterText { public: + /** + * @brief Constructs a unique_ptr for a Flexbox widget that contains a DynText widget + * + * @param text Text to display + * @param font font to render the text with + * @param size size of the font + * @param color color of the text + * @param fonts font loader + * @param y_pos bottom y position of the widget in GUI coordinates + * + * @returns a Flexbox widget that centers an internal DynText widget + */ static Widget::Ptr make( std::string text, font::Font font, diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index a2a70ee1..b016d855 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -22,6 +22,16 @@ class DynText : public Widget { float scale {1.0}; }; + /** + * @brief creates a DynText unique ptr widget + * + * @param origin (Optional) Bottom left coordinate position of the widget in GUI coordinates + * @param text Text to render + * @param load Font loader + * @param options (Optional) Options to customize how the text is rendered + * + * @returns a DynText widget + */ template static Ptr make(Params&&... params) { return std::make_unique(std::forward(params)...); diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index 63352b60..5adfa1ae 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -17,6 +17,20 @@ class Flexbox : public Widget { float padding; }; + /** + * @brief creates a flexbox unique ptr for use in the GUI + * + * NOTE: I don't think flexboxes inside of flexboxes will currently work, but I haven't tried. + * + * @param origin Bottom left (x,y) coordinate of the flexbox, from which the container + * grows upward + * @param size (Optional) Size of the container in pixels. You should only specify a size + * in the axis that does not grow in the direction of pushing (e.g. set a static width if you + * have a VERTICAL flexbox). + * @param options (Optional) Configurable options for the flexbox. + * + * @returns A Flexbox widget + */ template static Ptr make(Params&&... params) { return std::make_unique(std::forward(params)...); @@ -30,6 +44,9 @@ class Flexbox : public Widget { void doClick(float x, float y) override; void doHover(float x, float y) override; + /** + * @brief adds a new widget to the flexbox, from the bottom up + */ void push(Widget::Ptr&& widget); void render() override; diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp index 23edb967..fc0867c2 100644 --- a/include/client/gui/widget/staticimg.hpp +++ b/include/client/gui/widget/staticimg.hpp @@ -8,11 +8,25 @@ namespace gui::widget { + +/** + * Widget to display a static image (png) to the screen. + * + * BUG: This doesn't work at all currently! ASDAJSHHASHDAHHHHHHH! + */ class StaticImg : public Widget { public: using Ptr = std::unique_ptr; static GLuint shader; + /** + * @brief creates a StaticImg unique ptr widget + * + * @param origin (Optional) bottom left coordinate of the widget in GUI Coordinates + * @param img Id for the image to render + * + * @returns A StaticImg widget + */ template static Ptr make(Params&&... params) { return std::make_unique(std::forward(params)...); diff --git a/include/client/gui/widget/textinput.hpp b/include/client/gui/widget/textinput.hpp index f7f951aa..e2cccf5b 100644 --- a/include/client/gui/widget/textinput.hpp +++ b/include/client/gui/widget/textinput.hpp @@ -23,6 +23,15 @@ class TextInput : public Widget { public: using Ptr = std::unique_ptr; + /** + * @brief Constructs a TextInput unique ptr widget + * + * @param Origin bottom left (x,y) coordinate in GUI coordinates + * @param placeholder Text to display if no text has been inputted yet + * @param gui Pointer to the GUI class, so internally we can access keystroke information + * @param fonts Font loader + * @param options (Optional) Configurable options for the widget + */ template static Ptr make(Params&&... params) { return std::make_unique(std::forward(params)...); @@ -50,6 +59,8 @@ class TextInput : public Widget { static std::string prev_input; widget::DynText::Ptr dyntext; + + /// NOTE: this widget needs a pointer to the GUI so it can access the GUI's keystroke capturing functionality gui::GUI* gui; }; diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp index 1b5b6534..fbcc8162 100644 --- a/include/client/gui/widget/widget.hpp +++ b/include/client/gui/widget/widget.hpp @@ -35,6 +35,8 @@ using Callback = std::function; * * You can see the documentation for these functions for explanations of why you may or * may not need to override this functions for a specific derived widget. + * + * In addition, any derived class must also set width and height once these values are known. */ class Widget { public: @@ -127,35 +129,106 @@ class Widget { /// ============================================================================= /** - * Renders the widget to the screen using the specified shader + * @brief Renders the widget to the screen + * + * This is the only function that must be overridden by derived classes. */ virtual void render() = 0; /// ====================================================================================== - + /// ====================================================================== + /** + * @brief Queries the widget::Type of the widget + * @return Type of the widget + */ [[nodiscard]] Type getType() const; + /** + * @brief Queries the origin position of the widget + * @return origin position of the widget in GUI coordinates + */ [[nodiscard]] const glm::vec2& getOrigin() const; + /** + * @brief Queries the size of the widget in pixels + * @return the width and height of the widget + */ [[nodiscard]] std::pair getSize() const; + /** + * @brief Queries the handle for this specific widget + * @return the handle for this widget + */ [[nodiscard]] Handle getHandle() const; + /// ====================================================================================== + + /// ==================================================================== + /// + /// The following functions are incredibly important for GUI manipulation, especially in + /// event handlers. They essentially define how outside users can gain internal access to + /// widgets that are inside of the GUI + /// + /// hasHandle should always be called before calling borrow if you aren't sure whether or + /// not the widget actually has the specified handle, because borrow will terminate the + /// program if the specified widget does not have the handle. + /// + /// By default hasHandle and borrow only check the widget itself to see if the handle matches. + /// This means that if some derived Widget acts as a container for some set of internal + /// subwidgets, then these functions should be overridden to provide access to those subwidgets. + /// + /** + * @brief Checks to see whether or not this widget "has" the specified handle, whether + * because the handle is this widgets handle, or because this widget internally contains + * some subwidget that has that handle. + * + * @param handle Handle of the widget for which you want to query + * + * @returns True if this widget is or contains the specified widget, false otherwise + */ [[nodiscard]] virtual bool hasHandle(Handle handle) const; + /** + * @brief Gives a pointer to the subwidget specified by the handle + * + * NOTE: if `handle` is not a valid handle for either (1) this widget or (2) any subwidgets, + * then this function will terminate the program and print out an error message. + * + * @returns a pointer to the widget specified by handle + */ [[nodiscard]] virtual Widget* borrow(Handle handle); + /// ====================================================================================== protected: + /// @brief Handle for this widget Handle handle; + + /// @brief Static counter for the number of widgets that have been created, so a new identifier + /// can be assigned to each widget as they are created static std::size_t num_widgets; + /// @brief Type of the widget Type type; + + /// @brief Origin position (bottom left) of the widget in GUI coordinates glm::vec2 origin; + + /// NOTE: Both the widget and the height of the widget are initialized to 0 and are not set + /// anywhere inside of this base class. Instead, derived classes must set these values themselves + /// once they are known. + + /// @brief Width of the widget, in pixels std::size_t width {0}; + /// @brief Height of the widget, in pixels std::size_t height {0}; + /// @brief all of the onClick handlers, indexable by handle std::unordered_map on_clicks; + /// @brief all of the onHover handlers, indexable by handle std::unordered_map on_hovers; private: + /// @brief internal counter to assign to the next onClick handler CallbackHandle next_click_handle {0}; + /// @brief internal counter to assign to the next onHover handler CallbackHandle next_hover_handle {0}; + /// @brief helper function to determine if bool _doesIntersect(float x, float y) const; }; diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index f8ae65c2..d696f324 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -9,6 +9,8 @@ add_subdirectory(../../dependencies/glew ${CMAKE_BINARY_DIR}/glew) add_subdirectory(../../dependencies/freetype ${CMAKE_BINARY_DIR}/freetype) add_subdirectory(../../dependencies/sfml ${CMAKE_BINARY_DIR}/sfml) +add_subdirectory(gui) + set(FILES sound.cpp camera.cpp @@ -17,19 +19,6 @@ set(FILES util.cpp lobbyfinder.cpp - gui/imgs/img.cpp - gui/imgs/loader.cpp - gui/font/font.cpp - gui/font/loader.cpp - gui/widget/centertext.cpp - gui/widget/textinput.cpp - gui/widget/widget.cpp - gui/widget/dyntext.cpp - gui/widget/flexbox.cpp - gui/widget/staticimg.cpp - gui/gui.cpp - - ../../dependencies/stb/stb_image.cpp shaders.cpp ) @@ -37,14 +26,13 @@ set(FILES set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL REQUIRED) - add_library(${LIB_NAME} STATIC ${FILES}) -target_include_directories(${LIB_NAME} PRIVATE ../../dependencies/stb) # include stb image loading header target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) target_link_libraries(${LIB_NAME} PRIVATE game_shared_lib + game_gui_lib Boost::asio Boost::filesystem Boost::thread diff --git a/src/client/gui/CMakeLists.txt b/src/client/gui/CMakeLists.txt new file mode 100644 index 00000000..1211b3a5 --- /dev/null +++ b/src/client/gui/CMakeLists.txt @@ -0,0 +1,41 @@ +set(LIB_NAME game_gui_lib) + +set(FILES + imgs/img.cpp + imgs/loader.cpp + + font/font.cpp + font/loader.cpp + + widget/centertext.cpp + widget/textinput.cpp + widget/widget.cpp + widget/dyntext.cpp + widget/flexbox.cpp + widget/staticimg.cpp + + gui.cpp + + ../../../dependencies/stb/stb_image.cpp +) + +# OpenGL +set(OpenGL_GL_PREFERENCE GLVND) +find_package(OpenGL REQUIRED) + +add_library(${LIB_NAME} STATIC ${FILES}) +target_include_directories(${LIB_NAME} PRIVATE ../../../dependencies/stb) # include stb image loading header +target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) +target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) +target_link_libraries(${LIB_NAME} PRIVATE game_shared_lib) +target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static freetype) +target_link_libraries(${LIB_NAME} + PRIVATE + Boost::asio + Boost::filesystem + Boost::thread + Boost::program_options + Boost::serialization + nlohmann_json::nlohmann_json +) \ No newline at end of file diff --git a/src/client/tests/CMakeLists.txt b/src/client/tests/CMakeLists.txt index fbe4eb69..c1a4412d 100644 --- a/src/client/tests/CMakeLists.txt +++ b/src/client/tests/CMakeLists.txt @@ -5,13 +5,14 @@ set(FILES ) add_executable(${TARGET_NAME} ${FILES}) -target_link_libraries(${TARGET_NAME} PRIVATE game_client_lib game_shared_lib) +target_link_libraries(${TARGET_NAME} PRIVATE game_client_lib game_shared_lib game_gui_lib) target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) target_link_libraries(${TARGET_NAME} PUBLIC gtest_main) add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) target_include_directories(${TARGET_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) + target_link_libraries(${TARGET_NAME} PRIVATE Boost::asio @@ -21,8 +22,8 @@ target_link_libraries(${TARGET_NAME} Boost::serialization nlohmann_json::nlohmann_json ) -target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") -target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static) +target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static freetype) # setup make target set(RUN_TESTS_TARGET "run_${TARGET_NAME}") From 32b84f2a41275d9bc923553e631dc7e00bbd7004 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 3 May 2024 15:05:19 -0700 Subject: [PATCH 53/92] attempt to fix windows compile --- src/client/client.cpp | 2 +- src/client/gui/gui.cpp | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index cce16054..3ec5de45 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -126,7 +126,7 @@ bool Client::init() { /* Load shader programs */ std::cout << "loading shader" << std::endl; auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; - auto textShaderProgram = LoadShaders((shader_path / "text.vert").c_str(), (shader_path / "text.frag").c_str()); + auto textShaderProgram = LoadShaders((shader_path / "text.vert").string(), (shader_path / "text.frag").string()); if (!textShaderProgram) { std::cerr << "Failed to initialize text shader program" << std::endl; diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 9c29b4b0..931d8445 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -182,8 +182,13 @@ void GUI::_layoutTitleScreen() { } ); start_flex->push(std::move(start_text)); - this->addWidget(std::move(start_flex)); + + auto yoshi = widget::StaticImg::make( + glm::vec2(0.0f, 0.0f), + this->images.getImg(img::ImgID::Yoshi) + ); + this->addWidget(std::move(yoshi)); } void GUI::_layoutLobbyBrowser() { From d287fc63e4ad8d8186255dce7a3f99c7e7dcee44 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Fri, 3 May 2024 15:37:25 -0700 Subject: [PATCH 54/92] load and render model materials --- include/client/client.hpp | 4 ++ include/client/lightsource.hpp | 33 +++++++++++ include/client/model.hpp | 21 ++++++- include/client/shader.hpp | 7 ++- include/client/util.hpp | 5 ++ src/client/CMakeLists.txt | 1 + src/client/client.cpp | 14 ++++- src/client/lightsource.cpp | 59 ++++++++++++++++++++ src/client/model.cpp | 85 ++++++++++++++++++++++++----- src/client/shader.cpp | 12 +++- src/client/shaders/lightsource.frag | 7 +++ src/client/shaders/lightsource.vert | 14 +++++ src/client/shaders/material.frag | 44 +++++++++++++++ src/client/shaders/shader.frag | 1 + src/client/shaders/shader.vert | 4 +- src/client/util.cpp | 4 ++ 16 files changed, 292 insertions(+), 23 deletions(-) create mode 100644 include/client/lightsource.hpp create mode 100644 src/client/lightsource.cpp create mode 100644 src/client/shaders/lightsource.frag create mode 100644 src/client/shaders/lightsource.vert create mode 100644 src/client/shaders/material.frag diff --git a/include/client/client.hpp b/include/client/client.hpp index 12e67537..b88dcac2 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -12,6 +12,7 @@ #include #include "client/cube.hpp" +#include "client/lightsource.hpp" #include "client/shader.hpp" #include "client/model.hpp" #include "client/util.hpp" @@ -55,6 +56,9 @@ class Client { SharedGameState gameState; std::shared_ptr cubeShader; + std::shared_ptr lightSourceShader; + + std::unique_ptr lightSource; std::unique_ptr playerModel; float playerMovementDelta = 0.05f; diff --git a/include/client/lightsource.hpp b/include/client/lightsource.hpp new file mode 100644 index 00000000..af9405bd --- /dev/null +++ b/include/client/lightsource.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "client/core.hpp" +#include "client/shader.hpp" + +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +#include +#include +#include +#include + +class LightSource { +public: + LightSource(); + void draw(std::shared_ptr shader); + + glm::vec3 lightPos; +private: + GLuint VAO, VBO; + + glm::mat4 model; + glm::vec3 color; + + // Cube Information + std::vector positions; + std::vector normals; + std::vector indices; + +}; diff --git a/include/client/model.hpp b/include/client/model.hpp index 7505631f..97ec9476 100644 --- a/include/client/model.hpp +++ b/include/client/model.hpp @@ -50,6 +50,13 @@ class Texture { std::string type; }; +struct Material { + glm::vec3 ambient; + glm::vec3 diffuse; + glm::vec3 specular; + float shininess; +}; + /** * Mesh holds the data needed to render a mesh (collection of triangles). * @@ -62,7 +69,12 @@ class Mesh { /** * Creates a new mesh from a collection of vertices, indices and textures */ - Mesh(const std::vector& vertices, const std::vector& indices, const std::vector& textures); + Mesh( + const std::vector& vertices, + const std::vector& indices, + const std::vector& textures, + const Material& material + ); /** * Render the Mesh to the viewport using the provided shader program. @@ -72,11 +84,12 @@ class Mesh { * @param modelView determines the scaling/rotation/translation of the * mesh */ - void Draw(std::shared_ptr shader, glm::mat4 modelView) const; + void Draw(std::shared_ptr shader, glm::mat4 modelView) ; private: std::vector vertices; std::vector indices; std::vector textures; + Material material; // render data opengl needs GLuint VAO, VBO, EBO; @@ -127,4 +140,8 @@ class Model { std::vector loadMaterialTextures(aiMaterial* mat, const aiTextureType& type); glm::mat4 modelView; + + // store the directory of the model file so that textures can be + // loaded relative to the model file + std::string directory; }; diff --git a/include/client/shader.hpp b/include/client/shader.hpp index 89c48b4a..ca6dcd37 100644 --- a/include/client/shader.hpp +++ b/include/client/shader.hpp @@ -1,11 +1,12 @@ #pragma once -#include - #include #include #include #include + +#include +#include class Shader { @@ -54,6 +55,8 @@ class Shader { * @param value is the float value to write to that variable */ void setFloat(const std::string &name, float value) const; + void setMat4(const std::string &name, glm::mat4& value); + void setVec3(const std::string &name, glm::vec3& value); private: // the shader program ID unsigned int ID; diff --git a/include/client/util.hpp b/include/client/util.hpp index 3f59c932..1206e4fc 100644 --- a/include/client/util.hpp +++ b/include/client/util.hpp @@ -1,2 +1,7 @@ #pragma once +#include "assimp/types.h" +#include + +glm::vec3 aiColorToGLM(const aiColor3D& color); + diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index e2f27751..6f4d762a 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -9,6 +9,7 @@ set(FILES lobbyfinder.cpp shader.cpp model.cpp + lightsource.cpp ${imgui-source} ) diff --git a/src/client/client.cpp b/src/client/client.cpp index 2b0cf4a0..b442ddc9 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1,5 +1,4 @@ #include "client/client.hpp" - #include #include @@ -9,6 +8,7 @@ #include #include +#include "client/lightsource.hpp" #include "client/shader.hpp" #include "client/model.hpp" #include "shared/game/event.hpp" @@ -81,13 +81,20 @@ bool Client::init() { boost::filesystem::path vertFilepath = this->root_path / "src/client/shaders/shader.vert"; - boost::filesystem::path fragFilepath = this->root_path / "src/client/shaders/shader.frag"; + boost::filesystem::path fragFilepath = this->root_path / "src/client/shaders/material.frag"; this->cubeShader = std::make_shared(vertFilepath.string(), fragFilepath.string()); - boost::filesystem::path playerModelFilepath = this->root_path / "src/client/models/bear-sp22.obj"; + boost::filesystem::path playerModelFilepath = this->root_path / "src/client/models/bear_full.obj"; this->playerModel = std::make_unique(playerModelFilepath.string()); this->playerModel->Scale(0.25); + this->lightSource = std::make_unique(); + boost::filesystem::path lightVertFilepath = this->root_path / "src/client/shaders/lightsource.vert"; + boost::filesystem::path lightFragFilepath = this->root_path / "src/client/shaders/lightsource.frag"; + this->lightSourceShader = std::make_shared(lightVertFilepath.string(), lightFragFilepath.string()); + + this->cubeShader->setVec3("lightPos", lightSource->lightPos); + return true; } @@ -146,6 +153,7 @@ void Client::processServerInput(boost::asio::io_context& context) { } void Client::draw() { + this->lightSource->draw(this->lightSourceShader); for (int i = 0; i < this->gameState.objects.size(); i++) { std::shared_ptr sharedObject = this->gameState.objects.at(i); diff --git a/src/client/lightsource.cpp b/src/client/lightsource.cpp new file mode 100644 index 00000000..5c4d3337 --- /dev/null +++ b/src/client/lightsource.cpp @@ -0,0 +1,59 @@ +#include "client/lightsource.hpp" + +#include + +#include + +#include "client/shader.hpp" + +LightSource::LightSource() { + this->lightPos = glm::vec3(1.0f, 10.0f, 0.0f); + model = glm::mat4(1.0f); + model = glm::translate(model, lightPos); + model = glm::scale(model, glm::vec3(0.2f)); + + glGenVertexArrays(1, &VAO); + glBindVertexArray(VAO); + // we only need to bind to the VBO, the container's VBO's data already contains the data. + glBindBuffer(GL_ARRAY_BUFFER, VBO); + // set the vertex attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); +} + +void LightSource::draw(std::shared_ptr shader) { + shader->use(); + // Currently 'hardcoding' camera logic in + float FOV = 45.0f; + float Aspect = 1.33f; + float NearClip = 0.1f; + float FarClip = 100.0f; + + float Distance = 10.0f; + float Azimuth = 0.0f; + float Incline = 20.0f; + + glm::mat4 world(1); + world[3][2] = Distance; + world = glm::eulerAngleY(glm::radians(-Azimuth)) * glm::eulerAngleX(glm::radians(-Incline)) * world; + + // Compute view matrix (inverse of world matrix) + glm::mat4 view = glm::inverse(world); + + // Compute perspective projection matrix + glm::mat4 project = glm::perspective(glm::radians(FOV), Aspect, NearClip, FarClip); + + // Compute final view-projection matrix + glm::mat4 viewProjMtx = project * view; + + // get the locations and send the uniforms to the shader + shader->setMat4("viewProj", viewProjMtx); + shader->setMat4("model", model); + + // draw the light cube object + glBindVertexArray(VAO); + glDrawArrays(GL_TRIANGLES, 0, 36); + + glBindVertexArray(0); + glUseProgram(0); +} diff --git a/src/client/model.cpp b/src/client/model.cpp index 1d06a09a..6260c532 100644 --- a/src/client/model.cpp +++ b/src/client/model.cpp @@ -13,6 +13,8 @@ #include #include "assimp/material.h" +#include "assimp/types.h" +#include "client/util.hpp" #include "glm/ext/matrix_transform.hpp" #include #include @@ -30,8 +32,12 @@ #include -Mesh::Mesh(const std::vector& vertices, const std::vector& indices, const std::vector& textures) : - vertices(vertices), indices(indices), textures(textures) { +Mesh::Mesh( + const std::vector& vertices, + const std::vector& indices, + const std::vector& textures, + const Material& material) : + vertices(vertices), indices(indices), textures(textures), material(material) { glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); @@ -60,9 +66,13 @@ Mesh::Mesh(const std::vector& vertices, const std::vector& glBindVertexArray(0); std::cout << "Loaded mesh with " << vertices.size() << " vertices, and " << textures.size() << " textures" << std::endl; + std::cout << "\t diffuse " << glm::to_string(this->material.diffuse) << std::endl; + std::cout << "\t ambient " << glm::to_string(this->material.diffuse) << std::endl; + std::cout << "\t specular " << glm::to_string(this->material.specular) << std::endl; + std::cout << "\t shininess" << this->material.shininess << std::endl; } -void Mesh::Draw(std::shared_ptr shader, glm::mat4 modelView) const { +void Mesh::Draw(std::shared_ptr shader, glm::mat4 modelView) { // actiavte the shader program shader->use(); @@ -89,12 +99,19 @@ void Mesh::Draw(std::shared_ptr shader, glm::mat4 modelView) const { // Compute final view-projection matrix glm::mat4 viewProjMtx = project * view; - auto color = glm::vec3(0.0f, 1.0f, 1.0f); + auto lightColor = glm::vec3(1.0f, 1.0f, 1.0f); + shader->setVec3("lightColor", lightColor); - // get the locations and send the uniforms to the shader - glUniformMatrix4fv(glGetUniformLocation(shader->getID(), "viewProj"), 1, false, reinterpret_cast(&viewProjMtx)); - glUniformMatrix4fv(glGetUniformLocation(shader->getID(), "model"), 1, GL_FALSE, reinterpret_cast(&modelView)); - glUniform3fv(glGetUniformLocation(shader->getID(), "DiffuseColor"), 1, &color[0]); + glm::vec3 viewPos = glm::vec3(world[3].x, world[3].y, world[3].z); + shader->setVec3("viewPos", viewPos); + + shader->setMat4("viewProj", viewProjMtx); + shader->setMat4("model", modelView); + + shader->setVec3("material.diffuse", this->material.diffuse); + shader->setVec3("material.ambient", this->material.ambient); + shader->setVec3("material.specular", this->material.specular); + shader->setFloat("materia.shininess", this->material.shininess); unsigned int diffuseNr = 1; unsigned int specularNr = 1; @@ -108,7 +125,8 @@ void Mesh::Draw(std::shared_ptr shader, glm::mat4 modelView) const { else if(name == "texture_specular") number = std::to_string(specularNr++); - shader->setInt(("material." + name + number).c_str(), i); + std::string shaderTextureName = "material." + name + number; + shader->setInt(shaderTextureName, i); glBindTexture(GL_TEXTURE_2D, textures[i].getID()); } glActiveTexture(GL_TEXTURE0); @@ -122,7 +140,7 @@ void Mesh::Draw(std::shared_ptr shader, glm::mat4 modelView) const { } Model::Model(const std::string& filepath) : - modelView(1.0f) { + modelView(1.0f), directory(filepath.substr(0, filepath.find_last_of('/'))) { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(filepath, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_SplitLargeMeshes | aiProcess_OptimizeMeshes); @@ -134,7 +152,7 @@ Model::Model(const std::string& filepath) : } void Model::Draw(std::shared_ptr shader) { - for(const Mesh& mesh : this->meshes) + for(Mesh& mesh : this->meshes) mesh.Draw(shader, this->modelView); } @@ -196,24 +214,60 @@ Mesh Model::processMesh(aiMesh *mesh, const aiScene *scene) { } // process material + aiColor3D diffuse_color; + aiColor3D ambient_color; + aiColor3D specular_color; + float shininess; + if(mesh->mMaterialIndex >= 0) { + std::cout << "processing material of id: " << mesh->mMaterialIndex << std::endl; aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; + std::vector diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE); textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); + std::vector specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR); textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); + + if(AI_SUCCESS != material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse_color)) { + std::cout << "couldn't get diffuse color" << std::endl; + } + + if(AI_SUCCESS != material->Get(AI_MATKEY_COLOR_AMBIENT, ambient_color)) { + std::cout << "couldn't get ambient color" << std::endl; + } + + if(AI_SUCCESS != material->Get(AI_MATKEY_COLOR_SPECULAR, specular_color)) { + std::cout << "couldn't get specular color" << std::endl; + } + + if(AI_SUCCESS != material->Get(AI_MATKEY_SHININESS, shininess)) { + std::cout << "couldn't get shininess factor" << std::endl; + } } - return Mesh(vertices, indices, textures); + return Mesh( + vertices, + indices, + textures, + Material { + aiColorToGLM(diffuse_color), + aiColorToGLM(ambient_color), + aiColorToGLM(specular_color), + shininess + } + ); } std::vector Model::loadMaterialTextures(aiMaterial* mat, const aiTextureType& type) { std::vector textures; + std::cout << "material has " << mat->GetTextureCount(type) << " textures of type " << aiTextureTypeToString(type) << std::endl; for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); - Texture texture(std::string(str.C_Str()), type); + std::string textureFilepath = this->directory + "/" + std::string(str.C_Str()); + Texture texture(textureFilepath, type); textures.push_back(texture); } return textures; @@ -224,8 +278,10 @@ Texture::Texture(const std::string& filepath, const aiTextureType& type) { switch (type) { case aiTextureType_DIFFUSE: this->type = "texture_diffuse"; + break; case aiTextureType_SPECULAR: this->type = "texture_specular"; + break; default: throw std::invalid_argument(std::string("Unimplemented texture type ") + aiTextureTypeToString(type)); } @@ -234,12 +290,13 @@ Texture::Texture(const std::string& filepath, const aiTextureType& type) { glGenTextures(1, &textureID); int width, height, nrComponents; - std::cout << "attempting to load texture at " << filepath << std::endl; + std::cout << "Attempting to load texture at " << filepath << std::endl; unsigned char *data = stbi_load(filepath.c_str(), &width, &height, &nrComponents, 0); if (!data) { std::cout << "Texture failed to load at path: " << filepath << std::endl; stbi_image_free(data); } + std::cout << "Succesfully loaded texture at " << filepath << std::endl; GLenum format; if (nrComponents == 1) format = GL_RED; diff --git a/src/client/shader.cpp b/src/client/shader.cpp index 5433a8aa..4209a53f 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -1,10 +1,12 @@ #include -#include #include #include #include +#include +#include + #include "client/shader.hpp" Shader::Shader(const std::string& vertexPath, const std::string& fragmentPath) { @@ -106,6 +108,14 @@ void Shader::setFloat(const std::string &name, float value) const { glUniform1f(glGetUniformLocation(ID, name.c_str()), value); } +void Shader::setMat4(const std::string &name, glm::mat4& value) { + glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, reinterpret_cast(&value)); +} + +void Shader::setVec3(const std::string &name, glm::vec3& value) { + glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, reinterpret_cast(&value)); +} + GLuint Shader::getID() { return ID; } diff --git a/src/client/shaders/lightsource.frag b/src/client/shaders/lightsource.frag new file mode 100644 index 00000000..6b1b3513 --- /dev/null +++ b/src/client/shaders/lightsource.frag @@ -0,0 +1,7 @@ +#version 330 core +out vec4 FragColor; + +void main() +{ + FragColor = vec4(1.0); // set all 4 vector values to 1.0 +} diff --git a/src/client/shaders/lightsource.vert b/src/client/shaders/lightsource.vert new file mode 100644 index 00000000..820e2b27 --- /dev/null +++ b/src/client/shaders/lightsource.vert @@ -0,0 +1,14 @@ +#version 330 core +// NOTE: Do NOT use any version older than 330! Bad things will happen! + +layout (location = 0) in vec3 position; + +// Uniform variables +uniform mat4 viewProj; +uniform mat4 model; + +void main() +{ + // OpenGL maintains the D matrix so you only need to multiply by P, V (aka C inverse), and M + gl_Position = viewProj * model * vec4(position, 1.0); +} diff --git a/src/client/shaders/material.frag b/src/client/shaders/material.frag new file mode 100644 index 00000000..b0086ef5 --- /dev/null +++ b/src/client/shaders/material.frag @@ -0,0 +1,44 @@ +#version 330 core + +// Inputs to the fragment shader are the outputs of the same name from the vertex shader. +// Note that you do not have access to the vertex shader's default output, gl_Position. + +in vec3 fragNormal; +in vec3 fragPos; + +struct Material { + vec3 ambient; + vec3 diffuse; + vec3 specular; + float shininess; +}; + +uniform Material material; +uniform vec3 viewPos; + +uniform vec3 lightPos; +uniform vec3 lightColor; + +// You can output many things. The first vec4 type output determines the color of the fragment +out vec4 fragColor; + +void main() +{ + // ambient + vec3 ambient = lightColor * material.ambient; + + // diffuse + vec3 norm = normalize(fragNormal); + vec3 lightDir = normalize(lightPos - fragPos); + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = lightColor * (diff * material.diffuse); + + // specular + vec3 viewDir = normalize(viewPos - fragPos); + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + vec3 specular = lightColor * (spec * material.specular); + + vec3 result = ambient + diffuse + specular; + fragColor = vec4(result, 1.0); +} diff --git a/src/client/shaders/shader.frag b/src/client/shaders/shader.frag index 9b84c61c..3bc9cbee 100644 --- a/src/client/shaders/shader.frag +++ b/src/client/shaders/shader.frag @@ -4,6 +4,7 @@ // Note that you do not have access to the vertex shader's default output, gl_Position. in vec3 fragNormal; +in vec3 fragPos; // uniforms used for lighting uniform vec3 AmbientColor = vec3(0.2); diff --git a/src/client/shaders/shader.vert b/src/client/shaders/shader.vert index 56567efe..a3f529c8 100644 --- a/src/client/shaders/shader.vert +++ b/src/client/shaders/shader.vert @@ -11,7 +11,7 @@ uniform mat4 model; // Outputs of the vertex shader are the inputs of the same name of the fragment shader. // The default output, gl_Position, should be assigned something. out vec3 fragNormal; - +out vec3 fragPos; void main() { @@ -20,4 +20,6 @@ void main() // for shading fragNormal = vec3(model * vec4(normal, 0)); + //fragNormal = normal; + fragPos = vec3(model * vec4(position, 1.0)); } \ No newline at end of file diff --git a/src/client/util.cpp b/src/client/util.cpp index 4abfa034..39a0ffac 100644 --- a/src/client/util.cpp +++ b/src/client/util.cpp @@ -1,2 +1,6 @@ #include "client/util.hpp" +glm::vec3 aiColorToGLM(const aiColor3D& color) { + return glm::vec3(color.r, color.g, color.b); +} + From 1698639211012565391cf854d238c74552bc8379 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 3 May 2024 15:39:47 -0700 Subject: [PATCH 55/92] refactor sprite loading but it still doesn't work... --- include/client/gui/gui.hpp | 2 + include/client/gui/widget/staticimg.hpp | 5 +- src/client/client.cpp | 8 ++- src/client/gui/gui.cpp | 4 ++ src/client/gui/widget/dyntext.cpp | 4 +- src/client/gui/widget/staticimg.cpp | 69 +++++++++++-------------- src/client/shaders/img.frag | 16 ++++++ src/client/shaders/img.vert | 14 +++++ 8 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 src/client/shaders/img.frag create mode 100644 src/client/shaders/img.vert diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 864cd1aa..7342392e 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -47,6 +47,8 @@ enum class GUIState { */ class GUI { public: + static glm::mat4 projection; + /// ========================================================================= /// /// These are the functions that need to be called to setup a GUI object. Doing anything diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp index fc0867c2..4aca72d1 100644 --- a/include/client/gui/widget/staticimg.hpp +++ b/include/client/gui/widget/staticimg.hpp @@ -13,6 +13,9 @@ namespace gui::widget { * Widget to display a static image (png) to the screen. * * BUG: This doesn't work at all currently! ASDAJSHHASHDAHHHHHHH! + * + * Reference: The chapter on sprite rendering from + * https://learnopengl.com/book/book_pdf.pdf */ class StaticImg : public Widget { public: @@ -39,7 +42,7 @@ class StaticImg : public Widget { private: gui::img::Img img; - GLuint VBO, VAO, EBO; + GLuint quadVAO; }; } diff --git a/src/client/client.cpp b/src/client/client.cpp index 3ec5de45..d5dc4606 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -133,9 +133,15 @@ bool Client::init() { return false; } + auto imgShaderProgram = LoadShaders((shader_path / "img.vert").string(), (shader_path / "img.frag").string()); + if (!imgShaderProgram) { + std::cerr << "Failed to initialize img shader program" << std::endl; + return false; + } + // Init GUI (e.g. load in all fonts) // TODO: pass in shader for image loading in second param - if (!this->gui.init(textShaderProgram, textShaderProgram)) { + if (!this->gui.init(textShaderProgram, imgShaderProgram)) { std::cerr << "GUI failed to init" << std::endl; return false; } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 931d8445..fd6dc34b 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -10,6 +10,8 @@ namespace gui { +glm::mat4 GUI::projection = glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); + GUI::GUI(Client* client) { this->client = client; } @@ -32,6 +34,8 @@ bool GUI::init(GLuint text_shader, GLuint image_shader) // Need to register all of the necessary shaders for each widget widget::DynText::shader = text_shader; widget::StaticImg::shader = image_shader; + glUniformMatrix4fv(glGetUniformLocation(widget::DynText::shader, "projection"), 1, false, reinterpret_cast(&projection)); + glUniformMatrix4fv(glGetUniformLocation(widget::StaticImg::shader, "projection"), 1, false, reinterpret_cast(&projection)); std::cout << "Initialized GUI\n"; return true; diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 67d1eaf4..15547986 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -58,9 +58,7 @@ DynText::DynText(std::string text, std::shared_ptr fonts, Dyn void DynText::render() { glUseProgram(DynText::shader); - // todo move to gui - glm::mat4 projection = glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); - glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&projection)); + glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); glUniform3f(glGetUniformLocation(DynText::shader, "textColor"), this->options.color.x, this->options.color.y, this->options.color.z); glActiveTexture(GL_TEXTURE0); diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index a00c552c..749ffd98 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -1,5 +1,7 @@ #include "client/gui/gui.hpp" +#include "client/client.hpp" + namespace gui::widget { GLuint StaticImg::shader = 0; @@ -7,43 +9,26 @@ GLuint StaticImg::shader = 0; StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): Widget(Type::StaticImg, origin) { - // reference: https://learnopengl.com/code_viewer.php?code=getting-started/textures - - // Set up vertex data (and buffer(s)) and attribute pointers - GLfloat vertices[] = { - // Positions // Colors // Texture Coords - 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // Top Right - 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // Bottom Right - -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // Bottom Left - -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // Top Left - }; - GLuint indices[] = { // Note that we start from 0! - 0, 1, 3, // First Triangle - 1, 2, 3 // Second Triangle + // configure VAO/VBO + unsigned int VBO; + float vertices[] = { + // pos // tex + 0.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 0.0f, 1.0f, 0.0f }; - glGenVertexArrays(1, &VAO); + glGenVertexArrays(1, &quadVAO); glGenBuffers(1, &VBO); - glGenBuffers(1, &EBO); - - glBindVertexArray(VAO); - glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - - // Position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0); + glBindVertexArray(quadVAO); glEnableVertexAttribArray(0); - // Color attribute - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat))); - glEnableVertexAttribArray(1); - // TexCoord attribute - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat))); - glEnableVertexAttribArray(2); - - glBindVertexArray(0); // Unbind VAO + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float),(void*)0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); this->width = img.width; this->height = img.height; @@ -55,13 +40,19 @@ StaticImg::StaticImg(gui::img::Img img): void StaticImg::render() { glUseProgram(StaticImg::shader); - // Bind Texture - glBindTexture(GL_TEXTURE_2D, this->img.texture_id); - // Draw container - glBindVertexArray(VAO); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - glBindVertexArray(0); - glBindTexture(GL_TEXTURE_2D, 0); + glm::mat4 model = glm::mat4(1.0f); + model = glm::translate(model, glm::vec3(origin, 0.0f)); + model = glm::translate(model, glm::vec3(0.5*width, 0.5*height, 0.0)); + model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0, 0.0, 1.0)); + model = glm::translate(model, glm::vec3(-0.5*width, -0.5*height, 0.0)); + model = glm::scale(model, glm::vec3(glm::vec2(width, height), 1.0f)); + glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); + glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "model"), 1, false, reinterpret_cast(&model)); + glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 0.5f, 0.5f, 0.5f); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, img.texture_id); + glBindVertexArray(quadVAO); + glDrawArrays(GL_TRIANGLES, 0, 6); glUseProgram(0); } diff --git a/src/client/shaders/img.frag b/src/client/shaders/img.frag new file mode 100644 index 00000000..b8a4dac8 --- /dev/null +++ b/src/client/shaders/img.frag @@ -0,0 +1,16 @@ +#version 330 core +layout (location = 0) in vec4 vertex // + +// Reference: The chapter on sprite rendering from +// https://learnopengl.com/book/book_pdf.pdf + +out vec2 TexCoords; + +uniform mat4 model; +uniform mat4 projection; + +void main() +{ + TexCoords = vertex.zw; + gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0); +} \ No newline at end of file diff --git a/src/client/shaders/img.vert b/src/client/shaders/img.vert new file mode 100644 index 00000000..f0813055 --- /dev/null +++ b/src/client/shaders/img.vert @@ -0,0 +1,14 @@ +#version 330 core +in vec2 TexCoords +out vec4 color + +// Reference: The chapter on sprite rendering from +// https://learnopengl.com/book/book_pdf.pdf + +uniform sampler2D image; +uniform vec3 spriteColor; + +void main() +{ + color = vec4(spriteColor, 1.0) * texture(image, texCoords); +} \ No newline at end of file From 0552cc9fac38be394714e51d95442d6c48162d22 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 3 May 2024 15:49:51 -0700 Subject: [PATCH 56/92] fix frag and vert shaders being swapped, but still doesn't work --- src/client/gui/gui.cpp | 2 +- src/client/gui/widget/staticimg.cpp | 2 ++ src/client/shaders/img.frag | 12 +++++------- src/client/shaders/img.vert | 12 +++++++----- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index fd6dc34b..01422824 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -189,7 +189,7 @@ void GUI::_layoutTitleScreen() { this->addWidget(std::move(start_flex)); auto yoshi = widget::StaticImg::make( - glm::vec2(0.0f, 0.0f), + glm::vec2(200.0f, 200.0f), this->images.getImg(img::ImgID::Yoshi) ); this->addWidget(std::move(yoshi)); diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 749ffd98..8e9fee27 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -11,6 +11,8 @@ StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): { // configure VAO/VBO unsigned int VBO; + // there might be some mismatch here because this might be assuming that the top left + // corner is 0,0 when we are specifying origin by bottom left coordinate float vertices[] = { // pos // tex 0.0f, 1.0f, 0.0f, 1.0f, diff --git a/src/client/shaders/img.frag b/src/client/shaders/img.frag index b8a4dac8..62e36c4a 100644 --- a/src/client/shaders/img.frag +++ b/src/client/shaders/img.frag @@ -1,16 +1,14 @@ #version 330 core -layout (location = 0) in vec4 vertex // +in vec2 TexCoords +out vec4 color // Reference: The chapter on sprite rendering from // https://learnopengl.com/book/book_pdf.pdf -out vec2 TexCoords; - -uniform mat4 model; -uniform mat4 projection; +uniform sampler2D image; +uniform vec3 spriteColor; void main() { - TexCoords = vertex.zw; - gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0); + color = vec4(spriteColor, 1.0) * texture(image, TexCoords); } \ No newline at end of file diff --git a/src/client/shaders/img.vert b/src/client/shaders/img.vert index f0813055..b8a4dac8 100644 --- a/src/client/shaders/img.vert +++ b/src/client/shaders/img.vert @@ -1,14 +1,16 @@ #version 330 core -in vec2 TexCoords -out vec4 color +layout (location = 0) in vec4 vertex // // Reference: The chapter on sprite rendering from // https://learnopengl.com/book/book_pdf.pdf -uniform sampler2D image; -uniform vec3 spriteColor; +out vec2 TexCoords; + +uniform mat4 model; +uniform mat4 projection; void main() { - color = vec4(spriteColor, 1.0) * texture(image, texCoords); + TexCoords = vertex.zw; + gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0); } \ No newline at end of file From a2faa763757287da67eee01787360697149b9517 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Fri, 3 May 2024 16:05:16 -0700 Subject: [PATCH 57/92] messing around w/ stuff and it still doesn't work --- assets/imgs/pikachu.png | Bin 0 -> 8137 bytes src/client/gui/imgs/loader.cpp | 23 +++++++++-------------- src/client/gui/widget/staticimg.cpp | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) create mode 100644 assets/imgs/pikachu.png diff --git a/assets/imgs/pikachu.png b/assets/imgs/pikachu.png new file mode 100644 index 0000000000000000000000000000000000000000..b0fa72a2853168c09b4da63d9a4771e7ac3b1687 GIT binary patch literal 8137 zcmV;)A2#5LP)r79}w%9RM^rNdN*Uh>DT` z7$#0E07*(p86PkJ4gd@C=S~y2)16Mv9!8db;2)AD zW6PE-8H3Cj+t`u;*-42(#%_cg2;e|@Bq41`)1*z>*XFh5J^tjkyZ3AFZuP^m^`o6n zf_|Ov_IoosGdnvockW!j&@c@3dI%k(U&BhThtMf{2>_RRJ&4ZG28ITiUXP+9G((`l za<7Nc5xPdPecJ0`bb_*AQm@C+0UEKO{U=(1>o&IkJY_OwLxW)s>!F-A zWgKX5qt|0OYr5uQyWZ=uoH4nx?aU3_a<(`&(o09~C4=pZ4cv+)4aFYWmLcdIwoC4b zR}v#{x3k#Bg5dC4 zsw;DE2IA?CVsn-lUV4@|`z!frA*t)e%}!!-9dU+6ON)>l?!d)Zf|R15(cMmBWBneR z;wWyc!b(TYUSd-hMNw0BJBe*10BupncT^uO!_rJAE@;wHO%((|R7WkbyxEbf1oX)f z!WR`)9j(FMSVt~N{Dx71l4_V2?NGqPXJ}v(iR7xHf>0WEI-@ib>*NNKd#IJ@MLV?) z@F~li$h@Wa(gfW?C){aMBq`~uoy2B)%{C$XvJ_nsHqQQvV4olVuaIkYE^6)tr-;s# zF(IWed)D?YGjHENQ52|Db}**k@hnp=GLc+dPaQovTcxs)R^6K?p!v?mOIRCj$fmGZ zGJ};_Jb$)I1q=%ry@HzWaGZv9xK&NykgAu?D$=$`BtvA*t#v$RZrizCWZzJhd7_BW z>e;Xk6oHx1m{OhJT(muu35CZvOnLa z-zl}vy0t}BBtjVqg(NJP3hSOY^G`3F#vT>Rn!xHwQ5f&Ed2?w9Ac`4B&|Owo=gZM9 zdhHaB#6n3GJm*ucLKnl@*P%){X9}h)1V}{r_PyIjhF8=@rP|f4{32|{EY?klis+L` z{GVpD4Gwg{Bj@t?_Tw`hd@~w({6sQYM+7s-I#ErvX~u^(eX2yhyw&Y|t}M)%tiysi zVVZK!v{OghTNxw@n)gJ?)L{o!_Kj)i^V<&Vu%i-S9R}BqSRSXnmRlAh(>=(*a9I~3 z8q;eIFu0WMq702?5|lDCO&$8ByuIc&Pozvp(CHvHk4E;Us~*8*XB-8WNG$J}PUzEI zZjrv3U0NtMh}u3+dD*W_byrQglz{z^Uoah=X%ql9SIg;AK85tSj{ifcd?{TfAfFxA zvB#i^%b~dM#32TC)!_VNLNZZSnx~dSWY9%a^{;d6`Ssw z$Hk$UJr|uNn0{L>cr}d)P}7yPe={#p_Q<3>9**`nqjzi%`_GYRT~p4g7$z8j;l=Zd z&slWYHBU4~xf-f=*DK-B%nlq>V+5Y$_c8s^6?VZciWHq|z>aIIc@t$RvULVwiqq0? zcJ{%R6vK)oJdsy9D?5`@K~nV!EZLs^jwkvlPlO}4yRZY@0?lN3Ef)CaH*m)KPppFq?-$z#xzn~{`{5BXBuyrZV^h&KcX0jTjCr>G)eVNuOYE? zC(rIwcsb?+%e=LMNK1z5+y_w^?2qZ@(4#b}&mSH{>q}&iRnd|Y~aqzL8QE9?Yv=4 z5)Jia?Z&4)!wbt7b#Bf%tpbMSb&26nWP7jIh#VR#Aki<8^)zNzZVkZ`wG5LvBXPd$2Gu%WA!Re`cCveiE|509)^ zhbXqmGm|uWhr*#yta}>)&B#o7p-^PiiX;d;J~4b1eDlp_AYof{`-@&fa_F}u3yqA7 zQXyBUXpL5>jft}Decx!3j2WU51vPbK?fRP5VwFf;5R&c45F0zGYXgM9CsE7w zPb^`>)`+6ULik$;mWq`>G<;HIKg`tHC{CmDcWETVA$G53H}X`F@CSC9K(o z$;8HRMq4c)RV4{Cnud^QNPGx%U?OzUI($IV3Le^^1;Kq7co72Wo=}*_6>c+V6)bM z*qiK&o#5-Qee;dg7MLp+J#-gJuKP?6Q)|A}9{Kw|vD^%{ugd|x<$c&LzQGQ(HzJGH zUAEb4K-3=rg&Qumhb7e)|4=I!+uOSj?oCckPMc!+7LHOHBa;|h&V^5eLxxKY&r&Fv8t+~oX_LBG?OW2j~5n}_O5q1(vVHY5u2>aa$xNb0c=Dp$pV|p zH%-geN+Pp(8_pc>-2vFl6_RV|TJ5OIhHnUvC)193ucd#y!eQL+Z!*0!7#~eb6=3qs zU9jUoGspK~zFabpjtQH06o1;m9z|SwRPkzmm(o zY?|y_KJKKpG*{e^xdBQM%iyLJLXFukI{k zmx2JcD}PRE?12{7(8q4z3Z-)3gQUHS26ey^d@Lxyb^1K~23!Oy$V@1wW_uuckP z^Z81twpLyT*e#SqqtA?Kg;Se2Nsv zu0t-5+^|3T!PF*KKKSd$h~)vx4sey^SG4VnpR*=10qrhzSd)y*9;I~ElW#3JAT@n7 z37Zuscr2&j$17K^eDEWz>GhVQse6pw_IC?%E~4dA@lNmX&p<8i?Pcg?f`IfI92s_~ zhor&&>94Sw@`S-W?-;&eZ!7q_DFEf?vy<~LI|^+G@`yh~t6+*&Hz)D((~3T-s8XsZ zI%i|i;fP}b?iK1W7f)QgIw*ElB=6)kkuhY<(uQ50qSWGU$}ac9>c(fSyz@;fe{C}E zUJm)r=p2T2@9gYkHxik#k&eX`mXdbK4YRWHjMQYF8ZVQQDA80iUhxu`1L+6tr!NQ2 z4}1azo#RHDN0e{oOX*~Jb)9ycxc_Wt;ka1DK+h0b23kx%69v5lNlgpZ2#7%+Urqh| z@bzm*@{4cn8S+K%P9TW%=qh7>zTIC_+(GFhJc%ufy}>Hu(44N!=AZue|w2NjZYSaYbR%hKJPr zqSf{=Bw>KzaMUbtCI?oMh1zHwS$8T>--xA4oC=3xw@9)&@5Tind5@rbo4Q_Eg}v6` z`AgHAVG~kdjE6%ON#Q}{G*cCkj*#H>SlAD;a+%ck9e`(LP)9fe_r=C66L^?=E$Eix zvL`pBu1q>#Y9&CI!xpmWINPOGWcnm|)IWVu#xc`EkvYhK60qeyG{F&x?7Y!*RNI7w zX6ZO%lPAmgSB?DBwfN<*BvEAb8vF(cGfl$Ks*uU-@-ErT_;?N_%dS)s9yLj%n$4>X z?qWbS8*o%eA$?+M>Z*;*f5SOS2wUqY;WgI&MW={~&o1syfdi>((My|MXK1rS64AXUh4^ zg{bPO)dt-a6E}6D%%#ZG;N^)47NmQNWSLrOHBX9H>a={Sck^YU_!($Jv>^(ae_`z4 zc)&Gg6IY!Rbc^*LY_eA#l&HBf4VD!3fK&*q@rl8ysc{V@3yLwy!r+-A&orUd+JqJ| zZ*XFhMwykUgca>o!7rE%<{MulX!%A#clhr}h#?zf*yQBp!Kn~oYjuCMl0QO9LxtvF zrkaYMP(-r!*^cSTLpF^h?b_CqYXObMP-AH4Mqn-$;*bdv{}YPFs)tZW?>f($5%}}b zJN2+An>r*!mA|F1pP8y;!op1YTsAu+=L+_VW;EF<8M3?d@+lIlzJNkXNAbMD#}b_T zrnQdR9!zB0>DF&njVptar9YO38E!`Oux5nsSZOGHgYC=%UeRBo^JiMG{z9KQSQr{M#*yHneEu z@C#?ZXeu+-DE1>PW`MX z*qn(-pD`J|3+B$pga;c{DuSPN%`Vz1G=eVs7@pj|wSO=-cd&o!Z$P)G@R~&p+%?g2 zUC!+mXjeZ+i?GJkjH}6)rI3$kk5cbB+$-`fbOLCEjtlwPTC%XZx>{Z<85ozQiagWW zoQ=v=%V6WF=2?z_;X&>}7hziiH*=mbv=im!LmnY>^9|9kIGY^m)l!XZfN0BHB=q8Q zPmpTHj2S8%Xtq*eSyulFCxtd5?JSes>aka4k!A_qHZe00EleBGFtb}H8uNtboZg?(yFHSU}^i0QOM5Z&Tyk zM_KI4Y+Mm6kXwZv9mO@%UuMJ;LvsW zY^xdzu#Y~CC`fHVJ7*Rd^5&1==OfRSdu}p`(Q@q1Fi|D!BEanZlv%g~hua}P`@(~Y z?7y&@y4eP_#7TA;Ib`n;TDO;LTdfUfXAuyYvwJ@cZS2AOt*RGZIKo&1v%wGdmNliN zGMiP{;(QZ4hceEXTH)m~WpkcIKs3sFyO$Cx0Q=jlav>^(l?)86ot*ni&9?hm;2iT_ zEt%Ms7m8zU0C9(AWh(X}AhPeb6Ek=%@?wr*U-&9B>Dq*Lu6Pp#r&eIhK_9D*Emy4^ zw3Qd-d6?bFW~eCLAwAz6%Z(KYCvG}i+<|)sj&cc4$+Bf^Rk6)9(!Dj_&U;;z*-V#e zX>%rR9L9urs+3#@*jp%?j*nt1lK1sWkTT1&aBEw#TKrqa07Gl9klNBhD|OLCx!m5WP9R~j-E71W;46%ld@kPcHnw4qqNZo>geFW?5|vuD zR7ANz`Dxo}&De)My{EdyH|p&}W(b&|o3EwrCQdf)6W(8heLbG&Akn0~Qs znkw={6Z*TUO1jrXbO>7C+-;52nuYE9T!is41UsG(<1T?ZLD^M}5y@8Oo4_Pl@QznY z7Pf^gXOqa3OS%Yf9m$T?H%>G z@>aQe;svw0N92+=k>2lG!S`~3m+|KlH$O7Y*X&!nV@YPx-rI{1A0s?C9~SIRgG+YrRaN;Hb6*d%=j6#fu<(zQ~>F<|{Ik68|mJHWNzffStG|Lzj} zJzD)G@=G*wuy8y}m)EiF&$|VUR+qL!t(bTF7;W8(1;NE%{^y^ue?-HFm{~*FJ-EHy z#fmpyn}gVccA|W8HzL_wS4#8g$ozn{p(#X+F1F!IR$vp9t+VC zz^5A{=QbW*AoT7+587Cn53Xo1-^GM{GoG@mY%-wb{amt{@{X1J;1ipxU+mq zxe+pF2SGM~AbR`oe=ThP*L`euHg+AKt6^SVLmv$7zPZ5CwXQ=;tfM+(_bUkh*cs{M zg4W8DEVQ?st>WX+Qn0+HWooQt)I6}*-gN!3Kf|E$YuN&v@E*hpBj47GB7(aNw!7Va zAo;oCD&OB~Q}KI9D@(Gad7?L4X0sSsX6X>;Q^+`jFu!?<=RV4lG%G&LON0uJ+-zAPw9A_ z0=Q7My1l)%f9w4u!0hACNvo+B`&(NF2N>V2TT&BngXm;6TxU#)`$t9`nSmWeiAgj) zDLRK4ba?AFMl!p!bDS+E#)iz>KT}+O3Gifpt}4Z1n3s*gMM^z<5P4>H-iRly<_5Az zM36wMF5Wskn0r#Jvp5+^WOrr>!Yz`!8L5j?_kR8k+^%#${I$`rx$0_*v`Wj2@p#I#qnk}^3e*s-y#)56Nog26-mDtwE~+ZRDiWy4~^Z~(AdVx7n8p!8JcD_ofcK4 z47*0^=6bq8dzgP48Xn8;Tw71(1=3WTbYvnznJUBD)2?p{cZP;D*`>uoDMg!x6LjS~ z%=>G-|D9=OV-Iq*6jD`!u2z8lc);D6QDO%+$+O|&qq{Y3N!-6TjU7LKTBsQYtpDd> j!!%MnUYgBz&Iimg_map.insert({img_id, Img { diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 8e9fee27..af89e444 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -50,7 +50,7 @@ void StaticImg::render() { model = glm::scale(model, glm::vec3(glm::vec2(width, height), 1.0f)); glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "model"), 1, false, reinterpret_cast(&model)); - glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 0.5f, 0.5f, 0.5f); + glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 1.0f, 1.0f, 1.0f); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, img.texture_id); glBindVertexArray(quadVAO); From f263d923cf96b754f4d110ee9ee19239580c2d5c Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 4 May 2024 15:48:23 -0700 Subject: [PATCH 58/92] move graphics models/materials/textures to assets/graphics --- .../models => assets/graphics}/.gitignore | 0 include/client/client.hpp | 10 ++--- src/client/client.cpp | 37 ++++++++++--------- src/client/model.cpp | 35 ++++++++++-------- 4 files changed, 43 insertions(+), 39 deletions(-) rename {src/client/models => assets/graphics}/.gitignore (100%) diff --git a/src/client/models/.gitignore b/assets/graphics/.gitignore similarity index 100% rename from src/client/models/.gitignore rename to assets/graphics/.gitignore diff --git a/include/client/client.hpp b/include/client/client.hpp index 9f530492..462bc5ea 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -58,13 +58,13 @@ class Client { SharedGameState gameState; - std::shared_ptr cubeShader; - std::shared_ptr bearShader; - std::shared_ptr lightSourceShader; + std::shared_ptr cube_shader; + std::shared_ptr model_shader; + std::shared_ptr light_source_shader; - std::unique_ptr lightSource; + std::unique_ptr bear_model; + std::unique_ptr light_source; - std::unique_ptr bearModel; float playerMovementDelta = 0.05f; GLFWwindow *window; diff --git a/src/client/client.cpp b/src/client/client.cpp index abcf1151..dd1aa1ec 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -108,24 +108,25 @@ bool Client::init() { std::cout << "shader version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl; std::cout << "shader version: " << glGetString(GL_VERSION) << std::endl; + boost::filesystem::path shaders_dir = this->root_path / "src/client/shaders"; + boost::filesystem::path graphics_assets_dir = this->root_path / "assets/graphics"; - boost::filesystem::path cubeVertFilepath = this->root_path / "src/client/shaders/shader.vert"; - boost::filesystem::path cubeFragFilepath = this->root_path / "src/client/shaders/shader.frag"; - this->cubeShader = std::make_shared(cubeVertFilepath.string(), cubeFragFilepath.string()); + boost::filesystem::path cube_vert_path = shaders_dir / "shader.vert"; + boost::filesystem::path cube_frag_path = shaders_dir / "shader.frag"; + this->cube_shader = std::make_shared(cube_vert_path.string(), cube_frag_path.string()); - boost::filesystem::path playerVertFilepath = this->root_path / "src/client/shaders/model.vert"; - boost::filesystem::path playerFragFilepath = this->root_path / "src/client/shaders/model.frag"; - this->bearShader = std::make_shared(playerVertFilepath.string(), playerFragFilepath.string()); + boost::filesystem::path model_vert_path = shaders_dir / "model.vert"; + boost::filesystem::path model_frag_path = shaders_dir / "model.frag"; + this->model_shader = std::make_shared(model_vert_path.string(), model_frag_path.string()); - boost::filesystem::path playerModelFilepath = this->root_path / "src/client/models/bear-sp22.obj"; - this->bearModel = std::make_unique(playerModelFilepath.string()); - this->bearModel->Scale(0.25); + boost::filesystem::path bear_model_path = graphics_assets_dir / "bear-sp22.obj"; + this->bear_model = std::make_unique(bear_model_path.string()); + this->bear_model->Scale(0.25); - this->lightSource = std::make_unique(); + this->light_source = std::make_unique(); boost::filesystem::path lightVertFilepath = this->root_path / "src/client/shaders/lightsource.vert"; boost::filesystem::path lightFragFilepath = this->root_path / "src/client/shaders/lightsource.frag"; - this->lightSourceShader = std::make_shared(lightVertFilepath.string(), lightFragFilepath.string()); - + this->light_source_shader = std::make_shared(lightVertFilepath.string(), lightFragFilepath.string()); return true; } @@ -241,15 +242,15 @@ void Client::draw() { continue; } if (sharedObject->type == ObjectType::Enemy) { - this->bearModel->TranslateTo(sharedObject->physics.position); - this->bearModel->Draw(this->cam->getViewProj(), this->cam->getPos(), this->bearShader, this->cam->getPos()); + this->bear_model->TranslateTo(sharedObject->physics.position); + this->bear_model->Draw(this->cam->getViewProj(), this->cam->getPos(), this->model_shader, this->cam->getPos()); } // Get camera position from server, update position and don't render player object (or special handling) if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { cam->updatePos(sharedObject->physics.position); - this->lightSource->TranslateTo(cam->getPos()); - this->lightSource->draw(this->lightSourceShader); + this->light_source->TranslateTo(cam->getPos()); + this->light_source->draw(this->light_source_shader); continue; } @@ -257,14 +258,14 @@ void Client::draw() { if(sharedObject->solidSurface.has_value()){ Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f), sharedObject->solidSurface->dimensions); cube->update(sharedObject->physics.position); - cube->draw(this->cam->getViewProj(), this->cubeShader->getID(), true); + cube->draw(this->cam->getViewProj(), this->cube_shader->getID(), true); continue; } // tmp: all objects are cubes Cube* cube = new Cube(glm::vec3(0.0f,1.0f,1.0f), glm::vec3(1.0f)); cube->update(sharedObject->physics.position); - cube->draw(this->cam->getViewProj(), this->cubeShader->getID(), false); + cube->draw(this->cam->getViewProj(), this->cube_shader->getID(), false); } } diff --git a/src/client/model.cpp b/src/client/model.cpp index e2ff7298..c9e43181 100644 --- a/src/client/model.cpp +++ b/src/client/model.cpp @@ -90,23 +90,26 @@ void Mesh::Draw(std::shared_ptr shader, glm::mat4 model, glm::mat4 viewP shader->setVec3("material.specular", this->material.specular); shader->setFloat("material.shininess", this->material.shininess); - unsigned int diffuseNr = 1; - unsigned int specularNr = 1; - for(unsigned int i = 0; i < textures.size(); i++) { - glActiveTexture(GL_TEXTURE0 + i); // activate proper texture unit before binding - // retrieve texture number (the N in diffuse_textureN) - std::string number; - std::string name = textures[i].getType(); - if(name == "texture_diffuse") - number = std::to_string(diffuseNr++); - else if(name == "texture_specular") - number = std::to_string(specularNr++); - - std::string shaderTextureName = "material." + name + number; - shader->setInt(shaderTextureName, i); - glBindTexture(GL_TEXTURE_2D, textures[i].getID()); + if (textures.size() == 0) { + } else { + unsigned int diffuseNr = 1; + unsigned int specularNr = 1; + for(unsigned int i = 0; i < textures.size(); i++) { + glActiveTexture(GL_TEXTURE0 + i); // activate proper texture unit before binding + // retrieve texture number (the N in diffuse_textureN) + std::string number; + std::string name = textures[i].getType(); + if(name == "texture_diffuse") + number = std::to_string(diffuseNr++); + else if(name == "texture_specular") + number = std::to_string(specularNr++); + + std::string shaderTextureName = "material." + name + number; + shader->setInt(shaderTextureName, i); + glBindTexture(GL_TEXTURE_2D, textures[i].getID()); + } + glActiveTexture(GL_TEXTURE0); } - glActiveTexture(GL_TEXTURE0); // draw mesh glBindVertexArray(VAO); From f8b35f2307100a2ebda3c36ed8e7d0173f3bd77d Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 4 May 2024 15:48:47 -0700 Subject: [PATCH 59/92] update make pull_models to pull entire drive folder --- CMakeLists.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 508e864b..6d5400af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,8 +76,5 @@ add_custom_target(lint add_custom_target(pull_models COMMAND - gdown 133bVNM4_27hg_VoZGo9EUfCn7n6VXzOU -O ${CMAKE_SOURCE_DIR}/src/client/models/Player1-fire.obj && - gdown 1v3XO_E1ularO5Ku2WaA8O9GqE402a8cx -O ${CMAKE_SOURCE_DIR}/src/client/models/bear-sp22.obj && - gdown 1hHK-0iKMT6uboFUl3DXC9JcbnfsNCNF4 -O ${CMAKE_SOURCE_DIR}/src/client/models/cube.obj && - gdown 1mEWRgBP7G-s3XOr6NO9yichD4_eKc3JH -O ${CMAKE_SOURCE_DIR}/src/client/models/teapot.obj + gdown 1N7a5cDgMcXbPO0RtgznnEo-1XUfdMScM -O ${CMAKE_SOURCE_DIR}/assets/graphics --folder ) From 070d07d7b0b5da108afd75047739e32ef7e76513 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 4 May 2024 23:19:55 -0700 Subject: [PATCH 60/92] add renderable class all renderable items inherit from the renderable class to avoid redefining functionality they all share (translation, scaling) --- include/client/cube.hpp | 16 ++++---- include/client/model.hpp | 41 +++++++++++++++----- include/client/renderable.hpp | 70 +++++++++++++++++++++++++++++++++++ src/client/CMakeLists.txt | 1 + src/client/client.cpp | 29 ++++++++++----- src/client/cube.cpp | 51 ++++++------------------- src/client/model.cpp | 65 ++++++++++++++++++++++---------- src/client/renderable.cpp | 28 ++++++++++++++ src/client/shader.cpp | 6 +-- 9 files changed, 219 insertions(+), 88 deletions(-) create mode 100644 include/client/renderable.hpp create mode 100644 src/client/renderable.cpp diff --git a/include/client/cube.hpp b/include/client/cube.hpp index 20ea8d55..6a191736 100644 --- a/include/client/cube.hpp +++ b/include/client/cube.hpp @@ -1,6 +1,7 @@ #pragma once #include "client/core.hpp" +#include "client/renderable.hpp" #define GLM_ENABLE_EXPERIMENTAL #include @@ -11,23 +12,22 @@ #include #include -class Cube { +class Cube : public Renderable { public: - Cube(glm::vec3 newColor, glm::vec3 scale); + Cube(glm::vec3 newColor); ~Cube(); - void draw(glm::mat4 viewProjMat, GLuint shader, bool fill); - void update(glm::vec3 new_pos); - void update_delta(glm::vec3 delta); - + void draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) override; private: GLuint VAO; GLuint VBO_positions, VBO_normals, EBO; - glm::mat4 model; glm::vec3 color; - // Cube Information std::vector positions; std::vector normals; diff --git a/include/client/model.hpp b/include/client/model.hpp index 6797ab99..74be2229 100644 --- a/include/client/model.hpp +++ b/include/client/model.hpp @@ -12,6 +12,7 @@ #include #include "assimp/material.h" +#include "client/renderable.hpp" #include "client/shader.hpp" /** @@ -64,7 +65,7 @@ struct Material { * smaller meshes. This is useful for animating parts of model individual (ex: legs, * arms, head) */ -class Mesh { +class Mesh : public Renderable { public: /** * Creates a new mesh from a collection of vertices, indices and textures @@ -84,7 +85,11 @@ class Mesh { * @param modelView determines the scaling/rotation/translation of the * mesh */ - void Draw(std::shared_ptr shader, glm::mat4 model, glm::mat4 viewProj, glm::vec3 camPos, glm::vec3 lightPos); + void draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) override; private: std::vector vertices; std::vector indices; @@ -96,7 +101,7 @@ class Mesh { }; -class Model { +class Model : public Renderable { public: /** * Loads Model from a given filename. Can be of format @@ -113,7 +118,11 @@ class Model { * @param Shader to use while drawing all the * meshes of the model */ - void Draw(glm::mat4 viewProj, glm::vec3 camPos, std::shared_ptr shader, glm::vec3 lightPos); + void draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) override; /** * Sets the position of the Model to the given x,y,z @@ -121,7 +130,16 @@ class Model { * * @param vector of x, y, z of the model's new position */ - void TranslateTo(const glm::vec3& new_pos); + void translateAbsolute(const glm::vec3& new_pos); + + /** + * Updates the position of the Model relative to it's + * previous position + * + * @param vector of x, y, z of the change in the Model's + * position + */ + void translateRelative(const glm::vec3& delta); /** * Scale the Model across all axes (x,y,z) @@ -131,9 +149,16 @@ class Model { * Ex: setting it to 0.5 will cut the model's rendered size * in half. */ - void Scale(const float& new_factor); + void scale(const float& new_factor); - void setModelView(const glm::mat4& modelView); + /** + * Scale the model across all axes (x,y,z) + * by the scale factor in each axis. + * + * @param the scale vector describes how much to independently scale + * the model in each axis (x, y, z) + */ + void scale(const glm::vec3& scale); private: std::vector meshes; @@ -141,8 +166,6 @@ class Model { Mesh processMesh(aiMesh* mesh, const aiScene* scene); std::vector loadMaterialTextures(aiMaterial* mat, const aiTextureType& type); - glm::mat4 model; - // store the directory of the model file so that textures can be // loaded relative to the model file std::string directory; diff --git a/include/client/renderable.hpp b/include/client/renderable.hpp new file mode 100644 index 00000000..b0b5f9a6 --- /dev/null +++ b/include/client/renderable.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include + +#include "client/shader.hpp" + +class Renderable { + public: + Renderable(); + /** + * Draws the renderable item + * + * @param Shader to use while drawing all the + * meshes of the model + * @param + */ + virtual void draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) = 0; + + /** + * Sets the position of the item to the given x,y,z + * values + * + * @param vector of x, y, z of the 's new position + */ + void translateAbsolute(const glm::vec3& new_pos); + + /** + * Updates the position of the item relative to it's + * previous position + * + * @param vector of x, y, z of the change in the item's + * position + */ + void translateRelative(const glm::vec3& delta); + + /** + * Scale the Model across all axes (x,y,z) + * by a factor + * + * @param new_factor describes how much to scale the model by. + * Ex: setting it to 0.5 will cut the model's rendered size + * in half. + */ + void scale(const float& new_factor); + + /** + * Scale the item across all axes (x,y,z) + * by the scale factor in each axis. + * + * @param the scale vector describes how much to independently scale + * the item in each axis (x, y, z) + */ + void scale(const glm::vec3& scale); + + /** + * Gets the model matrix given all the transformations + * applied to it + * + * @return updated model matrix + */ + glm::mat4 getModelMat(); + private: + glm::mat4 model; +}; diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 199ec06c..6fe0794a 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -11,6 +11,7 @@ set(FILES shader.cpp model.cpp lightsource.cpp + renderable.cpp ${imgui-source} ) diff --git a/src/client/client.cpp b/src/client/client.cpp index dd1aa1ec..fda63733 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -11,7 +11,9 @@ #include "client/lightsource.hpp" #include "client/shader.hpp" #include "client/model.hpp" +#include "glm/fwd.hpp" #include "shared/game/event.hpp" +#include "shared/game/sharedobject.hpp" #include "shared/network/constants.hpp" #include "shared/network/packet.hpp" #include "shared/utilities/config.hpp" @@ -121,7 +123,7 @@ bool Client::init() { boost::filesystem::path bear_model_path = graphics_assets_dir / "bear-sp22.obj"; this->bear_model = std::make_unique(bear_model_path.string()); - this->bear_model->Scale(0.25); + this->bear_model->scale(0.25); this->light_source = std::make_unique(); boost::filesystem::path lightVertFilepath = this->root_path / "src/client/shaders/lightsource.vert"; @@ -242,8 +244,13 @@ void Client::draw() { continue; } if (sharedObject->type == ObjectType::Enemy) { - this->bear_model->TranslateTo(sharedObject->physics.position); - this->bear_model->Draw(this->cam->getViewProj(), this->cam->getPos(), this->model_shader, this->cam->getPos()); + this->bear_model->translateAbsolute(sharedObject->physics.position); + this->bear_model->draw( + this->model_shader, + this->cam->getViewProj(), + this->cam->getPos(), + this->cam->getPos(), + true); } // Get camera position from server, update position and don't render player object (or special handling) @@ -256,16 +263,18 @@ void Client::draw() { // If solidsurface, scale cube to given dimensions if(sharedObject->solidSurface.has_value()){ - Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f), sharedObject->solidSurface->dimensions); - cube->update(sharedObject->physics.position); - cube->draw(this->cam->getViewProj(), this->cube_shader->getID(), true); + std::cout << "solid surface has type " << objectTypeString(sharedObject->type) << std::endl; + Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f)); + cube->scale( sharedObject->solidSurface->dimensions); + cube->translateAbsolute(sharedObject->physics.position); + cube->draw(this->cube_shader, this->cam->getViewProj(), this->cam->getPos(), glm::vec3(), true); continue; } - // tmp: all objects are cubes - Cube* cube = new Cube(glm::vec3(0.0f,1.0f,1.0f), glm::vec3(1.0f)); - cube->update(sharedObject->physics.position); - cube->draw(this->cam->getViewProj(), this->cube_shader->getID(), false); + // original center cube because why not + Cube* cube = new Cube(glm::vec3(0.0f,1.0f,1.0f)); + cube->translateAbsolute(sharedObject->physics.position); + cube->draw(this->cube_shader, this->cam->getViewProj(), this->cam->getPos(), glm::vec3(), false); } } diff --git a/src/client/cube.cpp b/src/client/cube.cpp index 5b01e8ef..163ef89e 100644 --- a/src/client/cube.cpp +++ b/src/client/cube.cpp @@ -1,16 +1,11 @@ #include "client/cube.hpp" -Cube::Cube(glm::vec3 newColor, glm::vec3 scale) { +Cube::Cube(glm::vec3 newColor) { // create a vertex buffer for positions and normals // insert the data into these buffers // initialize model matrix - // Model matrix. glm::vec3 cubeMin = glm::vec3(-1.0f, -1.0f, -1.0f); glm::vec3 cubeMax = glm::vec3(1.0f, 1.0f, 1.0f); - model = glm::mat4(1.0f); - - //scale the cube to with given vector - model = glm::scale(model, scale); // The color of the cube. Try setting it to something else! color = newColor; @@ -139,34 +134,20 @@ Cube::~Cube() { glDeleteVertexArrays(1, &VAO); } -void Cube::draw(glm::mat4 viewProjMat, GLuint shader, bool fill) { - // actiavte the shader program - glUseProgram(shader); - - // Currently 'hardcoding' camera logic in - float FOV = 45.0f; - float Aspect = 1.33f; - float NearClip = 0.1f; - float FarClip = 100.0f; - - float Distance = 10.0f; - float Azimuth = 0.0f; - float Incline = 20.0f; - - glm::mat4 world(1); - world[3][2] = Distance; - world = glm::eulerAngleY(glm::radians(-Azimuth)) * glm::eulerAngleX(glm::radians(-Incline)) * world; +void Cube::draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) { - // Compute view matrix (inverse of world matrix) - glm::mat4 view = glm::inverse(world); - - // Compute perspective projection matrix - glm::mat4 project = glm::perspective(glm::radians(FOV), Aspect, NearClip, FarClip); + // actiavte the shader program + shader->use(); // get the locations and send the uniforms to the shader - glUniformMatrix4fv(glGetUniformLocation(shader, "viewProj"), 1, false, reinterpret_cast(&viewProjMat)); - glUniformMatrix4fv(glGetUniformLocation(shader, "model"), 1, GL_FALSE, reinterpret_cast(&model)); - glUniform3fv(glGetUniformLocation(shader, "DiffuseColor"), 1, &color[0]); + shader->setMat4("viewProj", viewProj); + auto model = this->getModelMat(); + shader->setMat4("model", model); + shader->setVec3("DiffuseColor", color); // Bind the VAO glBindVertexArray(VAO); @@ -185,11 +166,3 @@ void Cube::draw(glm::mat4 viewProjMat, GLuint shader, bool fill) { glBindVertexArray(0); glUseProgram(0); } - -void Cube::update(glm::vec3 new_pos) { - model[3] = glm::vec4(new_pos, 1.0f); -} - -void Cube::update_delta(glm::vec3 delta) { - model = glm::translate(model, delta); -} \ No newline at end of file diff --git a/src/client/model.cpp b/src/client/model.cpp index c9e43181..d8da0915 100644 --- a/src/client/model.cpp +++ b/src/client/model.cpp @@ -72,23 +72,29 @@ Mesh::Mesh( std::cout << "\t shininess" << this->material.shininess << std::endl; } -void Mesh::Draw(std::shared_ptr shader, glm::mat4 model, glm::mat4 viewProj, glm::vec3 camPos, glm::vec3 lightPos) { +void Mesh::draw( + std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) { // actiavte the shader program shader->use(); - auto lightColor = glm::vec3(1.0f, 1.0f, 1.0f); - shader->setVec3("lightColor", lightColor); - shader->setVec3("lightPos", lightPos); - - shader->setVec3("viewPos", camPos); - + // vertex shader uniforms shader->setMat4("viewProj", viewProj); + auto model = this->getModelMat(); shader->setMat4("model", model); + // fragment shader uniforms shader->setVec3("material.diffuse", this->material.diffuse); shader->setVec3("material.ambient", this->material.ambient); shader->setVec3("material.specular", this->material.specular); shader->setFloat("material.shininess", this->material.shininess); + shader->setVec3("viewPos", camPos); + auto lightColor = glm::vec3(1.0f, 1.0f, 1.0f); + shader->setVec3("lightColor", lightColor); + shader->setVec3("lightPos", lightPos); if (textures.size() == 0) { } else { @@ -113,14 +119,18 @@ void Mesh::Draw(std::shared_ptr shader, glm::mat4 model, glm::mat4 viewP // draw mesh glBindVertexArray(VAO); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + if(fill){ + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } else { + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); glBindVertexArray(0); glUseProgram(0); } Model::Model(const std::string& filepath) : - model(1.0f), directory(filepath.substr(0, filepath.find_last_of('/'))) { + directory(filepath.substr(0, filepath.find_last_of('/'))) { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(filepath, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_SplitLargeMeshes | aiProcess_OptimizeMeshes); @@ -131,22 +141,39 @@ Model::Model(const std::string& filepath) : processNode(scene->mRootNode, scene); } -void Model::Draw(glm::mat4 viewProj, glm::vec3 camPos, std::shared_ptr shader, glm::vec3 lightPos) { - for(Mesh& mesh : this->meshes) - mesh.Draw(shader, this->model, viewProj, camPos, lightPos); +void Model::draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) { + + for(Mesh& mesh : this->meshes) { + mesh.draw(shader, viewProj, camPos, lightPos, fill); + } +} + +void Model::translateAbsolute(const glm::vec3& new_pos) { + for(Mesh& mesh : this->meshes) { + mesh.translateAbsolute(new_pos); + } } -void Model::TranslateTo(const glm::vec3 &new_pos) { - model[3] = glm::vec4(new_pos, 1.0f); +void Model::translateRelative(const glm::vec3& delta) { + for(Mesh& mesh : this->meshes) { + mesh.translateAbsolute(delta); + } } -void Model::Scale(const float& new_factor) { - glm::vec3 scaleVector(new_factor, new_factor, new_factor); - this->model = glm::scale(this->model, scaleVector); +void Model::scale(const float& new_factor) { + for(Mesh& mesh : this->meshes) { + mesh.scale(new_factor); + } } -void Model::setModelView(const glm::mat4& modelView) { - this->model = modelView; +void Model::scale(const glm::vec3& scale) { + for(Mesh& mesh : this->meshes) { + mesh.scale(scale); + } } void Model::processNode(aiNode *node, const aiScene *scene) { diff --git a/src/client/renderable.cpp b/src/client/renderable.cpp new file mode 100644 index 00000000..5094337c --- /dev/null +++ b/src/client/renderable.cpp @@ -0,0 +1,28 @@ +#include "client/renderable.hpp" + +#include +#define GLM_ENABLE_EXPERIMENTAL +#include + +Renderable::Renderable() : model(1.0f) { } + +void Renderable::translateAbsolute(const glm::vec3 &new_pos) { + this->model[3] = glm::vec4(new_pos, 1.0f); +} + +void Renderable::translateRelative(const glm::vec3& delta) { + this->model = glm::translate(this->model, delta); +} + +void Renderable::scale(const float& new_factor) { + glm::vec3 scaleVector(new_factor, new_factor, new_factor); + this->model = glm::scale(this->model, scaleVector); +} + +void Renderable::scale(const glm::vec3& scale) { + this->model = glm::scale(this->model, scale); +} + +glm::mat4 Renderable::getModelMat() { + return this->model; +} diff --git a/src/client/shader.cpp b/src/client/shader.cpp index 48c1f2b3..0d984189 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -33,7 +33,7 @@ Shader::Shader(const std::string& vertexPath, const std::string& fragmentPath) { vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure& e) { - throw std::invalid_argument("Error: could not read shader file"); + throw std::invalid_argument("Error: could not read shader file " + vertexPath + " and " + fragmentPath); } const char* vShaderCode = vertexCode.c_str(); @@ -51,7 +51,7 @@ Shader::Shader(const std::string& vertexPath, const std::string& fragmentPath) { glGetShaderiv(vertex, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(vertex, 512, NULL, infoLog); - std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; + std::cout << "ERROR: Vertex shader compilation failed\n" << infoLog << std::endl; }; // fragment shader @@ -62,7 +62,7 @@ Shader::Shader(const std::string& vertexPath, const std::string& fragmentPath) { glGetShaderiv(fragment, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(vertex, 512, NULL, infoLog); - std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; + std::cout << "ERROR: Fragment shader compilation failed\n" << infoLog << std::endl; }; if (vertex == 0 && fragment == 0) { From 914415c38628d1ad1b439376aefc6d87f6c83db1 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 4 May 2024 23:20:46 -0700 Subject: [PATCH 61/92] populated objectTypeToString --- src/shared/game/sharedobject.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared/game/sharedobject.cpp b/src/shared/game/sharedobject.cpp index fd112aff..d1521876 100644 --- a/src/shared/game/sharedobject.cpp +++ b/src/shared/game/sharedobject.cpp @@ -4,6 +4,14 @@ std::string objectTypeString(ObjectType type) { switch (type) { case ObjectType::Object: return "Object"; + case ObjectType::Item: + return "Item"; + case ObjectType::SolidSurface: + return "SolidSurface"; + case ObjectType::Player: + return "Player"; + case ObjectType::Enemy: + return "Enemy"; default: return "Unknown"; } From 4bd91e2717bdde03c431799bd7562ee9dac325e7 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 4 May 2024 23:27:44 -0700 Subject: [PATCH 62/92] clean up client draw function --- src/client/client.cpp | 48 +++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index fda63733..4d282174 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -12,6 +12,7 @@ #include "client/shader.hpp" #include "client/model.hpp" #include "glm/fwd.hpp" +#include "server/game/solidsurface.hpp" #include "shared/game/event.hpp" #include "shared/game/sharedobject.hpp" #include "shared/network/constants.hpp" @@ -243,15 +244,6 @@ void Client::draw() { if (sharedObject == nullptr) { continue; } - if (sharedObject->type == ObjectType::Enemy) { - this->bear_model->translateAbsolute(sharedObject->physics.position); - this->bear_model->draw( - this->model_shader, - this->cam->getViewProj(), - this->cam->getPos(), - this->cam->getPos(), - true); - } // Get camera position from server, update position and don't render player object (or special handling) if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { @@ -261,20 +253,32 @@ void Client::draw() { continue; } - // If solidsurface, scale cube to given dimensions - if(sharedObject->solidSurface.has_value()){ - std::cout << "solid surface has type " << objectTypeString(sharedObject->type) << std::endl; - Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f)); - cube->scale( sharedObject->solidSurface->dimensions); - cube->translateAbsolute(sharedObject->physics.position); - cube->draw(this->cube_shader, this->cam->getViewProj(), this->cam->getPos(), glm::vec3(), true); - continue; + switch (sharedObject->type) { + case ObjectType::Enemy: { + // warren bear is an enemy because why not + this->bear_model->translateAbsolute(sharedObject->physics.position); + this->bear_model->draw( + this->model_shader, + this->cam->getViewProj(), + this->cam->getPos(), + this->cam->getPos(), + true); + break; + } + case ObjectType::SolidSurface: { + Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f)); + cube->scale( sharedObject->solidSurface->dimensions); + cube->translateAbsolute(sharedObject->physics.position); + cube->draw(this->cube_shader, + this->cam->getViewProj(), + this->cam->getPos(), + glm::vec3(), + true); + break; + } + default: + break; } - - // original center cube because why not - Cube* cube = new Cube(glm::vec3(0.0f,1.0f,1.0f)); - cube->translateAbsolute(sharedObject->physics.position); - cube->draw(this->cube_shader, this->cam->getViewProj(), this->cam->getPos(), glm::vec3(), false); } } From 29d68260611d22f4f39baa895b28d800baf5fa00 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 4 May 2024 23:31:39 -0700 Subject: [PATCH 63/92] renamed solid color shaders to cube shader also added some comments to clarify what each shader can and can't do --- src/client/client.cpp | 4 ++-- src/client/shaders/{shader.frag => cube.frag} | 9 ++------- src/client/shaders/{shader.vert => cube.vert} | 3 ++- src/client/shaders/model.frag | 9 ++++----- src/client/shaders/model.vert | 7 ++++--- 5 files changed, 14 insertions(+), 18 deletions(-) rename src/client/shaders/{shader.frag => cube.frag} (63%) rename src/client/shaders/{shader.vert => cube.vert} (90%) diff --git a/src/client/client.cpp b/src/client/client.cpp index 4d282174..0ec27f92 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -114,8 +114,8 @@ bool Client::init() { boost::filesystem::path shaders_dir = this->root_path / "src/client/shaders"; boost::filesystem::path graphics_assets_dir = this->root_path / "assets/graphics"; - boost::filesystem::path cube_vert_path = shaders_dir / "shader.vert"; - boost::filesystem::path cube_frag_path = shaders_dir / "shader.frag"; + boost::filesystem::path cube_vert_path = shaders_dir / "cube.vert"; + boost::filesystem::path cube_frag_path = shaders_dir / "cube.frag"; this->cube_shader = std::make_shared(cube_vert_path.string(), cube_frag_path.string()); boost::filesystem::path model_vert_path = shaders_dir / "model.vert"; diff --git a/src/client/shaders/shader.frag b/src/client/shaders/cube.frag similarity index 63% rename from src/client/shaders/shader.frag rename to src/client/shaders/cube.frag index 3bc9cbee..9c30ceae 100644 --- a/src/client/shaders/shader.frag +++ b/src/client/shaders/cube.frag @@ -1,23 +1,18 @@ #version 330 core -// Inputs to the fragment shader are the outputs of the same name from the vertex shader. -// Note that you do not have access to the vertex shader's default output, gl_Position. +// Fragment shader of solid color, untextured cubes in vec3 fragNormal; in vec3 fragPos; -// uniforms used for lighting uniform vec3 AmbientColor = vec3(0.2); uniform vec3 LightDirection = normalize(vec3(2, 4, 3)); uniform vec3 LightColor = vec3(1.0, 1.0, 1.0); uniform vec3 DiffuseColor = vec3(1.0, 1.0, 1.0); -// You can output many things. The first vec4 type output determines the color of the fragment out vec4 fragColor; -void main() -{ - +void main() { // Compute irradiance (sum of ambient & direct lighting) vec3 irradiance = AmbientColor + LightColor * max(0, dot(LightDirection, fragNormal)); diff --git a/src/client/shaders/shader.vert b/src/client/shaders/cube.vert similarity index 90% rename from src/client/shaders/shader.vert rename to src/client/shaders/cube.vert index a3f529c8..2ae4fdda 100644 --- a/src/client/shaders/shader.vert +++ b/src/client/shaders/cube.vert @@ -1,5 +1,6 @@ #version 330 core -// NOTE: Do NOT use any version older than 330! Bad things will happen! +# +// Fragment shader of solid color, untextured cubes layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal; diff --git a/src/client/shaders/model.frag b/src/client/shaders/model.frag index b737a5bc..18572321 100644 --- a/src/client/shaders/model.frag +++ b/src/client/shaders/model.frag @@ -1,7 +1,8 @@ #version 330 core -// Inputs to the fragment shader are the outputs of the same name from the vertex shader. -// Note that you do not have access to the vertex shader's default output, gl_Position. +// Fragment shader for loaded models. +// This shader currently expects textured models. Untextured +// models will show up as black. in vec3 fragNormal; in vec3 fragPos; @@ -21,11 +22,9 @@ uniform vec3 viewPos; uniform vec3 lightPos; uniform vec3 lightColor; -// You can output many things. The first vec4 type output determines the color of the fragment out vec4 fragColor; -void main() -{ +void main() { // ambient vec3 ambient = lightColor * material.ambient; diff --git a/src/client/shaders/model.vert b/src/client/shaders/model.vert index cca0b196..7d681852 100644 --- a/src/client/shaders/model.vert +++ b/src/client/shaders/model.vert @@ -1,5 +1,7 @@ #version 330 core -// NOTE: Do NOT use any version older than 330! Bad things will happen! +# +// Vertex shader for loaded models. +// Also forwards texture coordinates to fragment shader. layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal; @@ -15,8 +17,7 @@ out vec3 fragNormal; out vec3 fragPos; out vec2 TexCoords; -void main() -{ +void main() { // OpenGL maintains the D matrix so you only need to multiply by P, V (aka C inverse), and M gl_Position = viewProj * model * vec4(position, 1.0); From 07dfe93c8ef477e83bc2f462cf226437fb9b1d97 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Sat, 4 May 2024 23:51:05 -0700 Subject: [PATCH 64/92] lint --- include/client/cube.hpp | 2 +- include/client/model.hpp | 8 ++++---- include/client/renderable.hpp | 8 ++++---- include/debugger/debugger.hpp | 2 +- include/server/game/creature.hpp | 2 +- include/server/game/servergamestate.hpp | 2 +- include/shared/game/sharedgamestate.hpp | 2 +- src/client/lightsource.cpp | 5 +---- src/client/model.cpp | 2 +- src/client/shader.cpp | 2 +- src/server/game/enemy.cpp | 4 +++- src/server/game/item.cpp | 2 +- src/server/game/player.cpp | 2 +- src/server/game/servergamestate.cpp | 4 ++-- src/server/game/solidsurface.cpp | 6 +++--- src/server/server.cpp | 12 ++++++------ 16 files changed, 32 insertions(+), 33 deletions(-) diff --git a/include/client/cube.hpp b/include/client/cube.hpp index 6a191736..58cf1574 100644 --- a/include/client/cube.hpp +++ b/include/client/cube.hpp @@ -14,7 +14,7 @@ class Cube : public Renderable { public: - Cube(glm::vec3 newColor); + explicit Cube(glm::vec3 newColor); ~Cube(); void draw(std::shared_ptr shader, diff --git a/include/client/model.hpp b/include/client/model.hpp index 74be2229..27dcb9a5 100644 --- a/include/client/model.hpp +++ b/include/client/model.hpp @@ -130,7 +130,7 @@ class Model : public Renderable { * * @param vector of x, y, z of the model's new position */ - void translateAbsolute(const glm::vec3& new_pos); + void translateAbsolute(const glm::vec3& new_pos) override; /** * Updates the position of the Model relative to it's @@ -139,7 +139,7 @@ class Model : public Renderable { * @param vector of x, y, z of the change in the Model's * position */ - void translateRelative(const glm::vec3& delta); + void translateRelative(const glm::vec3& delta) override; /** * Scale the Model across all axes (x,y,z) @@ -149,7 +149,7 @@ class Model : public Renderable { * Ex: setting it to 0.5 will cut the model's rendered size * in half. */ - void scale(const float& new_factor); + void scale(const float& new_factor) override; /** * Scale the model across all axes (x,y,z) @@ -158,7 +158,7 @@ class Model : public Renderable { * @param the scale vector describes how much to independently scale * the model in each axis (x, y, z) */ - void scale(const glm::vec3& scale); + void scale(const glm::vec3& scale) override; private: std::vector meshes; diff --git a/include/client/renderable.hpp b/include/client/renderable.hpp index b0b5f9a6..a06efa05 100644 --- a/include/client/renderable.hpp +++ b/include/client/renderable.hpp @@ -28,7 +28,7 @@ class Renderable { * * @param vector of x, y, z of the 's new position */ - void translateAbsolute(const glm::vec3& new_pos); + virtual void translateAbsolute(const glm::vec3& new_pos); /** * Updates the position of the item relative to it's @@ -37,7 +37,7 @@ class Renderable { * @param vector of x, y, z of the change in the item's * position */ - void translateRelative(const glm::vec3& delta); + virtual void translateRelative(const glm::vec3& delta); /** * Scale the Model across all axes (x,y,z) @@ -47,7 +47,7 @@ class Renderable { * Ex: setting it to 0.5 will cut the model's rendered size * in half. */ - void scale(const float& new_factor); + virtual void scale(const float& new_factor); /** * Scale the item across all axes (x,y,z) @@ -56,7 +56,7 @@ class Renderable { * @param the scale vector describes how much to independently scale * the item in each axis (x, y, z) */ - void scale(const glm::vec3& scale); + virtual void scale(const glm::vec3& scale); /** * Gets the model matrix given all the transformations diff --git a/include/debugger/debugger.hpp b/include/debugger/debugger.hpp index 6b7e3188..89306510 100644 --- a/include/debugger/debugger.hpp +++ b/include/debugger/debugger.hpp @@ -258,7 +258,7 @@ class CreateCommand : public Command { // Create a new base object in the game state unsigned int globalID = state.objects.createObject(ObjectType::Object); - Object* obj = state.objects.getObject(globalID); + const Object* obj = state.objects.getObject(globalID); // cppcheck-suppress unreadVariable std::cout << "Created new object (global id " << globalID << ")" << std::endl; } diff --git a/include/server/game/creature.hpp b/include/server/game/creature.hpp index dcdd36cf..aee3bbab 100644 --- a/include/server/game/creature.hpp +++ b/include/server/game/creature.hpp @@ -6,7 +6,7 @@ class Creature : public Object { public: - Creature(ObjectType type); + explicit Creature(ObjectType type); ~Creature(); virtual SharedObject toShared() override; diff --git a/include/server/game/servergamestate.hpp b/include/server/game/servergamestate.hpp index b49a030c..9c3a7e3d 100644 --- a/include/server/game/servergamestate.hpp +++ b/include/server/game/servergamestate.hpp @@ -47,7 +47,7 @@ class ServerGameState { */ explicit ServerGameState(GamePhase start_phase); - ServerGameState(GamePhase start_phase, GameConfig config); + ServerGameState(GamePhase start_phase, const GameConfig& config); ~ServerGameState(); diff --git a/include/shared/game/sharedgamestate.hpp b/include/shared/game/sharedgamestate.hpp index f4f9a868..c1a47cc8 100644 --- a/include/shared/game/sharedgamestate.hpp +++ b/include/shared/game/sharedgamestate.hpp @@ -65,7 +65,7 @@ struct SharedGameState { this->lobby.max_players = MAX_PLAYERS; } - SharedGameState(GamePhase start_phase, GameConfig config): + SharedGameState(GamePhase start_phase, const GameConfig& config): objects(std::vector>()) { this->objects.reserve(MAX_NUM_OBJECTS); diff --git a/src/client/lightsource.cpp b/src/client/lightsource.cpp index c53b42e8..7129aaf5 100644 --- a/src/client/lightsource.cpp +++ b/src/client/lightsource.cpp @@ -6,10 +6,7 @@ #include "client/shader.hpp" -LightSource::LightSource() { - model = glm::mat4(1.0f); - // model = glm::scale(model, glm::vec3(0.2f)); - +LightSource::LightSource() : model(1.0f) { glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); // we only need to bind to the VBO, the container's VBO's data already contains the data. diff --git a/src/client/model.cpp b/src/client/model.cpp index d8da0915..a20c44d4 100644 --- a/src/client/model.cpp +++ b/src/client/model.cpp @@ -228,7 +228,7 @@ Mesh Model::processMesh(aiMesh *mesh, const aiScene *scene) { aiColor3D diffuse_color; aiColor3D ambient_color; aiColor3D specular_color; - float shininess; + float shininess = 0.0f; if(mesh->mMaterialIndex >= 0) { std::cout << "processing material of id: " << mesh->mMaterialIndex << std::endl; diff --git a/src/client/shader.cpp b/src/client/shader.cpp index 0d984189..437c115c 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -118,7 +118,7 @@ void Shader::setVec3(const std::string &name, glm::vec3& value) { glm::vec3 Shader::getVec3(const std::string &name) { glm::vec3 vec; - glGetUniformfv(ID, glGetUniformLocation(ID, name.c_str()), (float*)&vec); + glGetUniformfv(ID, glGetUniformLocation(ID, name.c_str()), reinterpret_cast(&vec)); return vec; } diff --git a/src/server/game/enemy.cpp b/src/server/game/enemy.cpp index d1c331a6..61119d71 100644 --- a/src/server/game/enemy.cpp +++ b/src/server/game/enemy.cpp @@ -6,6 +6,8 @@ SharedObject Enemy::toShared() { return so; } -Enemy::Enemy() : Creature(ObjectType::Enemy) {} +Enemy::Enemy() : Creature(ObjectType::Enemy) { + this->stats = EnemyStats { 100 }; +} Enemy::~Enemy() {} \ No newline at end of file diff --git a/src/server/game/item.cpp b/src/server/game/item.cpp index dbb256bf..e655869d 100644 --- a/src/server/game/item.cpp +++ b/src/server/game/item.cpp @@ -2,7 +2,7 @@ #include "shared/game/sharedobject.hpp" /* Constructors and Destructors */ -Item::Item() : Object(ObjectType::Item) { +Item::Item() : Object(ObjectType::Item) { // cppcheck-suppress uninitMemberVar } diff --git a/src/server/game/player.cpp b/src/server/game/player.cpp index 95473d44..7082da9a 100644 --- a/src/server/game/player.cpp +++ b/src/server/game/player.cpp @@ -8,7 +8,7 @@ SharedObject Player::toShared() { } Player::Player() : Creature(ObjectType::Player) { - + this->stats = PlayerStats{ 100 }; } Player::~Player() { diff --git a/src/server/game/servergamestate.cpp b/src/server/game/servergamestate.cpp index e34e06a8..5d316637 100644 --- a/src/server/game/servergamestate.cpp +++ b/src/server/game/servergamestate.cpp @@ -3,7 +3,7 @@ /* Constructors and Destructors */ -ServerGameState::ServerGameState(GamePhase start_phase, GameConfig config) { +ServerGameState::ServerGameState(GamePhase start_phase, const GameConfig& config) { this->phase = start_phase; this->timestep = FIRST_TIMESTEP; this->timestep_length = config.game.timestep_length_ms; @@ -159,7 +159,7 @@ void ServerGameState::useItem() { SmartVector items = this->objects.getItems(); for (int i = 0; i < items.size(); i++) { - Item* item = items.get(i); + const Item* item = items.get(i); if (item == nullptr) continue; diff --git a/src/server/game/solidsurface.cpp b/src/server/game/solidsurface.cpp index 04d2c6db..8f3171e4 100644 --- a/src/server/game/solidsurface.cpp +++ b/src/server/game/solidsurface.cpp @@ -9,9 +9,9 @@ SolidSurface::~SolidSurface() {} /* SharedGameState generation */ SharedObject SolidSurface::toShared() { - SharedObject shared = Object::toShared(); + SharedObject sharedSolidSurface = Object::toShared(); - shared.solidSurface = this->shared; + sharedSolidSurface.solidSurface = this->shared; - return shared; + return sharedSolidSurface; } \ No newline at end of file diff --git a/src/server/server.cpp b/src/server/server.cpp index 34fd5714..5e3a9df2 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -38,7 +38,7 @@ Server::Server(boost::asio::io_context& io_context, GameConfig config) state.objects.createObject(ObjectType::Object); EntityID bearID = state.objects.createObject(ObjectType::Enemy); - Enemy* bear = (Enemy*)state.objects.getObject(bearID); + Enemy* bear = reinterpret_cast(state.objects.getObject(bearID)); bear->physics.shared.position = glm::vec3(0.0f, -1.3f, 0.0f); // Create a room @@ -57,11 +57,11 @@ Server::Server(boost::asio::io_context& io_context, GameConfig config) // # # // ##4## - SolidSurface* wall1 = (SolidSurface*)state.objects.getObject(wall1ID); - SolidSurface* wall2 = (SolidSurface*)state.objects.getObject(wall2ID); - SolidSurface* wall3 = (SolidSurface*)state.objects.getObject(wall3ID); - SolidSurface* wall4 = (SolidSurface*)state.objects.getObject(wall4ID); - SolidSurface* floor = (SolidSurface*)state.objects.getObject(floorID); + SolidSurface* wall1 = reinterpret_cast(state.objects.getObject(wall1ID)); + SolidSurface* wall2 = reinterpret_cast(state.objects.getObject(wall2ID)); + SolidSurface* wall3 = reinterpret_cast(state.objects.getObject(wall3ID)); + SolidSurface* wall4 = reinterpret_cast(state.objects.getObject(wall4ID)); + SolidSurface* floor = reinterpret_cast(state.objects.getObject(floorID)); // Wall1 has dimensions (20, 4, 1); and position (0, 0, -19.5) wall1->shared.dimensions = glm::vec3(20, 4, 1); From 481bcf4419e0f4c8377000dbee76cafe3c83ad78 Mon Sep 17 00:00:00 2001 From: David Min Date: Sun, 5 May 2024 18:47:32 -0700 Subject: [PATCH 65/92] Modified existing texture code using LearnOpenGL tutorial --- assets/.DS_Store | Bin 0 -> 6148 bytes assets/imgs/awesomeface.png | Bin 0 -> 44004 bytes include/client/client.hpp | 4 +- include/client/gui/font/font.hpp | 4 +- include/client/gui/img/img.hpp | 5 +- include/client/gui/widget/staticimg.hpp | 4 +- src/client/client.cpp | 2 +- src/client/gui/font/loader.cpp | 14 +- src/client/gui/gui.cpp | 97 +++---- src/client/gui/imgs/img.cpp | 2 + src/client/gui/imgs/loader.cpp | 16 +- src/client/gui/widget/staticimg.cpp | 105 ++++++-- src/client/main.cpp | 321 +++++++++++++++++++++++- src/client/shaders/img.frag | 4 +- src/client/shaders/test.frag | 13 + src/client/shaders/test.vert | 14 ++ 16 files changed, 513 insertions(+), 92 deletions(-) create mode 100644 assets/.DS_Store create mode 100644 assets/imgs/awesomeface.png create mode 100644 src/client/shaders/test.frag create mode 100644 src/client/shaders/test.vert diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..df5a4be4ea78fd1654b79e55592dfa55ca906922 GIT binary patch literal 6148 zcmeHK%}T>S5Z<-XrW7Fug&r5Y7L1Ju;w8lT0!H+pQWH{YFwK@GwTDv3SzpK}@p+ut z-H63{6|pn0`_1oe_JiyXV~hv0bgvC#;;2z+&ZokvIv5Cg;jF|fG| zm@~m{Z!QJ2a$(9A`0mEmOvB+eS?)oh=6ci3aCrD zd17!~4t`5TgtsV4@AfmH??+H~;zzkpw6?IVA+ zge+o!82D!laH}8o16Y(fTfZ$2&sqWP0U8SCRj7b~zH$iw1NV`R3hKB(9rApGl|~!| S{i+<0E&_@W>WG0~VBiDY$VyWH literal 0 HcmV?d00001 diff --git a/assets/imgs/awesomeface.png b/assets/imgs/awesomeface.png new file mode 100644 index 0000000000000000000000000000000000000000..23c778d7a295984701047685137f9e2bbf9d2ea8 GIT binary patch literal 44004 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 QtN;K207*qoM6N<$f&_Q( a fourth of the way from the left diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index 84e003f6..fde52847 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -15,7 +15,7 @@ namespace gui::font { */ enum class Font { MENU, - TEXT, + TEXT }; /** @@ -25,7 +25,7 @@ enum FontSizePx { SMALL = 64, MEDIUM = 96, LARGE = 128, - HUGE = 256 + XLARGE = 256, }; /** diff --git a/include/client/gui/img/img.hpp b/include/client/gui/img/img.hpp index c7f27694..d5b47f25 100644 --- a/include/client/gui/img/img.hpp +++ b/include/client/gui/img/img.hpp @@ -15,10 +15,11 @@ namespace gui::img { * the image loader will actually load it. */ enum class ImgID { - Yoshi + Yoshi, + Pikachu }; #define GET_ALL_IMG_IDS() \ - {ImgID::Yoshi} + {ImgID::Yoshi, ImgID::Pikachu} /** * Representation of a loaded image diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp index 4aca72d1..5c6cd47f 100644 --- a/include/client/gui/widget/staticimg.hpp +++ b/include/client/gui/widget/staticimg.hpp @@ -37,12 +37,14 @@ class StaticImg : public Widget { StaticImg(glm::vec2 origin, gui::img::Img img); StaticImg(gui::img::Img img); + ~StaticImg(); void render() override; private: gui::img::Img img; - GLuint quadVAO; + GLuint quadVAO, VBO, EBO; + GLuint texture_id; }; } diff --git a/src/client/client.cpp b/src/client/client.cpp index d5dc4606..f7cada38 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -133,7 +133,7 @@ bool Client::init() { return false; } - auto imgShaderProgram = LoadShaders((shader_path / "img.vert").string(), (shader_path / "img.frag").string()); + auto imgShaderProgram = LoadShaders((shader_path / "test.vert").string(), (shader_path / "test.frag").string()); if (!imgShaderProgram) { std::cerr << "Failed to initialize img shader program" << std::endl; return false; diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 233df563..84bbc059 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -25,12 +25,12 @@ bool Loader::init() { // so this is supposed to prevent seg faults related to that glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - if (!this->_loadFont(Font::MENU)) { - return false; - } - if (!this->_loadFont(Font::TEXT)) { - return false; - } + // if (!this->_loadFont(Font::MENU)) { + // return false; + // } + // if (!this->_loadFont(Font::TEXT)) { + // return false; + // } FT_Done_FreeType(this->ft); // done loading fonts, so can release these resources @@ -56,7 +56,7 @@ bool Loader::_loadFont(Font font) { return false; } - for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE, FontSizePx::HUGE}) { + for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE, FontSizePx::XLARGE}) { FT_Set_Pixel_Sizes(face, 0, font_size); std::unordered_map characters; for (unsigned char c = 0; c < 128; c++) { diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 01422824..c397e244 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -23,9 +23,9 @@ bool GUI::init(GLuint text_shader, GLuint image_shader) this->fonts = std::make_shared(); this->capture_keystrokes = false; - if (!this->fonts->init()) { - return false; - } + // if (!this->fonts->init()) { + // return false; + // } if (!this->images.init()) { return false; @@ -48,6 +48,7 @@ void GUI::beginFrame() { void GUI::renderFrame() { // for text rendering + glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_CULL_FACE); @@ -150,49 +151,55 @@ void GUI::layoutFrame(GUIState state) { void GUI::_layoutTitleScreen() { - this->addWidget(widget::CenterText::make( - "Arcana", - font::Font::MENU, - font::FontSizePx::HUGE, - font::FontColor::RED, - fonts, - FRAC_WINDOW_HEIGHT(2, 3) - )); - - auto start_text = widget::DynText::make( - "Start Game", - fonts, - widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::MEDIUM, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f, - } - ); - start_text->addOnHover([this](widget::Handle handle) { - auto widget = this->borrowWidget(handle); - widget->changeColor(font::FontColor::RED); - }); - start_text->addOnClick([this](widget::Handle handle) { - client->gui_state = GUIState::LOBBY_BROWSER; - }); - auto start_flex = widget::Flexbox::make( - glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), - glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 0.0f, - } - ); - start_flex->push(std::move(start_text)); - this->addWidget(std::move(start_flex)); - - auto yoshi = widget::StaticImg::make( - glm::vec2(200.0f, 200.0f), - this->images.getImg(img::ImgID::Yoshi) + // this->addWidget(widget::CenterText::make( + // "Arcana", + // font::Font::MENU, + // font::FontSizePx::XLARGE, + // font::FontColor::RED, + // fonts, + // FRAC_WINDOW_HEIGHT(2, 3) + // )); + + // auto start_text = widget::DynText::make( + // "Start Game", + // fonts, + // widget::DynText::Options { + // .font = font::Font::MENU, + // .font_size = font::FontSizePx::MEDIUM, + // .color = font::getRGB(font::FontColor::BLACK), + // .scale = 1.0f, + // } + // ); + // start_text->addOnHover([this](widget::Handle handle) { + // auto widget = this->borrowWidget(handle); + // widget->changeColor(font::FontColor::RED); + // }); + // start_text->addOnClick([this](widget::Handle handle) { + // client->gui_state = GUIState::LOBBY_BROWSER; + // }); + // auto start_flex = widget::Flexbox::make( + // glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), + // glm::vec2(WINDOW_WIDTH, 0.0f), + // widget::Flexbox::Options { + // .direction = widget::JustifyContent::VERTICAL, + // .alignment = widget::AlignItems::CENTER, + // .padding = 0.0f, + // } + // ); + // start_flex->push(std::move(start_text)); + // this->addWidget(std::move(start_flex)); + + // auto yoshi = widget::StaticImg::make( + // glm::vec2(200.0f, 200.0f), + // this->images.getImg(img::ImgID::Yoshi) + // ); + // this->addWidget(std::move(yoshi)); + + auto pikachu = widget::StaticImg::make( + glm::vec2(100.0f, 100.0f), + this->images.getImg(img::ImgID::Pikachu) ); - this->addWidget(std::move(yoshi)); + this->addWidget(std::move(pikachu)); } void GUI::_layoutLobbyBrowser() { diff --git a/src/client/gui/imgs/img.cpp b/src/client/gui/imgs/img.cpp index 57cc2675..4e12f286 100644 --- a/src/client/gui/imgs/img.cpp +++ b/src/client/gui/imgs/img.cpp @@ -9,6 +9,8 @@ std::string getImgFilepath(ImgID img) { switch (img) { default: case ImgID::Yoshi: return (img_root / "Yoshi.png").string(); + case ImgID::Pikachu: return (img_root / "awesomeface.png").string(); + } } diff --git a/src/client/gui/imgs/loader.cpp b/src/client/gui/imgs/loader.cpp index 67eceba6..46b7641c 100644 --- a/src/client/gui/imgs/loader.cpp +++ b/src/client/gui/imgs/loader.cpp @@ -26,7 +26,12 @@ bool Loader::_loadImg(ImgID img_id) { int width, height, channels; + stbi_set_flip_vertically_on_load(1); unsigned char* img_data = stbi_load(path.c_str(), &width, &height, &channels, 0); + std::cout << img_data << std::endl; + + if (stbi_failure_reason()) + std::cout << "failure: " << stbi_failure_reason() << std::endl; GLuint texture_id; if (img_data == 0 || width == 0 || height == 0) { @@ -34,15 +39,19 @@ bool Loader::_loadImg(ImgID img_id) { ", " << width << ", " << height << "\n" << std::endl; return false; } + glGenTextures(1, &texture_id); - glBindTexture(GL_TEXTURE_2D, texture_id); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data); + // set Texture wrap and filter modes glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data); + glGenerateMipmap(GL_TEXTURE_2D); + // unbind texture glBindTexture(GL_TEXTURE_2D, 0); @@ -51,6 +60,9 @@ bool Loader::_loadImg(ImgID img_id) { .width = width, .height = height }}); + + stbi_image_free(img_data); + return true; } diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index af89e444..62f0eea1 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -2,6 +2,8 @@ #include "client/client.hpp" + + namespace gui::widget { GLuint StaticImg::shader = 0; @@ -9,53 +11,102 @@ GLuint StaticImg::shader = 0; StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): Widget(Type::StaticImg, origin) { - // configure VAO/VBO - unsigned int VBO; - // there might be some mismatch here because this might be assuming that the top left - // corner is 0,0 when we are specifying origin by bottom left coordinate - float vertices[] = { - // pos // tex - 0.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 1.0f, 1.0f, - 1.0f, 0.0f, 1.0f, 0.0f + // // configure VAO/VBO + // unsigned int VBO; + // // there might be some mismatch here because this might be assuming that the top left + // // corner is 0,0 when we are specifying origin by bottom left coordinate + // float vertices[] = { + // // pos // tex + // 0.0f, 1.0f, 0.0f, 1.0f, + // 1.0f, 0.0f, 1.0f, 0.0f, + // 0.0f, 0.0f, 0.0f, 0.0f, + // 0.0f, 1.0f, 0.0f, 1.0f, + // 1.0f, 1.0f, 1.0f, 1.0f, + // 1.0f, 0.0f, 1.0f, 0.0f + // }; + // glGenVertexArrays(1, &quadVAO); + // glGenBuffers(1, &VBO); + + // glBindBuffer(GL_ARRAY_BUFFER, VBO); + // glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + // glBindVertexArray(quadVAO); + // glEnableVertexAttribArray(0); + // glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float),(void*)0); + // glBindBuffer(GL_ARRAY_BUFFER, 0); + // glBindVertexArray(0); + + float vertices[] = { + // positions // colors // texture coords + 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right + 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left + -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left }; + unsigned int indices[] = { + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + glGenVertexArrays(1, &quadVAO); glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(quadVAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - glBindVertexArray(quadVAO); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // position attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float),(void*)0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); + // color attribute + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + // texture coord attribute + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); + glEnableVertexAttribArray(2); + this->width = img.width; this->height = img.height; + this->texture_id = img.texture_id; } StaticImg::StaticImg(gui::img::Img img): StaticImg({0.0f, 0.0f}, img) {} +StaticImg::~StaticImg() { + glDeleteVertexArrays(1, &quadVAO); + glDeleteBuffers(1, &VBO); + glDeleteBuffers(1, &EBO); +} + void StaticImg::render() { glUseProgram(StaticImg::shader); + glUniform1i(glGetUniformLocation(StaticImg::shader, "texture1"), this->texture_id); glm::mat4 model = glm::mat4(1.0f); - model = glm::translate(model, glm::vec3(origin, 0.0f)); - model = glm::translate(model, glm::vec3(0.5*width, 0.5*height, 0.0)); - model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0, 0.0, 1.0)); - model = glm::translate(model, glm::vec3(-0.5*width, -0.5*height, 0.0)); - model = glm::scale(model, glm::vec3(glm::vec2(width, height), 1.0f)); - glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); - glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "model"), 1, false, reinterpret_cast(&model)); - glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 1.0f, 1.0f, 1.0f); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, img.texture_id); + // model = glm::translate(model, glm::vec3(origin, 0.0f)); + // model = glm::translate(model, glm::vec3(0.5*width, 0.5*height, 0.0)); + // model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0, 0.0, 1.0)); + // model = glm::translate(model, glm::vec3(-0.5*width, -0.5*height, 0.0)); + // model = glm::scale(model, glm::vec3(glm::vec2(width, height), 1.0f)); + // glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); + // glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "model"), 1, false, reinterpret_cast(&model)); + // glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 1.0f, 1.0f, 1.0f); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, this->texture_id); glBindVertexArray(quadVAO); - glDrawArrays(GL_TRIANGLES, 0, 6); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); glUseProgram(0); } diff --git a/src/client/main.cpp b/src/client/main.cpp index 59a10d6a..cb5f5888 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -11,6 +11,10 @@ #include "shared/utilities/root_path.hpp" #include "client/sound.hpp" +#include "shared/utilities/root_path.hpp" +#include "client/gui/img/img.hpp" +#include "stb_image.h" + using namespace std::chrono_literals; void error_callback(int error, const char* description) { @@ -83,7 +87,133 @@ int main(int argc, char* argv[]) sound.setLoop(true); - sound.play(); + // sound.play(); + + // float vertices[] = { + // // positions // colors // texture coords + // 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right + // 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right + // -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left + // -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left + // }; + // unsigned int indices[] = { + // 0, 1, 3, // first triangle + // 1, 2, 3 // second triangle + // }; + // unsigned int VBO, VAO, EBO; + // glGenVertexArrays(1, &VAO); + // glGenBuffers(1, &VBO); + // glGenBuffers(1, &EBO); + + // glBindVertexArray(VAO); + + // glBindBuffer(GL_ARRAY_BUFFER, VBO); + // glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + // glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // // position attribute + // glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); + // glEnableVertexAttribArray(0); + // // color attribute + // glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); + // glEnableVertexAttribArray(1); + // // texture coord attribute + // glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); + // glEnableVertexAttribArray(2); + + + // // load and create a texture + // // ------------------------- + // unsigned int texture1; + // // texture 1 + // // --------- + // glGenTextures(1, &texture1); + // glBindTexture(GL_TEXTURE_2D, texture1); + // // set the texture wrapping parameters + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + // // set texture filtering parameters + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // // load image, create texture and generate mipmaps + // int width, height, nrChannels; + // stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis. + // // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path. + // auto img_root = getRepoRoot() / "assets/imgs"; + + // std::cout << (img_root / "awesomeface.png").string() << std::endl; + // unsigned char *data = stbi_load((img_root / "awesomeface.png").string().c_str(), &width, &height, &nrChannels, 0); + // if (data) + // { + // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + // glGenerateMipmap(GL_TEXTURE_2D); + // } + // else + // { + // std::cout << "Failed to load texture" << std::endl; + // } + // std::cout << "data: " << data << std::endl; + // stbi_image_free(data); + + // auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; + // auto imgShaderProgram = LoadShaders((shader_path / "test.vert").string(), (shader_path / "test.frag").string()); + // if (!imgShaderProgram) { + // std::cerr << "Failed to initialize test shader program" << std::endl; + // return false; + // } + + // // tell opengl for each sampler to which texture unit it belongs to (only has to be done once) + // // ------------------------------------------------------------------------------------------- + // glUseProgram(imgShaderProgram); // don't forget to activate/use the shader before setting uniforms! + // // either set it manually like so: + // glUniform1i(glGetUniformLocation(imgShaderProgram, "texture1"), texture1); + // // or set it via the texture class + + // std::cout << texture1 << std::endl; + + + + // // render loop + // // ----------- + // while (!glfwWindowShouldClose(window)) + // { + // // input + // // ----- + // // processInput(window); + + // // render + // // ------ + // glClearColor(0.2f, 0.3f, 0.3f, 1.0f); + // glClear(GL_COLOR_BUFFER_BIT); + + // // bind textures on corresponding texture units + // glActiveTexture(GL_TEXTURE3); + // glBindTexture(GL_TEXTURE_2D, texture1); + + // // render container + // glUseProgram(imgShaderProgram); + // glBindVertexArray(VAO); + // glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + // glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + + // // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) + // // ------------------------------------------------------------------------------- + // glfwSwapBuffers(window); + // glfwPollEvents(); + // } + + // // optional: de-allocate all resources once they've outlived their purpose: + // // ------------------------------------------------------------------------ + // glDeleteVertexArrays(1, &VAO); + // glDeleteBuffers(1, &VBO); + // glDeleteBuffers(1, &EBO); + + // // glfw: terminate, clearing all previously allocated GLFW resources. + // // ------------------------------------------------------------------ + // glfwTerminate(); + // return 0; // Loop while GLFW window should stay open. while (!glfwWindowShouldClose(window)) { @@ -105,3 +235,192 @@ int main(int argc, char* argv[]) return 0; } + +// #include +// #include + + +// #include +// #include + +// #include "client/client.hpp" +// #include "shared/utilities/rng.hpp" +// #include "shared/utilities/config.hpp" +// #include "shared/utilities/root_path.hpp" +// #include "client/sound.hpp" + +// #include "shared/utilities/root_path.hpp" +// #include "client/gui/img/img.hpp" +// #include "stb_image.h" + +// void framebuffer_size_callback(GLFWwindow* window, int width, int height); +// void processInput(GLFWwindow *window); + +// // settings +// const unsigned int SCR_WIDTH = 800; +// const unsigned int SCR_HEIGHT = 600; + +// int main() +// { +// // glfw: initialize and configure +// // ------------------------------ +// glfwInit(); +// glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); +// glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); +// glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + +// #ifdef __APPLE__ +// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +// #endif + +// // glfw window creation +// // -------------------- +// GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); +// if (window == NULL) +// { +// std::cout << "Failed to create GLFW window" << std::endl; +// glfwTerminate(); +// return -1; +// } +// glfwMakeContextCurrent(window); +// glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); + +// // glad: load all OpenGL function pointers +// // --------------------------------------- +// // if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) +// // { +// // std::cout << "Failed to initialize GLAD" << std::endl; +// // return -1; +// // } +// GLenum err = glewInit() ; +// if (GLEW_OK != err) { +// std::cerr << "Error loading GLEW: " << glewGetString(err) << std::endl; +// return false; +// } + +// // build and compile our shader zprogram +// // ------------------------------------ +// // Shader ourShader("/Users/dmin/Documents/Homework/Spring_2024/LearnOpenGL/src/1.getting_started/4.1.textures/4.1.texture.vs", "/Users/dmin/Documents/Homework/Spring_2024/LearnOpenGL/src/1.getting_started/4.1.textures/4.1.texture.fs"); +// auto shaderprog = LoadShaders("/Users/dmin/Documents/Homework/Spring_2024/group3/src/client/shaders/test.vert", "/Users/dmin/Documents/Homework/Spring_2024/group3/src/client/shaders/test.frag"); + +// // set up vertex data (and buffer(s)) and configure vertex attributes +// // ------------------------------------------------------------------ +// float vertices[] = { +// // positions // colors // texture coords +// 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right +// 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right +// -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left +// -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left +// }; +// unsigned int indices[] = { +// 0, 1, 3, // first triangle +// 1, 2, 3 // second triangle +// }; +// unsigned int VBO, VAO, EBO; +// glGenVertexArrays(1, &VAO); +// glGenBuffers(1, &VBO); +// glGenBuffers(1, &EBO); + +// glBindVertexArray(VAO); + +// glBindBuffer(GL_ARRAY_BUFFER, VBO); +// glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + +// glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); +// glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + +// // position attribute +// glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); +// glEnableVertexAttribArray(0); +// // color attribute +// glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); +// glEnableVertexAttribArray(1); +// // texture coord attribute +// glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); +// glEnableVertexAttribArray(2); + + +// // load and create a texture +// // ------------------------- +// unsigned int texture; +// glGenTextures(1, &texture); +// glBindTexture(GL_TEXTURE_2D, texture); // all upcoming GL_TEXTURE_2D operations now have effect on this texture object +// // set the texture wrapping parameters +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); +// // set texture filtering parameters +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); +// // load image, create texture and generate mipmaps +// int width, height, nrChannels; +// // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path. +// unsigned char *data = stbi_load("/Users/dmin/Documents/Homework/Spring_2024/group3/assets/imgs/awesomeface.png", &width, &height, &nrChannels, 0); +// // std::cout << "data: " << data << std::endl; +// if (data) +// { +// glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); +// glGenerateMipmap(GL_TEXTURE_2D); +// } +// else +// { +// std::cout << "Failed to load texture" << std::endl; +// } +// stbi_image_free(data); + + +// // render loop +// // ----------- +// while (!glfwWindowShouldClose(window)) +// { +// // input +// // ----- +// processInput(window); + +// // render +// // ------ +// glClearColor(0.2f, 0.3f, 0.3f, 1.0f); +// glClear(GL_COLOR_BUFFER_BIT); + +// // bind Texture +// glBindTexture(GL_TEXTURE_2D, texture); + +// // render container +// // ourShader.use(); +// glUseProgram(shaderprog); +// glBindVertexArray(VAO); +// glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + +// // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) +// // ------------------------------------------------------------------------------- +// glfwSwapBuffers(window); +// glfwPollEvents(); +// } + +// // optional: de-allocate all resources once they've outlived their purpose: +// // ------------------------------------------------------------------------ +// glDeleteVertexArrays(1, &VAO); +// glDeleteBuffers(1, &VBO); +// glDeleteBuffers(1, &EBO); + +// // glfw: terminate, clearing all previously allocated GLFW resources. +// // ------------------------------------------------------------------ +// glfwTerminate(); +// return 0; +// } + +// // process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly +// // --------------------------------------------------------------------------------------------------------- +// void processInput(GLFWwindow *window) +// { +// if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) +// glfwSetWindowShouldClose(window, true); +// } + +// // glfw: whenever the window size changed (by OS or user resize) this callback function executes +// // --------------------------------------------------------------------------------------------- +// void framebuffer_size_callback(GLFWwindow* window, int width, int height) +// { +// // make sure the viewport matches the new window dimensions; note that width and +// // height will be significantly larger than specified on retina displays. +// glViewport(0, 0, width, height); +// } diff --git a/src/client/shaders/img.frag b/src/client/shaders/img.frag index 62e36c4a..0e7beb86 100644 --- a/src/client/shaders/img.frag +++ b/src/client/shaders/img.frag @@ -10,5 +10,5 @@ uniform vec3 spriteColor; void main() { - color = vec4(spriteColor, 1.0) * texture(image, TexCoords); -} \ No newline at end of file + color = texture(image, TexCoords); +} diff --git a/src/client/shaders/test.frag b/src/client/shaders/test.frag new file mode 100644 index 00000000..d7bb3647 --- /dev/null +++ b/src/client/shaders/test.frag @@ -0,0 +1,13 @@ +#version 330 core +out vec4 FragColor; + +in vec3 ourColor; +in vec2 TexCoord; + +// texture sampler +uniform sampler2D texture1; + +void main() +{ + FragColor = texture(texture1, TexCoord); +} \ No newline at end of file diff --git a/src/client/shaders/test.vert b/src/client/shaders/test.vert new file mode 100644 index 00000000..4fd66038 --- /dev/null +++ b/src/client/shaders/test.vert @@ -0,0 +1,14 @@ +#version 330 core +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aColor; +layout (location = 2) in vec2 aTexCoord; + +out vec3 ourColor; +out vec2 TexCoord; + +void main() +{ + gl_Position = vec4(aPos, 1.0); + ourColor = aColor; + TexCoord = vec2(aTexCoord.x, aTexCoord.y); +} \ No newline at end of file From 1ad37547d6afbd79953cbeb1435e96fd379ff7d9 Mon Sep 17 00:00:00 2001 From: David Min Date: Sun, 5 May 2024 18:56:17 -0700 Subject: [PATCH 66/92] Client main using LearnOpenGL texture code --- src/client/main.cpp | 248 ++++++++++++++++++++++---------------------- 1 file changed, 123 insertions(+), 125 deletions(-) diff --git a/src/client/main.cpp b/src/client/main.cpp index cb5f5888..e7c56009 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -89,131 +89,129 @@ int main(int argc, char* argv[]) // sound.play(); - // float vertices[] = { - // // positions // colors // texture coords - // 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right - // 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right - // -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left - // -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left - // }; - // unsigned int indices[] = { - // 0, 1, 3, // first triangle - // 1, 2, 3 // second triangle - // }; - // unsigned int VBO, VAO, EBO; - // glGenVertexArrays(1, &VAO); - // glGenBuffers(1, &VBO); - // glGenBuffers(1, &EBO); - - // glBindVertexArray(VAO); - - // glBindBuffer(GL_ARRAY_BUFFER, VBO); - // glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); - // glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - - // // position attribute - // glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); - // glEnableVertexAttribArray(0); - // // color attribute - // glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); - // glEnableVertexAttribArray(1); - // // texture coord attribute - // glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); - // glEnableVertexAttribArray(2); - - - // // load and create a texture - // // ------------------------- - // unsigned int texture1; - // // texture 1 - // // --------- - // glGenTextures(1, &texture1); - // glBindTexture(GL_TEXTURE_2D, texture1); - // // set the texture wrapping parameters - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - // // set texture filtering parameters - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // // load image, create texture and generate mipmaps - // int width, height, nrChannels; - // stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis. - // // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path. - // auto img_root = getRepoRoot() / "assets/imgs"; - - // std::cout << (img_root / "awesomeface.png").string() << std::endl; - // unsigned char *data = stbi_load((img_root / "awesomeface.png").string().c_str(), &width, &height, &nrChannels, 0); - // if (data) - // { - // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); - // glGenerateMipmap(GL_TEXTURE_2D); - // } - // else - // { - // std::cout << "Failed to load texture" << std::endl; - // } - // std::cout << "data: " << data << std::endl; - // stbi_image_free(data); - - // auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; - // auto imgShaderProgram = LoadShaders((shader_path / "test.vert").string(), (shader_path / "test.frag").string()); - // if (!imgShaderProgram) { - // std::cerr << "Failed to initialize test shader program" << std::endl; - // return false; - // } - - // // tell opengl for each sampler to which texture unit it belongs to (only has to be done once) - // // ------------------------------------------------------------------------------------------- - // glUseProgram(imgShaderProgram); // don't forget to activate/use the shader before setting uniforms! - // // either set it manually like so: - // glUniform1i(glGetUniformLocation(imgShaderProgram, "texture1"), texture1); - // // or set it via the texture class - - // std::cout << texture1 << std::endl; - - - - // // render loop - // // ----------- - // while (!glfwWindowShouldClose(window)) - // { - // // input - // // ----- - // // processInput(window); - - // // render - // // ------ - // glClearColor(0.2f, 0.3f, 0.3f, 1.0f); - // glClear(GL_COLOR_BUFFER_BIT); - - // // bind textures on corresponding texture units - // glActiveTexture(GL_TEXTURE3); - // glBindTexture(GL_TEXTURE_2D, texture1); - - // // render container - // glUseProgram(imgShaderProgram); - // glBindVertexArray(VAO); - // glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - // glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - - // // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) - // // ------------------------------------------------------------------------------- - // glfwSwapBuffers(window); - // glfwPollEvents(); - // } - - // // optional: de-allocate all resources once they've outlived their purpose: - // // ------------------------------------------------------------------------ - // glDeleteVertexArrays(1, &VAO); - // glDeleteBuffers(1, &VBO); - // glDeleteBuffers(1, &EBO); - - // // glfw: terminate, clearing all previously allocated GLFW resources. - // // ------------------------------------------------------------------ - // glfwTerminate(); - // return 0; + float vertices[] = { + // positions // colors // texture coords + 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right + 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left + -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left + }; + unsigned int indices[] = { + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + unsigned int VBO, VAO, EBO; + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(VAO); + + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // position attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + // color attribute + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + // texture coord attribute + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); + glEnableVertexAttribArray(2); + + + // load and create a texture + // ------------------------- + unsigned int texture1; + // texture 1 + // --------- + glGenTextures(1, &texture1); + glBindTexture(GL_TEXTURE_2D, texture1); + // set the texture wrapping parameters + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + // set texture filtering parameters + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // load image, create texture and generate mipmaps + int width, height, nrChannels; + stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis. + // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path. + auto img_root = getRepoRoot() / "assets/imgs"; + + std::cout << (img_root / "awesomeface.png").string() << std::endl; + unsigned char *data = stbi_load((img_root / "awesomeface.png").string().c_str(), &width, &height, &nrChannels, 0); + if (data) + { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + glGenerateMipmap(GL_TEXTURE_2D); + } + else + { + std::cout << "Failed to load texture" << std::endl; + } + std::cout << "data: " << data << std::endl; + stbi_image_free(data); + + auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; + auto imgShaderProgram = LoadShaders((shader_path / "test.vert").string(), (shader_path / "test.frag").string()); + if (!imgShaderProgram) { + std::cerr << "Failed to initialize test shader program" << std::endl; + return false; + } + + // tell opengl for each sampler to which texture unit it belongs to (only has to be done once) + // ------------------------------------------------------------------------------------------- + glUseProgram(imgShaderProgram); // don't forget to activate/use the shader before setting uniforms! + // either set it manually like so: + glUniform1i(glGetUniformLocation(imgShaderProgram, "texture1"), texture1); + // or set it via the texture class + + std::cout << texture1 << std::endl; + + // render loop + // ----------- + while (!glfwWindowShouldClose(window)) + { + // input + // ----- + // processInput(window); + + // render + // ------ + glClearColor(0.2f, 0.3f, 0.3f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + // bind textures on corresponding texture units + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, texture1); + + // render container + glUseProgram(imgShaderProgram); + glBindVertexArray(VAO); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + + // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) + // ------------------------------------------------------------------------------- + glfwSwapBuffers(window); + glfwPollEvents(); + } + + // optional: de-allocate all resources once they've outlived their purpose: + // ------------------------------------------------------------------------ + glDeleteVertexArrays(1, &VAO); + glDeleteBuffers(1, &VBO); + glDeleteBuffers(1, &EBO); + + // glfw: terminate, clearing all previously allocated GLFW resources. + // ------------------------------------------------------------------ + glfwTerminate(); + return 0; // Loop while GLFW window should stay open. while (!glfwWindowShouldClose(window)) { From 7fe6abdb79c26fb6aeb2afb136b6b01044e5fcd6 Mon Sep 17 00:00:00 2001 From: David Min Date: Sun, 5 May 2024 19:21:31 -0700 Subject: [PATCH 67/92] Resolved texture rendering from cull_face --- src/client/gui/font/loader.cpp | 14 +- src/client/gui/gui.cpp | 91 +++++------ src/client/gui/imgs/loader.cpp | 33 ++-- src/client/gui/widget/dyntext.cpp | 8 +- src/client/gui/widget/staticimg.cpp | 2 +- src/client/main.cpp | 234 ++++++++++++++-------------- 6 files changed, 192 insertions(+), 190 deletions(-) diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 84bbc059..d4cbfcfd 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -25,12 +25,12 @@ bool Loader::init() { // so this is supposed to prevent seg faults related to that glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - // if (!this->_loadFont(Font::MENU)) { - // return false; - // } - // if (!this->_loadFont(Font::TEXT)) { - // return false; - // } + if (!this->_loadFont(Font::MENU)) { + return false; + } + if (!this->_loadFont(Font::TEXT)) { + return false; + } FT_Done_FreeType(this->ft); // done loading fonts, so can release these resources @@ -56,7 +56,7 @@ bool Loader::_loadFont(Font font) { return false; } - for (auto font_size : {FontSizePx::SMALL, FontSizePx::MEDIUM, FontSizePx::LARGE, FontSizePx::XLARGE}) { + for (auto font_size : {FontSizePx::MEDIUM}) { FT_Set_Pixel_Sizes(face, 0, font_size); std::unordered_map characters; for (unsigned char c = 0; c < 128; c++) { diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index c397e244..cf7cad99 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -23,9 +23,9 @@ bool GUI::init(GLuint text_shader, GLuint image_shader) this->fonts = std::make_shared(); this->capture_keystrokes = false; - // if (!this->fonts->init()) { - // return false; - // } + if (!this->fonts->init()) { + return false; + } if (!this->images.init()) { return false; @@ -35,7 +35,7 @@ bool GUI::init(GLuint text_shader, GLuint image_shader) widget::DynText::shader = text_shader; widget::StaticImg::shader = image_shader; glUniformMatrix4fv(glGetUniformLocation(widget::DynText::shader, "projection"), 1, false, reinterpret_cast(&projection)); - glUniformMatrix4fv(glGetUniformLocation(widget::StaticImg::shader, "projection"), 1, false, reinterpret_cast(&projection)); + // glUniformMatrix4fv(glGetUniformLocation(widget::StaticImg::shader, "projection"), 1, false, reinterpret_cast(&projection)); std::cout << "Initialized GUI\n"; return true; @@ -51,14 +51,12 @@ void GUI::renderFrame() { glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_CULL_FACE); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); for (auto& [handle, widget] : this->widgets) { widget->render(); } - glDisable(GL_CULL_FACE); glDisable(GL_BLEND); } @@ -151,49 +149,44 @@ void GUI::layoutFrame(GUIState state) { void GUI::_layoutTitleScreen() { - // this->addWidget(widget::CenterText::make( - // "Arcana", - // font::Font::MENU, - // font::FontSizePx::XLARGE, - // font::FontColor::RED, - // fonts, - // FRAC_WINDOW_HEIGHT(2, 3) - // )); - - // auto start_text = widget::DynText::make( - // "Start Game", - // fonts, - // widget::DynText::Options { - // .font = font::Font::MENU, - // .font_size = font::FontSizePx::MEDIUM, - // .color = font::getRGB(font::FontColor::BLACK), - // .scale = 1.0f, - // } - // ); - // start_text->addOnHover([this](widget::Handle handle) { - // auto widget = this->borrowWidget(handle); - // widget->changeColor(font::FontColor::RED); - // }); - // start_text->addOnClick([this](widget::Handle handle) { - // client->gui_state = GUIState::LOBBY_BROWSER; - // }); - // auto start_flex = widget::Flexbox::make( - // glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), - // glm::vec2(WINDOW_WIDTH, 0.0f), - // widget::Flexbox::Options { - // .direction = widget::JustifyContent::VERTICAL, - // .alignment = widget::AlignItems::CENTER, - // .padding = 0.0f, - // } - // ); - // start_flex->push(std::move(start_text)); - // this->addWidget(std::move(start_flex)); - - // auto yoshi = widget::StaticImg::make( - // glm::vec2(200.0f, 200.0f), - // this->images.getImg(img::ImgID::Yoshi) - // ); - // this->addWidget(std::move(yoshi)); + this->addWidget(widget::CenterText::make( + "Arcana", + font::Font::MENU, + font::FontSizePx::MEDIUM, + font::FontColor::RED, + fonts, + FRAC_WINDOW_HEIGHT(2, 3) + )); + + auto start_text = widget::DynText::make( + "Start Game", + fonts, + widget::DynText::Options { + .font = font::Font::MENU, + .font_size = font::FontSizePx::MEDIUM, + .color = font::getRGB(font::FontColor::BLACK), + .scale = 1.0f, + } + ); + start_text->addOnHover([this](widget::Handle handle) { + auto widget = this->borrowWidget(handle); + widget->changeColor(font::FontColor::RED); + }); + start_text->addOnClick([this](widget::Handle handle) { + client->gui_state = GUIState::LOBBY_BROWSER; + }); + auto start_flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options { + .direction = widget::JustifyContent::VERTICAL, + .alignment = widget::AlignItems::CENTER, + .padding = 0.0f, + } + ); + + start_flex->push(std::move(start_text)); + this->addWidget(std::move(start_flex)); auto pikachu = widget::StaticImg::make( glm::vec2(100.0f, 100.0f), diff --git a/src/client/gui/imgs/loader.cpp b/src/client/gui/imgs/loader.cpp index 46b7641c..d7141226 100644 --- a/src/client/gui/imgs/loader.cpp +++ b/src/client/gui/imgs/loader.cpp @@ -21,36 +21,39 @@ bool Loader::init() { } bool Loader::_loadImg(ImgID img_id) { - auto path = getImgFilepath(img_id); - std::cout << "Loading " << path << "...\n"; + GLuint texture_id; + + glGenTextures(1, &texture_id); + glBindTexture(GL_TEXTURE_2D, texture_id); + + // set Texture wrap and filter modes + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); int width, height, channels; - stbi_set_flip_vertically_on_load(1); + stbi_set_flip_vertically_on_load(true); + + auto path = getImgFilepath(img_id); + std::cout << "Loading " << path << "...\n"; unsigned char* img_data = stbi_load(path.c_str(), &width, &height, &channels, 0); std::cout << img_data << std::endl; if (stbi_failure_reason()) std::cout << "failure: " << stbi_failure_reason() << std::endl; - GLuint texture_id; if (img_data == 0 || width == 0 || height == 0) { std::cerr << "Error loading " << path << "! " << img_data << ", " << width << ", " << height << "\n" << std::endl; return false; } - glGenTextures(1, &texture_id); - glBindTexture(GL_TEXTURE_2D, texture_id); - - // set Texture wrap and filter modes - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data); - glGenerateMipmap(GL_TEXTURE_2D); + if (img_data) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data); + glGenerateMipmap(GL_TEXTURE_2D); + } // unbind texture glBindTexture(GL_TEXTURE_2D, 0); diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 15547986..9cb30176 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -56,12 +56,14 @@ DynText::DynText(std::string text, std::shared_ptr fonts, Dyn DynText({0.0f, 0.0f}, text, fonts, options) {} void DynText::render() { + glEnable(GL_CULL_FACE); + glUseProgram(DynText::shader); glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); glUniform3f(glGetUniformLocation(DynText::shader, "textColor"), this->options.color.x, this->options.color.y, this->options.color.z); - glActiveTexture(GL_TEXTURE0); + // glActiveTexture(GL_TEXTURE0); glBindVertexArray(VAO); float x = this->origin.x; @@ -98,10 +100,12 @@ void DynText::render() { // now advance cursors for next glyph (note that advance is number of 1/64 pixels) x += (ch.advance >> 6) * this->options.scale; // bitshift by 6 to get value in pixels (2^6 = 64) } + glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); - glUseProgram(0); + + glDisable(GL_CULL_FACE); } void DynText::changeColor(font::FontColor new_color) { diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 62f0eea1..75ace5b1 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -99,7 +99,7 @@ void StaticImg::render() { // glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "model"), 1, false, reinterpret_cast(&model)); // glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 1.0f, 1.0f, 1.0f); - glActiveTexture(GL_TEXTURE2); + // glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, this->texture_id); glBindVertexArray(quadVAO); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); diff --git a/src/client/main.cpp b/src/client/main.cpp index e7c56009..9f7044e9 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -89,129 +89,131 @@ int main(int argc, char* argv[]) // sound.play(); - float vertices[] = { - // positions // colors // texture coords - 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right - 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right - -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left - -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left - }; - unsigned int indices[] = { - 0, 1, 3, // first triangle - 1, 2, 3 // second triangle - }; - unsigned int VBO, VAO, EBO; - glGenVertexArrays(1, &VAO); - glGenBuffers(1, &VBO); - glGenBuffers(1, &EBO); - - glBindVertexArray(VAO); - - glBindBuffer(GL_ARRAY_BUFFER, VBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - - // position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - // color attribute - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - // texture coord attribute - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); - glEnableVertexAttribArray(2); + // float vertices[] = { + // // positions // colors // texture coords + // 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right + // 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right + // -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left + // -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left + // }; + // unsigned int indices[] = { + // 0, 1, 3, // first triangle + // 1, 2, 3 // second triangle + // }; + // unsigned int VBO, VAO, EBO; + // glGenVertexArrays(1, &VAO); + // glGenBuffers(1, &VBO); + // glGenBuffers(1, &EBO); + + // glBindVertexArray(VAO); + + // glBindBuffer(GL_ARRAY_BUFFER, VBO); + // glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + // glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // // position attribute + // glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); + // glEnableVertexAttribArray(0); + // // color attribute + // glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); + // glEnableVertexAttribArray(1); + // // texture coord attribute + // glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); + // glEnableVertexAttribArray(2); // load and create a texture // ------------------------- - unsigned int texture1; - // texture 1 - // --------- - glGenTextures(1, &texture1); - glBindTexture(GL_TEXTURE_2D, texture1); - // set the texture wrapping parameters - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - // set texture filtering parameters - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // load image, create texture and generate mipmaps - int width, height, nrChannels; - stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis. - // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path. - auto img_root = getRepoRoot() / "assets/imgs"; - - std::cout << (img_root / "awesomeface.png").string() << std::endl; - unsigned char *data = stbi_load((img_root / "awesomeface.png").string().c_str(), &width, &height, &nrChannels, 0); - if (data) - { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); - glGenerateMipmap(GL_TEXTURE_2D); - } - else - { - std::cout << "Failed to load texture" << std::endl; - } - std::cout << "data: " << data << std::endl; - stbi_image_free(data); - - auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; - auto imgShaderProgram = LoadShaders((shader_path / "test.vert").string(), (shader_path / "test.frag").string()); - if (!imgShaderProgram) { - std::cerr << "Failed to initialize test shader program" << std::endl; - return false; - } - - // tell opengl for each sampler to which texture unit it belongs to (only has to be done once) - // ------------------------------------------------------------------------------------------- - glUseProgram(imgShaderProgram); // don't forget to activate/use the shader before setting uniforms! - // either set it manually like so: - glUniform1i(glGetUniformLocation(imgShaderProgram, "texture1"), texture1); - // or set it via the texture class - - std::cout << texture1 << std::endl; - - // render loop - // ----------- - while (!glfwWindowShouldClose(window)) - { - // input - // ----- - // processInput(window); - - // render - // ------ - glClearColor(0.2f, 0.3f, 0.3f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - // bind textures on corresponding texture units - glActiveTexture(GL_TEXTURE3); - glBindTexture(GL_TEXTURE_2D, texture1); - - // render container - glUseProgram(imgShaderProgram); - glBindVertexArray(VAO); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - - // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) - // ------------------------------------------------------------------------------- - glfwSwapBuffers(window); - glfwPollEvents(); - } - - // optional: de-allocate all resources once they've outlived their purpose: - // ------------------------------------------------------------------------ - glDeleteVertexArrays(1, &VAO); - glDeleteBuffers(1, &VBO); - glDeleteBuffers(1, &EBO); + // unsigned int texture1; + // // texture 1 + // // --------- + // glGenTextures(1, &texture1); + // glBindTexture(GL_TEXTURE_2D, texture1); + // // set the texture wrapping parameters + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + // // set texture filtering parameters + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // // load image, create texture and generate mipmaps + // int width, height, nrChannels; + // stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis. + // // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path. + // auto img_root = getRepoRoot() / "assets/imgs"; + + // std::cout << (img_root / "awesomeface.png").string() << std::endl; + // unsigned char *data = stbi_load((img_root / "awesomeface.png").string().c_str(), &width, &height, &nrChannels, 0); + // if (data) + // { + // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + // glGenerateMipmap(GL_TEXTURE_2D); + // } + // else + // { + // std::cout << "Failed to load texture" << std::endl; + // } + // std::cout << "data: " << data << std::endl; + // stbi_image_free(data); + + // auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; + // auto imgShaderProgram = LoadShaders((shader_path / "test.vert").string(), (shader_path / "test.frag").string()); + // if (!imgShaderProgram) { + // std::cerr << "Failed to initialize test shader program" << std::endl; + // return false; + // } + + // // tell opengl for each sampler to which texture unit it belongs to (only has to be done once) + // // ------------------------------------------------------------------------------------------- + // glUseProgram(imgShaderProgram); // don't forget to activate/use the shader before setting uniforms! + // // either set it manually like so: + // glUniform1i(glGetUniformLocation(imgShaderProgram, "texture1"), texture1); + // // or set it via the texture class + + // std::cout << texture1 << std::endl; + + + + // // render loop + // // ----------- + // while (!glfwWindowShouldClose(window)) + // { + // // input + // // ----- + // // processInput(window); + + // // render + // // ------ + // glClearColor(0.2f, 0.3f, 0.3f, 1.0f); + // glClear(GL_COLOR_BUFFER_BIT); + + // // bind textures on corresponding texture units + // glActiveTexture(GL_TEXTURE3); + // glBindTexture(GL_TEXTURE_2D, texture1); + + // // render container + // glUseProgram(imgShaderProgram); + // glBindVertexArray(VAO); + // glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + // glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + + // // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) + // // ------------------------------------------------------------------------------- + // glfwSwapBuffers(window); + // glfwPollEvents(); + // } + + // // optional: de-allocate all resources once they've outlived their purpose: + // // ------------------------------------------------------------------------ + // glDeleteVertexArrays(1, &VAO); + // glDeleteBuffers(1, &VBO); + // glDeleteBuffers(1, &EBO); // glfw: terminate, clearing all previously allocated GLFW resources. // ------------------------------------------------------------------ - glfwTerminate(); - return 0; + // glfwTerminate(); + // return 0; // Loop while GLFW window should stay open. while (!glfwWindowShouldClose(window)) { From f784d702c37599f737563c4dc8f665bba53c78c0 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 5 May 2024 19:52:07 -0700 Subject: [PATCH 68/92] add TODO comments to remind myself what I need to do --- include/client/gui/gui.hpp | 11 +++++++++++ src/client/gui/font/loader.cpp | 2 ++ src/client/gui/gui.cpp | 2 ++ 3 files changed, 15 insertions(+) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 7342392e..cc8baec2 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -20,6 +20,17 @@ class Client; +/* + 1. Reduce number of textures being loaded by fonts + only load one font size + using scale factor + potentially also only load necessary ASCII characters, not random @$^#&*((*))*&*(^&) + 2. move shader loading inside of gui.init() + 3. dont force the user to pass in img / font loaders into widgets + 4. allow semi dynamic screen size scaling, with static 3:2 aspect ratio + 5. Text+Img Widget combined + 6. rename StaticImg -> Sprite[something] +*/ + namespace gui { /** diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index d4cbfcfd..17cb8087 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -1,5 +1,7 @@ #include "client/gui/font/loader.hpp" + + #include // freetype needs this extra include for whatever unholy reason diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index cf7cad99..96a7c04e 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -8,6 +8,8 @@ #include "shared/game/sharedgamestate.hpp" + + namespace gui { glm::mat4 GUI::projection = glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); From 77c98f647ed3e561758e290fb135ba19ae6bc607 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 5 May 2024 22:02:26 -0700 Subject: [PATCH 69/92] refactor font loading to only load one size and then scale, but text is randomly flickering --- include/client/client.hpp | 4 +- include/client/constants.hpp | 6 + include/client/gui/font/font.hpp | 38 ++- include/client/gui/font/loader.hpp | 18 +- include/client/gui/widget/centertext.hpp | 4 +- include/client/gui/widget/dyntext.hpp | 12 +- include/client/gui/widget/flexbox.hpp | 11 +- include/client/gui/widget/options.hpp | 4 +- include/client/gui/widget/textinput.hpp | 5 - src/client/gui/font/font.cpp | 24 +- src/client/gui/font/loader.cpp | 95 ++++--- src/client/gui/gui.cpp | 109 +++----- src/client/gui/widget/centertext.cpp | 22 +- src/client/gui/widget/dyntext.cpp | 46 ++-- src/client/gui/widget/flexbox.cpp | 22 +- src/client/gui/widget/staticimg.cpp | 2 +- src/client/gui/widget/textinput.cpp | 15 +- src/client/main.cpp | 317 +---------------------- 18 files changed, 181 insertions(+), 573 deletions(-) create mode 100644 include/client/constants.hpp diff --git a/include/client/client.hpp b/include/client/client.hpp index 8f64dd1a..bf869cba 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -24,8 +24,8 @@ #include "shared/network/session.hpp" #include "shared/utilities/config.hpp" -#define WINDOW_WIDTH 900 -#define WINDOW_HEIGHT 600 +#define WINDOW_WIDTH 1500 +#define WINDOW_HEIGHT 1000 // position something a "frac" of the way across the screen // e.g. WIDTH_FRAC(1, 4) -> a fourth of the way from the left diff --git a/include/client/constants.hpp b/include/client/constants.hpp new file mode 100644 index 00000000..b0215aea --- /dev/null +++ b/include/client/constants.hpp @@ -0,0 +1,6 @@ +#pragma once + +// The screen width that's defined as 1:1, from which other +// resolutions can calculate pixel sizes +#define UNIT_WINDOW_WIDTH 1500 +#define UNIT_WINDOW_HEIGHT 1000 \ No newline at end of file diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index fde52847..0699415a 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -4,9 +4,11 @@ #include #include +#include namespace gui::font { + /** * Abstract representation of the different fonts to use in our game * @@ -19,33 +21,41 @@ enum class Font { }; /** - * Preset sizes for fonts + * Mappings from our specified abstract fonts to the file to load */ -enum FontSizePx { - SMALL = 64, - MEDIUM = 96, - LARGE = 128, - XLARGE = 256, -}; +std::string getFilepath(Font font); /** * Preset colors for text */ -enum class FontColor { +enum class Color { BLACK, RED, BLUE, GRAY }; -/** - * Mappings from our specified abstract fonts to the file to load - */ -std::string getFilepath(Font font); - /** * Mapping from preset font colors to RGB values */ -glm::vec3 getRGB(FontColor color); +glm::vec3 getRGB(Color color); + +const int UNIT_LARGE_SIZE_PX = 128; // how many pixels a small font is on the unit screen size +enum class Size { + SMALL, + MEDIUM, + LARGE, + XLARGE +}; +const std::unordered_map SIZE_TO_SCALE = { + {Size::SMALL, 0.25f}, + {Size::MEDIUM, 0.50f}, + {Size::LARGE, 1.0f}, + {Size::XLARGE, 2.0f}, +}; + +float getFontSizePx(Size size); +float getScaleFactor(Size size); + } diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp index fecdf1d4..b991d823 100644 --- a/include/client/gui/font/loader.hpp +++ b/include/client/gui/font/loader.hpp @@ -23,14 +23,6 @@ struct Character { unsigned int advance; /// offset to advance to next glyph }; -/** - * Hash function so we can provide a pair of Font and FontSize and get the char - * mappings for that font. - */ -struct font_pair_hash { - std::size_t operator()(const std::pair& p) const; -}; - /** * Handles loading all of the fonts we want to use, and provides an interface to get * the Character information (e.g. opengl texture and sizing information). @@ -49,11 +41,10 @@ class Loader { * Loads the specified character with the specified font and size * * @param c ASCII char to load - * @param font Abstract font to use. NOTE: must be one of the preset values we provide! - * @param FontSizePx size of the font in pixels to load + * @param font Abstract font to use. * @returns the Character information for that glyph */ - [[nodiscard]] const Character& loadChar(char c, Font font, FontSizePx size) const; + [[nodiscard]] const Character& loadChar(char c, Font font) const; private: FT_Library ft; @@ -64,9 +55,8 @@ class Loader { bool _loadFont(Font font); std::unordered_map< - std::pair, - std::unordered_map, - font_pair_hash + Font, + std::unordered_map > font_map; }; diff --git a/include/client/gui/widget/centertext.hpp b/include/client/gui/widget/centertext.hpp index 8e041f13..45a25a04 100644 --- a/include/client/gui/widget/centertext.hpp +++ b/include/client/gui/widget/centertext.hpp @@ -27,8 +27,8 @@ class CenterText { static Widget::Ptr make( std::string text, font::Font font, - font::FontSizePx size, - font::FontColor color, + font::Size size, + font::Color color, std::shared_ptr fonts, float y_pos ); diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index b016d855..2b4699c8 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -16,10 +16,12 @@ class DynText : public Widget { static GLuint shader; struct Options { + Options(font::Font font, font::Size size, font::Color color): + font(font), size(size), color(color) {} + font::Font font {font::Font::TEXT}; - font::FontSizePx font_size {font::FontSizePx::MEDIUM}; - glm::vec3 color {font::getRGB(font::FontColor::BLACK)}; - float scale {1.0}; + font::Size size {font::Size::SMALL}; + font::Color color {font::Color::BLACK}; }; /** @@ -38,13 +40,11 @@ class DynText : public Widget { } DynText(glm::vec2 origin, std::string text, std::shared_ptr loader, Options options); - DynText(glm::vec2 origin, std::string text, std::shared_ptr loader); DynText(std::string text, std::shared_ptr loader, Options options); - DynText(std::string text, std::shared_ptr loader); void render() override; - void changeColor(font::FontColor new_color); + void changeColor(font::Color new_color); private: Options options; diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp index 5adfa1ae..58eb5729 100644 --- a/include/client/gui/widget/flexbox.hpp +++ b/include/client/gui/widget/flexbox.hpp @@ -12,9 +12,12 @@ class Flexbox : public Widget { using Ptr = std::unique_ptr; struct Options { - JustifyContent direction; - AlignItems alignment; - float padding; + Options(Justify direction, Align alignment, float padding): + direction(direction), alignment(alignment), padding(padding) {} + + Justify direction; + Align alignment; + float padding; }; /** @@ -37,9 +40,7 @@ class Flexbox : public Widget { } Flexbox(glm::vec2 origin, glm::vec2 size, Options options); - Flexbox(glm::vec2 origin, glm::vec2 size); Flexbox(glm::vec2 origin, Options options); - explicit Flexbox(glm::vec2 origin); void doClick(float x, float y) override; void doHover(float x, float y) override; diff --git a/include/client/gui/widget/options.hpp b/include/client/gui/widget/options.hpp index 244efe94..eca38ff0 100644 --- a/include/client/gui/widget/options.hpp +++ b/include/client/gui/widget/options.hpp @@ -2,12 +2,12 @@ namespace gui::widget { -enum class JustifyContent { +enum class Justify { VERTICAL, HORIZONTAL }; -enum class AlignItems { +enum class Align { CENTER, LEFT, RIGHT diff --git a/include/client/gui/widget/textinput.hpp b/include/client/gui/widget/textinput.hpp index e2cccf5b..3be609eb 100644 --- a/include/client/gui/widget/textinput.hpp +++ b/include/client/gui/widget/textinput.hpp @@ -43,11 +43,6 @@ class TextInput : public Widget { std::shared_ptr fonts, DynText::Options options); - TextInput(glm::vec2 origin, - std::string placeholder, - gui::GUI* gui, - std::shared_ptr fonts); - void render() override; bool hasHandle(Handle handle) const override; diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index 29a67b38..45bc7377 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -1,10 +1,23 @@ #include "client/gui/font/font.hpp" #include "client/core.hpp" +#include "client/constants.hpp" +#include "client/client.hpp" #include "shared/utilities/root_path.hpp" namespace gui::font { +float getFontSizePx(Size size) { + return UNIT_LARGE_SIZE_PX * getScaleFactor(size); +} + +float getScaleFactor(Size size) { + float screen_factor = WINDOW_WIDTH / UNIT_WINDOW_WIDTH; + + std::cout << SIZE_TO_SCALE.at(size) * screen_factor << "\n"; + return SIZE_TO_SCALE.at(size) * screen_factor; +} + std::string getFilepath(Font font) { auto dir = getRepoRoot() / "assets/fonts"; switch (font) { @@ -14,16 +27,17 @@ std::string getFilepath(Font font) { } } -glm::vec3 getRGB(FontColor color) { +glm::vec3 getRGB(Color color) { switch (color) { - case FontColor::RED: + case Color::RED: return {1.0f, 0.0f, 0.0f}; - case FontColor::BLUE: + case Color::BLUE: return {0.0f, 0.0f, 1.0f}; - case FontColor::GRAY: + case Color::GRAY: return {0.5f, 0.5f, 0.5f}; + + case Color::BLACK: default: - case FontColor::BLACK: return {0.0f, 0.0f, 0.0f}; } } diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 17cb8087..67614bf9 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -1,7 +1,5 @@ #include "client/gui/font/loader.hpp" - - #include // freetype needs this extra include for whatever unholy reason @@ -12,11 +10,6 @@ namespace gui::font { -std::size_t font_pair_hash::operator()(const std::pair& p) const { - // idk if this is actually doing what I think it is doing - return (static_cast(p.first) << 32 ^ p.second); -} - bool Loader::init() { if (FT_Init_FreeType(&this->ft)) { std::cerr << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; @@ -39,12 +32,14 @@ bool Loader::init() { return true; } -const Character& Loader::loadChar(char c, Font font, FontSizePx size) const { - if (!this->font_map.contains({font, size}) || !this->font_map.at({font, size}).contains(c)) { - return this->font_map.at({Font::TEXT, FontSizePx::MEDIUM}).at('?'); +const Character& Loader::loadChar(char c, Font font) const { + auto char_map = this->font_map.at(font); + + if (!char_map.contains(c)) { + return char_map.at('?'); } - return this->font_map.at({font, size}).at(c); + return char_map.at(c); } bool Loader::_loadFont(Font font) { @@ -58,49 +53,47 @@ bool Loader::_loadFont(Font font) { return false; } - for (auto font_size : {FontSizePx::MEDIUM}) { - FT_Set_Pixel_Sizes(face, 0, font_size); - std::unordered_map characters; - for (unsigned char c = 0; c < 128; c++) { - // load character glyph - if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { - std::cerr << "ERROR::FREETYTPE: Failed to load Glyph " << c << std::endl; - return false; - } - - // generate texture - unsigned int texture; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_RED, - face->glyph->bitmap.width, - face->glyph->bitmap.rows, - 0, - GL_RED, - GL_UNSIGNED_BYTE, - face->glyph->bitmap.buffer - ); - // set texture options - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // now store character for later use - Character character = { - texture, - glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), - glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), - static_cast(face->glyph->advance.x) - }; - characters.insert({c, character}); + FT_Set_Pixel_Sizes(face, 0, UNIT_LARGE_SIZE_PX); + std::unordered_map characters; + for (unsigned char c = 0; c < 128; c++) { + // load character glyph + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { + std::cerr << "ERROR::FREETYTPE: Failed to load Glyph " << c << std::endl; + return false; } - - this->font_map.insert({{font, font_size}, characters}); + + // generate texture + unsigned int texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RED, + face->glyph->bitmap.width, + face->glyph->bitmap.rows, + 0, + GL_RED, + GL_UNSIGNED_BYTE, + face->glyph->bitmap.buffer + ); + // set texture options + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // now store character for later use + Character character = { + texture, + glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), + glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), + static_cast(face->glyph->advance.x) + }; + characters.insert({c, character}); } + this->font_map.insert({font, characters}); + FT_Done_Face(face); return true; diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 96a7c04e..ec31aaf8 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -7,9 +7,6 @@ #include "client/client.hpp" #include "shared/game/sharedgamestate.hpp" - - - namespace gui { glm::mat4 GUI::projection = glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); @@ -37,7 +34,6 @@ bool GUI::init(GLuint text_shader, GLuint image_shader) widget::DynText::shader = text_shader; widget::StaticImg::shader = image_shader; glUniformMatrix4fv(glGetUniformLocation(widget::DynText::shader, "projection"), 1, false, reinterpret_cast(&projection)); - // glUniformMatrix4fv(glGetUniformLocation(widget::StaticImg::shader, "projection"), 1, false, reinterpret_cast(&projection)); std::cout << "Initialized GUI\n"; return true; @@ -154,8 +150,8 @@ void GUI::_layoutTitleScreen() { this->addWidget(widget::CenterText::make( "Arcana", font::Font::MENU, - font::FontSizePx::MEDIUM, - font::FontColor::RED, + font::Size::XLARGE, + font::Color::RED, fonts, FRAC_WINDOW_HEIGHT(2, 3) )); @@ -163,16 +159,11 @@ void GUI::_layoutTitleScreen() { auto start_text = widget::DynText::make( "Start Game", fonts, - widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::MEDIUM, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f, - } + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK) ); start_text->addOnHover([this](widget::Handle handle) { auto widget = this->borrowWidget(handle); - widget->changeColor(font::FontColor::RED); + widget->changeColor(font::Color::RED); }); start_text->addOnClick([this](widget::Handle handle) { client->gui_state = GUIState::LOBBY_BROWSER; @@ -180,19 +171,15 @@ void GUI::_layoutTitleScreen() { auto start_flex = widget::Flexbox::make( glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 0.0f, - } + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 0.0f) ); start_flex->push(std::move(start_text)); this->addWidget(std::move(start_flex)); auto pikachu = widget::StaticImg::make( - glm::vec2(100.0f, 100.0f), - this->images.getImg(img::ImgID::Pikachu) + glm::vec2(0.0f, 0.0f), + images.getImg(img::ImgID::Pikachu) ); this->addWidget(std::move(pikachu)); } @@ -201,32 +188,24 @@ void GUI::_layoutLobbyBrowser() { this->addWidget(widget::CenterText::make( "Lobbies", font::Font::MENU, - font::FontSizePx::LARGE, - font::FontColor::BLACK, + font::Size::LARGE, + font::Color::BLACK, this->fonts, - WINDOW_HEIGHT - font::FontSizePx::LARGE + WINDOW_HEIGHT - font::getFontSizePx(font::Size::LARGE) )); auto lobbies_flex = widget::Flexbox::make( glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 10.0f, - }); + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 10.0f) + ); for (const auto& [ip, packet]: client->lobby_finder.getFoundLobbies()) { std::stringstream ss; ss << packet.lobby_name << " " << packet.slots_taken << "/" << packet.slots_avail + packet.slots_taken; - auto entry = widget::DynText::make(ss.str(), - this->fonts, widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::SMALL, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - }); + auto entry = widget::DynText::make(ss.str(), this->fonts, + widget::DynText::Options(font::Font::MENU, font::Size::SMALL, font::Color::BLACK)); entry->addOnClick([ip, this](widget::Handle handle){ std::cout << "Connecting to " << ip.address() << " ...\n"; this->client->connectAndListen(ip.address().to_string()); @@ -234,7 +213,7 @@ void GUI::_layoutLobbyBrowser() { }); entry->addOnHover([this](widget::Handle handle){ auto widget = this->borrowWidget(handle); - widget->changeColor(font::FontColor::RED); + widget->changeColor(font::Color::RED); }); lobbies_flex->push(std::move(entry)); } @@ -243,12 +222,7 @@ void GUI::_layoutLobbyBrowser() { lobbies_flex->push(widget::DynText::make( "No lobbies found...", this->fonts, - widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::SMALL, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - } + widget::DynText::Options(font::Font::MENU, font::Size::SMALL, font::Color::BLACK) )); } @@ -259,12 +233,7 @@ void GUI::_layoutLobbyBrowser() { "Enter a name", this, fonts, - widget::DynText::Options { - .font = font::Font::TEXT, - .font_size = font::FontSizePx::SMALL, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - } + widget::DynText::Options(font::Font::TEXT, font::Size::SMALL, font::Color::BLACK) )); } @@ -272,10 +241,10 @@ void GUI::_layoutLobby() { auto lobby_title = widget::CenterText::make( this->client->gameState.lobby.name, font::Font::MENU, - font::FontSizePx::LARGE, - font::FontColor::BLACK, + font::Size::LARGE, + font::Color::BLACK, this->fonts, - WINDOW_HEIGHT - font::FontSizePx::LARGE + WINDOW_HEIGHT - font::getFontSizePx(font::Size::LARGE) ); this->addWidget(std::move(lobby_title)); std::stringstream ss; @@ -283,32 +252,23 @@ void GUI::_layoutLobby() { auto player_count = widget::CenterText::make( ss.str(), font::Font::MENU, - font::FontSizePx::MEDIUM, - font::FontColor::BLACK, + font::Size::MEDIUM, + font::Color::BLACK, this->fonts, - WINDOW_HEIGHT - (2 * font::FontSizePx::LARGE) - 10.0f + WINDOW_HEIGHT - (2 * font::getFontSizePx(font::Size::LARGE)) - 10.0f ); this->addWidget(std::move(player_count)); auto players_flex = widget::Flexbox::make( glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 5)), glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 10.0f - } + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 10.0f) ); for (const auto& [_eid, player_name] : this->client->gameState.lobby.players) { players_flex->push(widget::DynText::make( player_name, this->fonts, - widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::MEDIUM, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f - } + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK) )); } this->addWidget(std::move(players_flex)); @@ -316,8 +276,8 @@ void GUI::_layoutLobby() { auto waiting_msg = widget::CenterText::make( "Waiting for players...", font::Font::MENU, - font::FontSizePx::MEDIUM, - font::FontColor::GRAY, + font::Size::MEDIUM, + font::Color::GRAY, this->fonts, 30.0f ); @@ -332,16 +292,11 @@ void GUI::_layoutGameEscMenu() { auto exit_game_txt = widget::DynText::make( "Exit Game", fonts, - widget::DynText::Options { - .font = font::Font::MENU, - .font_size = font::FontSizePx::MEDIUM, - .color = font::getRGB(font::FontColor::BLACK), - .scale = 1.0f, - } + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK) ); exit_game_txt->addOnHover([this](widget::Handle handle) { auto widget = this->borrowWidget(handle); - widget->changeColor(font::FontColor::RED); + widget->changeColor(font::Color::RED); }); exit_game_txt->addOnClick([this](widget::Handle handle) { glfwDestroyWindow(this->client->getWindow()); @@ -349,11 +304,7 @@ void GUI::_layoutGameEscMenu() { auto flex = widget::Flexbox::make( glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 2)), glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 0.0f, - } + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 0.0f) ); flex->push(std::move(exit_game_txt)); diff --git a/src/client/gui/widget/centertext.cpp b/src/client/gui/widget/centertext.cpp index 95c6d6f6..e09ed7a5 100644 --- a/src/client/gui/widget/centertext.cpp +++ b/src/client/gui/widget/centertext.cpp @@ -6,28 +6,18 @@ namespace gui::widget { Widget::Ptr CenterText::make( std::string text, font::Font font, - font::FontSizePx size, - font::FontColor color, + font::Size size, + font::Color color, std::shared_ptr fonts, float y_pos ) { auto flex = widget::Flexbox::make( glm::vec2(0.0f, y_pos), glm::vec2(WINDOW_WIDTH, 0.0f), - widget::Flexbox::Options { - .direction = widget::JustifyContent::VERTICAL, - .alignment = widget::AlignItems::CENTER, - .padding = 0.0f, - }); - auto title = widget::DynText::make( - text, - fonts, - widget::DynText::Options { - .font = font, - .font_size = size, - .color = font::getRGB(color), - .scale = 1.0f - }); + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 0.0f) + ); + auto title = widget::DynText::make(text, fonts, + widget::DynText::Options(font, size, color)); flex->push(std::move(title)); return flex; } diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 9cb30176..7b400474 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -30,28 +30,20 @@ DynText::DynText(glm::vec2 origin, std::string text, // Calculate size of string of text this->width = 0; this->height = 0; + + float scale = font::getScaleFactor(options.size); + for (int i = 0; i < text.size(); i++) { - font::Character ch = this->fonts->loadChar(this->text[i], this->options.font, this->options.font_size); - this->height = std::max(this->height, static_cast(ch.size.y)); + font::Character ch = this->fonts->loadChar(this->text[i], this->options.font); + this->height = std::max(this->height, static_cast(ch.size.y * scale)); if (i != text.size() - 1 && i != 0) { - this->width += ch.advance / 64.0; + this->width += (ch.advance >> 6) * scale; } else { - this->width += ch.size.x; + this->width += ch.size.x * scale; } } } -DynText::DynText(glm::vec2 origin, std::string text, std::shared_ptr fonts): - DynText(origin, text, fonts, DynText::Options { - .font {font::Font::MENU}, - .font_size {font::FontSizePx::MEDIUM}, - .color {font::getRGB(font::FontColor::BLACK)}, - .scale {1.0}, - }) {} - -DynText::DynText(std::string text, std::shared_ptr fonts): - DynText({0.0f, 0.0f}, text, fonts) {} - DynText::DynText(std::string text, std::shared_ptr fonts, DynText::Options options): DynText({0.0f, 0.0f}, text, fonts, options) {} @@ -60,10 +52,10 @@ void DynText::render() { glUseProgram(DynText::shader); + glActiveTexture(GL_TEXTURE0); glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); - glUniform3f(glGetUniformLocation(DynText::shader, "textColor"), - this->options.color.x, this->options.color.y, this->options.color.z); - // glActiveTexture(GL_TEXTURE0); + auto color = font::getRGB(this->options.color); + glUniform3f(glGetUniformLocation(DynText::shader, "textColor"), color.x, color.y, color.z); glBindVertexArray(VAO); float x = this->origin.x; @@ -72,13 +64,15 @@ void DynText::render() { // iterate through all characters for (const char& c : this->text) { - font::Character ch = this->fonts->loadChar(c, this->options.font, this->options.font_size); + font::Character ch = this->fonts->loadChar(c, this->options.font); + + float scale = font::getScaleFactor(this->options.size); - float xpos = x + ch.bearing.x * this->options.scale; - float ypos = y - (ch.size.y - ch.bearing.y) * this->options.scale; + float xpos = x + ch.bearing.x * scale; + float ypos = y - (ch.size.y - ch.bearing.y) * scale; - float w = ch.size.x * this->options.scale; - float h = ch.size.y * this->options.scale; + float w = ch.size.x * scale; + float h = ch.size.y * scale; // update VBO for each character float vertices[6][4] = { { xpos, ypos + h, 0.0f, 0.0f }, @@ -98,7 +92,7 @@ void DynText::render() { // render quad glDrawArrays(GL_TRIANGLES, 0, 6); // now advance cursors for next glyph (note that advance is number of 1/64 pixels) - x += (ch.advance >> 6) * this->options.scale; // bitshift by 6 to get value in pixels (2^6 = 64) + x += (ch.advance >> 6) * scale; // bitshift by 6 to get value in pixels (2^6 = 64) } glBindVertexArray(0); @@ -108,8 +102,8 @@ void DynText::render() { glDisable(GL_CULL_FACE); } -void DynText::changeColor(font::FontColor new_color) { - this->options.color = font::getRGB(new_color); +void DynText::changeColor(font::Color new_color) { + this->options.color = new_color; } } \ No newline at end of file diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp index 860fba78..f46fe532 100644 --- a/src/client/gui/widget/flexbox.cpp +++ b/src/client/gui/widget/flexbox.cpp @@ -18,16 +18,6 @@ Flexbox::Flexbox(glm::vec2 origin, glm::vec2 size, Flexbox::Options options): Flexbox::Flexbox(glm::vec2 origin, Flexbox::Options options): Flexbox(origin, {0.0f, 0.0f}, options) {} -Flexbox::Flexbox(glm::vec2 origin, glm::vec2 size): - Flexbox(origin, size, Flexbox::Options { - .direction = JustifyContent::HORIZONTAL, - .alignment = AlignItems::LEFT, - .padding = 0 - }) {} - -Flexbox::Flexbox(glm::vec2 origin): - Flexbox(origin, {0.0f, 0.0f}) {} - void Flexbox::doClick(float x, float y) { Widget::doClick(x, y); for (auto& widget : this->widgets) { @@ -61,12 +51,12 @@ void Flexbox::push(Widget::Ptr&& widget) { prev_height = prev_widget->getSize().second; } - if (this->options.direction == JustifyContent::HORIZONTAL) { + if (this->options.direction == Justify::HORIZONTAL) { this->width += new_width + this->options.padding; this->height = std::max(this->height, new_height); glm::vec2 new_origin(prev_origin.x + prev_width + this->options.padding, prev_origin.y); widget->setOrigin(new_origin); - } else if (this->options.direction == JustifyContent::VERTICAL) { + } else if (this->options.direction == Justify::VERTICAL) { this->height += new_height + this->options.padding; this->width = std::max(this->width, new_width); glm::vec2 new_origin(prev_origin.x, prev_origin.y + prev_height + this->options.padding); @@ -75,17 +65,17 @@ void Flexbox::push(Widget::Ptr&& widget) { this->widgets.push_back(std::move(widget)); - if (this->options.alignment == AlignItems::CENTER) { - if (this->options.direction == JustifyContent::HORIZONTAL) { + if (this->options.alignment == Align::CENTER) { + if (this->options.direction == Justify::HORIZONTAL) { std::cerr << "Note: center alignment not yet implemented for horizontal justify. Doing nothing\n"; - } else if (this->options.direction == JustifyContent::VERTICAL) { + } else if (this->options.direction == Justify::VERTICAL) { for (auto& widget : this->widgets) { const auto [curr_width, _] = widget->getSize(); glm::vec2 new_origin(this->origin.x + ((this->width - curr_width) / 2.0f), widget->getOrigin().y); widget->setOrigin(new_origin); } } - } else if (this->options.alignment == AlignItems::RIGHT) { + } else if (this->options.alignment == Align::RIGHT) { std::cerr << "Note: right alignment not yet implemented. Doing nothing\n"; } // else it is align left, which is default behavior and requires no more messing diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 75ace5b1..7628757a 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -99,7 +99,7 @@ void StaticImg::render() { // glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "model"), 1, false, reinterpret_cast(&model)); // glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 1.0f, 1.0f, 1.0f); - // glActiveTexture(GL_TEXTURE2); + glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, this->texture_id); glBindVertexArray(quadVAO); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); diff --git a/src/client/gui/widget/textinput.cpp b/src/client/gui/widget/textinput.cpp index f8490655..d72c0929 100644 --- a/src/client/gui/widget/textinput.cpp +++ b/src/client/gui/widget/textinput.cpp @@ -19,11 +19,11 @@ TextInput::TextInput(glm::vec2 origin, Widget(Type::TextInput, origin) { std::string text_to_display; - font::FontColor color_to_display; + font::Color color_to_display; std::string captured_input = gui->getCapturedKeyboardInput(); if (captured_input.size() == 0) { text_to_display = placeholder; - options.color = font::getRGB(font::FontColor::GRAY); + options.color = font::Color::GRAY; } else { text_to_display = captured_input; } @@ -45,17 +45,6 @@ TextInput::TextInput(glm::vec2 origin, this->height = height; } -TextInput::TextInput(glm::vec2 origin, - std::string placeholder, - gui::GUI* gui, - std::shared_ptr fonts): - TextInput(origin, placeholder, gui, fonts, DynText::Options { - .font {font::Font::TEXT}, - .font_size {font::FontSizePx::MEDIUM}, - .color {font::getRGB(font::FontColor::BLACK)}, - .scale {1.0}, - }) {} - void TextInput::render() { this->dyntext->render(); } diff --git a/src/client/main.cpp b/src/client/main.cpp index 9f7044e9..46b5e91f 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -87,133 +87,7 @@ int main(int argc, char* argv[]) sound.setLoop(true); - // sound.play(); - - // float vertices[] = { - // // positions // colors // texture coords - // 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right - // 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right - // -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left - // -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left - // }; - // unsigned int indices[] = { - // 0, 1, 3, // first triangle - // 1, 2, 3 // second triangle - // }; - // unsigned int VBO, VAO, EBO; - // glGenVertexArrays(1, &VAO); - // glGenBuffers(1, &VBO); - // glGenBuffers(1, &EBO); - - // glBindVertexArray(VAO); - - // glBindBuffer(GL_ARRAY_BUFFER, VBO); - // glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); - // glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - - // // position attribute - // glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); - // glEnableVertexAttribArray(0); - // // color attribute - // glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); - // glEnableVertexAttribArray(1); - // // texture coord attribute - // glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); - // glEnableVertexAttribArray(2); - - - // load and create a texture - // ------------------------- - // unsigned int texture1; - // // texture 1 - // // --------- - // glGenTextures(1, &texture1); - // glBindTexture(GL_TEXTURE_2D, texture1); - // // set the texture wrapping parameters - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - // // set texture filtering parameters - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // // load image, create texture and generate mipmaps - // int width, height, nrChannels; - // stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis. - // // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path. - // auto img_root = getRepoRoot() / "assets/imgs"; - - // std::cout << (img_root / "awesomeface.png").string() << std::endl; - // unsigned char *data = stbi_load((img_root / "awesomeface.png").string().c_str(), &width, &height, &nrChannels, 0); - // if (data) - // { - // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); - // glGenerateMipmap(GL_TEXTURE_2D); - // } - // else - // { - // std::cout << "Failed to load texture" << std::endl; - // } - // std::cout << "data: " << data << std::endl; - // stbi_image_free(data); - - // auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; - // auto imgShaderProgram = LoadShaders((shader_path / "test.vert").string(), (shader_path / "test.frag").string()); - // if (!imgShaderProgram) { - // std::cerr << "Failed to initialize test shader program" << std::endl; - // return false; - // } - - // // tell opengl for each sampler to which texture unit it belongs to (only has to be done once) - // // ------------------------------------------------------------------------------------------- - // glUseProgram(imgShaderProgram); // don't forget to activate/use the shader before setting uniforms! - // // either set it manually like so: - // glUniform1i(glGetUniformLocation(imgShaderProgram, "texture1"), texture1); - // // or set it via the texture class - - // std::cout << texture1 << std::endl; - - - - // // render loop - // // ----------- - // while (!glfwWindowShouldClose(window)) - // { - // // input - // // ----- - // // processInput(window); - - // // render - // // ------ - // glClearColor(0.2f, 0.3f, 0.3f, 1.0f); - // glClear(GL_COLOR_BUFFER_BIT); - - // // bind textures on corresponding texture units - // glActiveTexture(GL_TEXTURE3); - // glBindTexture(GL_TEXTURE_2D, texture1); - - // // render container - // glUseProgram(imgShaderProgram); - // glBindVertexArray(VAO); - // glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - // glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - - // // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) - // // ------------------------------------------------------------------------------- - // glfwSwapBuffers(window); - // glfwPollEvents(); - // } - - // // optional: de-allocate all resources once they've outlived their purpose: - // // ------------------------------------------------------------------------ - // glDeleteVertexArrays(1, &VAO); - // glDeleteBuffers(1, &VBO); - // glDeleteBuffers(1, &EBO); - - // glfw: terminate, clearing all previously allocated GLFW resources. - // ------------------------------------------------------------------ - // glfwTerminate(); - // return 0; + sound.play(); // Loop while GLFW window should stay open. while (!glfwWindowShouldClose(window)) { @@ -235,192 +109,3 @@ int main(int argc, char* argv[]) return 0; } - -// #include -// #include - - -// #include -// #include - -// #include "client/client.hpp" -// #include "shared/utilities/rng.hpp" -// #include "shared/utilities/config.hpp" -// #include "shared/utilities/root_path.hpp" -// #include "client/sound.hpp" - -// #include "shared/utilities/root_path.hpp" -// #include "client/gui/img/img.hpp" -// #include "stb_image.h" - -// void framebuffer_size_callback(GLFWwindow* window, int width, int height); -// void processInput(GLFWwindow *window); - -// // settings -// const unsigned int SCR_WIDTH = 800; -// const unsigned int SCR_HEIGHT = 600; - -// int main() -// { -// // glfw: initialize and configure -// // ------------------------------ -// glfwInit(); -// glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); -// glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); -// glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - -// #ifdef __APPLE__ -// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); -// #endif - -// // glfw window creation -// // -------------------- -// GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); -// if (window == NULL) -// { -// std::cout << "Failed to create GLFW window" << std::endl; -// glfwTerminate(); -// return -1; -// } -// glfwMakeContextCurrent(window); -// glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); - -// // glad: load all OpenGL function pointers -// // --------------------------------------- -// // if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) -// // { -// // std::cout << "Failed to initialize GLAD" << std::endl; -// // return -1; -// // } -// GLenum err = glewInit() ; -// if (GLEW_OK != err) { -// std::cerr << "Error loading GLEW: " << glewGetString(err) << std::endl; -// return false; -// } - -// // build and compile our shader zprogram -// // ------------------------------------ -// // Shader ourShader("/Users/dmin/Documents/Homework/Spring_2024/LearnOpenGL/src/1.getting_started/4.1.textures/4.1.texture.vs", "/Users/dmin/Documents/Homework/Spring_2024/LearnOpenGL/src/1.getting_started/4.1.textures/4.1.texture.fs"); -// auto shaderprog = LoadShaders("/Users/dmin/Documents/Homework/Spring_2024/group3/src/client/shaders/test.vert", "/Users/dmin/Documents/Homework/Spring_2024/group3/src/client/shaders/test.frag"); - -// // set up vertex data (and buffer(s)) and configure vertex attributes -// // ------------------------------------------------------------------ -// float vertices[] = { -// // positions // colors // texture coords -// 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right -// 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right -// -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left -// -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left -// }; -// unsigned int indices[] = { -// 0, 1, 3, // first triangle -// 1, 2, 3 // second triangle -// }; -// unsigned int VBO, VAO, EBO; -// glGenVertexArrays(1, &VAO); -// glGenBuffers(1, &VBO); -// glGenBuffers(1, &EBO); - -// glBindVertexArray(VAO); - -// glBindBuffer(GL_ARRAY_BUFFER, VBO); -// glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - -// glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); -// glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - -// // position attribute -// glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); -// glEnableVertexAttribArray(0); -// // color attribute -// glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); -// glEnableVertexAttribArray(1); -// // texture coord attribute -// glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); -// glEnableVertexAttribArray(2); - - -// // load and create a texture -// // ------------------------- -// unsigned int texture; -// glGenTextures(1, &texture); -// glBindTexture(GL_TEXTURE_2D, texture); // all upcoming GL_TEXTURE_2D operations now have effect on this texture object -// // set the texture wrapping parameters -// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) -// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); -// // set texture filtering parameters -// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); -// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); -// // load image, create texture and generate mipmaps -// int width, height, nrChannels; -// // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path. -// unsigned char *data = stbi_load("/Users/dmin/Documents/Homework/Spring_2024/group3/assets/imgs/awesomeface.png", &width, &height, &nrChannels, 0); -// // std::cout << "data: " << data << std::endl; -// if (data) -// { -// glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); -// glGenerateMipmap(GL_TEXTURE_2D); -// } -// else -// { -// std::cout << "Failed to load texture" << std::endl; -// } -// stbi_image_free(data); - - -// // render loop -// // ----------- -// while (!glfwWindowShouldClose(window)) -// { -// // input -// // ----- -// processInput(window); - -// // render -// // ------ -// glClearColor(0.2f, 0.3f, 0.3f, 1.0f); -// glClear(GL_COLOR_BUFFER_BIT); - -// // bind Texture -// glBindTexture(GL_TEXTURE_2D, texture); - -// // render container -// // ourShader.use(); -// glUseProgram(shaderprog); -// glBindVertexArray(VAO); -// glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - -// // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) -// // ------------------------------------------------------------------------------- -// glfwSwapBuffers(window); -// glfwPollEvents(); -// } - -// // optional: de-allocate all resources once they've outlived their purpose: -// // ------------------------------------------------------------------------ -// glDeleteVertexArrays(1, &VAO); -// glDeleteBuffers(1, &VBO); -// glDeleteBuffers(1, &EBO); - -// // glfw: terminate, clearing all previously allocated GLFW resources. -// // ------------------------------------------------------------------ -// glfwTerminate(); -// return 0; -// } - -// // process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly -// // --------------------------------------------------------------------------------------------------------- -// void processInput(GLFWwindow *window) -// { -// if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) -// glfwSetWindowShouldClose(window, true); -// } - -// // glfw: whenever the window size changed (by OS or user resize) this callback function executes -// // --------------------------------------------------------------------------------------------- -// void framebuffer_size_callback(GLFWwindow* window, int width, int height) -// { -// // make sure the viewport matches the new window dimensions; note that width and -// // height will be significantly larger than specified on retina displays. -// glViewport(0, 0, width, height); -// } From 874587118461a365c476011787c06c425197f40b Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 5 May 2024 22:16:09 -0700 Subject: [PATCH 70/92] messing around with stuff --- include/client/gui/font/loader.hpp | 2 +- src/client/gui/font/font.cpp | 1 - src/client/gui/gui.cpp | 11 +++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp index b991d823..60f76dae 100644 --- a/include/client/gui/font/loader.hpp +++ b/include/client/gui/font/loader.hpp @@ -55,7 +55,7 @@ class Loader { bool _loadFont(Font font); std::unordered_map< - Font, + std::pair, std::unordered_map > font_map; }; diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index 45bc7377..e362f02b 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -14,7 +14,6 @@ float getFontSizePx(Size size) { float getScaleFactor(Size size) { float screen_factor = WINDOW_WIDTH / UNIT_WINDOW_WIDTH; - std::cout << SIZE_TO_SCALE.at(size) * screen_factor << "\n"; return SIZE_TO_SCALE.at(size) * screen_factor; } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index ec31aaf8..c4d26449 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -33,7 +33,6 @@ bool GUI::init(GLuint text_shader, GLuint image_shader) // Need to register all of the necessary shaders for each widget widget::DynText::shader = text_shader; widget::StaticImg::shader = image_shader; - glUniformMatrix4fv(glGetUniformLocation(widget::DynText::shader, "projection"), 1, false, reinterpret_cast(&projection)); std::cout << "Initialized GUI\n"; return true; @@ -177,11 +176,11 @@ void GUI::_layoutTitleScreen() { start_flex->push(std::move(start_text)); this->addWidget(std::move(start_flex)); - auto pikachu = widget::StaticImg::make( - glm::vec2(0.0f, 0.0f), - images.getImg(img::ImgID::Pikachu) - ); - this->addWidget(std::move(pikachu)); + // auto pikachu = widget::StaticImg::make( + // glm::vec2(0.0f, 0.0f), + // images.getImg(img::ImgID::Pikachu) + // ); + // this->addWidget(std::move(pikachu)); } void GUI::_layoutLobbyBrowser() { From ff8d4baafd920b24b7a6736c899a2958bd92c504 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 5 May 2024 22:32:27 -0700 Subject: [PATCH 71/92] more fiddling around... --- include/client/gui/font/font.hpp | 2 +- include/client/gui/font/loader.hpp | 13 ++++- src/client/gui/font/font.cpp | 2 +- src/client/gui/font/loader.cpp | 85 ++++++++++++++++------------- src/client/gui/widget/dyntext.cpp | 12 ++-- src/client/gui/widget/staticimg.cpp | 2 +- 6 files changed, 66 insertions(+), 50 deletions(-) diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index 0699415a..34e80e39 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -54,7 +54,7 @@ const std::unordered_map SIZE_TO_SCALE = { {Size::XLARGE, 2.0f}, }; -float getFontSizePx(Size size); +int getFontSizePx(Size size); float getScaleFactor(Size size); diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp index 60f76dae..b3d1d84d 100644 --- a/include/client/gui/font/loader.hpp +++ b/include/client/gui/font/loader.hpp @@ -23,6 +23,14 @@ struct Character { unsigned int advance; /// offset to advance to next glyph }; +/** + * Hash function so we can provide a pair of Font and FontSize and get the char + * mappings for that font. + */ +struct font_pair_hash { + std::size_t operator()(const std::pair& p) const; +}; + /** * Handles loading all of the fonts we want to use, and provides an interface to get * the Character information (e.g. opengl texture and sizing information). @@ -44,7 +52,7 @@ class Loader { * @param font Abstract font to use. * @returns the Character information for that glyph */ - [[nodiscard]] const Character& loadChar(char c, Font font) const; + [[nodiscard]] const Character& loadChar(char c, Font font, Size size) const; private: FT_Library ft; @@ -56,7 +64,8 @@ class Loader { std::unordered_map< std::pair, - std::unordered_map + std::unordered_map, + font_pair_hash > font_map; }; diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index e362f02b..4c238c24 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -7,7 +7,7 @@ namespace gui::font { -float getFontSizePx(Size size) { +int getFontSizePx(Size size) { return UNIT_LARGE_SIZE_PX * getScaleFactor(size); } diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 67614bf9..11e6e1ce 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -10,6 +10,11 @@ namespace gui::font { +std::size_t font_pair_hash::operator()(const std::pair& p) const { + // idk if this is actually doing what I think it is doing + return (static_cast(p.first) << 32 ^ static_cast(p.second)); +} + bool Loader::init() { if (FT_Init_FreeType(&this->ft)) { std::cerr << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; @@ -32,8 +37,8 @@ bool Loader::init() { return true; } -const Character& Loader::loadChar(char c, Font font) const { - auto char_map = this->font_map.at(font); +const Character& Loader::loadChar(char c, Font font, Size size) const { + auto char_map = this->font_map.at({font, size}); if (!char_map.contains(c)) { return char_map.at('?'); @@ -53,47 +58,49 @@ bool Loader::_loadFont(Font font) { return false; } - FT_Set_Pixel_Sizes(face, 0, UNIT_LARGE_SIZE_PX); - std::unordered_map characters; - for (unsigned char c = 0; c < 128; c++) { - // load character glyph - if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { - std::cerr << "ERROR::FREETYTPE: Failed to load Glyph " << c << std::endl; - return false; + for (auto size : {Size::SMALL, Size::MEDIUM, Size::LARGE, Size::XLARGE}) { + FT_Set_Pixel_Sizes(face, 0, getFontSizePx(size)); + std::unordered_map characters; + for (unsigned char c = 0; c < 128; c++) { + // load character glyph + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { + std::cerr << "ERROR::FREETYTPE: Failed to load Glyph " << c << std::endl; + return false; + } + + // generate texture + unsigned int texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RED, + face->glyph->bitmap.width, + face->glyph->bitmap.rows, + 0, + GL_RED, + GL_UNSIGNED_BYTE, + face->glyph->bitmap.buffer + ); + // set texture options + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // now store character for later use + Character character = { + texture, + glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), + glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), + static_cast(face->glyph->advance.x) + }; + characters.insert({c, character}); } - // generate texture - unsigned int texture; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_RED, - face->glyph->bitmap.width, - face->glyph->bitmap.rows, - 0, - GL_RED, - GL_UNSIGNED_BYTE, - face->glyph->bitmap.buffer - ); - // set texture options - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // now store character for later use - Character character = { - texture, - glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), - glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), - static_cast(face->glyph->advance.x) - }; - characters.insert({c, character}); + this->font_map.insert({{font, size}, characters}); } - this->font_map.insert({font, characters}); - FT_Done_Face(face); return true; diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 7b400474..d1169e2d 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -31,13 +31,13 @@ DynText::DynText(glm::vec2 origin, std::string text, this->width = 0; this->height = 0; - float scale = font::getScaleFactor(options.size); + float scale = 1.0f; for (int i = 0; i < text.size(); i++) { - font::Character ch = this->fonts->loadChar(this->text[i], this->options.font); + font::Character ch = this->fonts->loadChar(this->text[i], this->options.font, this->options.size); this->height = std::max(this->height, static_cast(ch.size.y * scale)); if (i != text.size() - 1 && i != 0) { - this->width += (ch.advance >> 6) * scale; + this->width += (ch.advance / 64) * scale; } else { this->width += ch.size.x * scale; } @@ -52,7 +52,7 @@ void DynText::render() { glUseProgram(DynText::shader); - glActiveTexture(GL_TEXTURE0); + // glActiveTexture(GL_TEXTURE0); glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); auto color = font::getRGB(this->options.color); glUniform3f(glGetUniformLocation(DynText::shader, "textColor"), color.x, color.y, color.z); @@ -64,9 +64,9 @@ void DynText::render() { // iterate through all characters for (const char& c : this->text) { - font::Character ch = this->fonts->loadChar(c, this->options.font); + font::Character ch = this->fonts->loadChar(c, this->options.font, this->options.size); - float scale = font::getScaleFactor(this->options.size); + float scale = 1.0f; float xpos = x + ch.bearing.x * scale; float ypos = y - (ch.size.y - ch.bearing.y) * scale; diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 7628757a..29419bfe 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -99,7 +99,7 @@ void StaticImg::render() { // glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "model"), 1, false, reinterpret_cast(&model)); // glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 1.0f, 1.0f, 1.0f); - glActiveTexture(GL_TEXTURE0); + // glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, this->texture_id); glBindVertexArray(quadVAO); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); From 8f80a526025af7e9dd0000dae493328e219320e9 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 5 May 2024 22:50:25 -0700 Subject: [PATCH 72/92] JESUS CHRIST HOW WAS THIS CAUSING THE FLICKERING --- src/client/gui/font/loader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index 11e6e1ce..c415cd93 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -38,7 +38,7 @@ bool Loader::init() { } const Character& Loader::loadChar(char c, Font font, Size size) const { - auto char_map = this->font_map.at({font, size}); + auto& char_map = this->font_map.at({font, size}); if (!char_map.contains(c)) { return char_map.at('?'); From 670b6e970e9f6af172f192740494a35cbafe4117 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Sun, 5 May 2024 22:56:36 -0700 Subject: [PATCH 73/92] switch font render to not load in separate textures for different sizes --- include/client/gui/font/loader.hpp | 15 ++---- src/client/gui/font/loader.cpp | 85 ++++++++++++++---------------- src/client/gui/widget/dyntext.cpp | 10 ++-- 3 files changed, 47 insertions(+), 63 deletions(-) diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp index b3d1d84d..b991d823 100644 --- a/include/client/gui/font/loader.hpp +++ b/include/client/gui/font/loader.hpp @@ -23,14 +23,6 @@ struct Character { unsigned int advance; /// offset to advance to next glyph }; -/** - * Hash function so we can provide a pair of Font and FontSize and get the char - * mappings for that font. - */ -struct font_pair_hash { - std::size_t operator()(const std::pair& p) const; -}; - /** * Handles loading all of the fonts we want to use, and provides an interface to get * the Character information (e.g. opengl texture and sizing information). @@ -52,7 +44,7 @@ class Loader { * @param font Abstract font to use. * @returns the Character information for that glyph */ - [[nodiscard]] const Character& loadChar(char c, Font font, Size size) const; + [[nodiscard]] const Character& loadChar(char c, Font font) const; private: FT_Library ft; @@ -63,9 +55,8 @@ class Loader { bool _loadFont(Font font); std::unordered_map< - std::pair, - std::unordered_map, - font_pair_hash + Font, + std::unordered_map > font_map; }; diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp index c415cd93..daa43b9c 100644 --- a/src/client/gui/font/loader.cpp +++ b/src/client/gui/font/loader.cpp @@ -10,11 +10,6 @@ namespace gui::font { -std::size_t font_pair_hash::operator()(const std::pair& p) const { - // idk if this is actually doing what I think it is doing - return (static_cast(p.first) << 32 ^ static_cast(p.second)); -} - bool Loader::init() { if (FT_Init_FreeType(&this->ft)) { std::cerr << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; @@ -37,8 +32,8 @@ bool Loader::init() { return true; } -const Character& Loader::loadChar(char c, Font font, Size size) const { - auto& char_map = this->font_map.at({font, size}); +const Character& Loader::loadChar(char c, Font font) const { + auto& char_map = this->font_map.at(font); if (!char_map.contains(c)) { return char_map.at('?'); @@ -58,49 +53,47 @@ bool Loader::_loadFont(Font font) { return false; } - for (auto size : {Size::SMALL, Size::MEDIUM, Size::LARGE, Size::XLARGE}) { - FT_Set_Pixel_Sizes(face, 0, getFontSizePx(size)); - std::unordered_map characters; - for (unsigned char c = 0; c < 128; c++) { - // load character glyph - if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { - std::cerr << "ERROR::FREETYTPE: Failed to load Glyph " << c << std::endl; - return false; - } - - // generate texture - unsigned int texture; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_RED, - face->glyph->bitmap.width, - face->glyph->bitmap.rows, - 0, - GL_RED, - GL_UNSIGNED_BYTE, - face->glyph->bitmap.buffer - ); - // set texture options - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // now store character for later use - Character character = { - texture, - glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), - glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), - static_cast(face->glyph->advance.x) - }; - characters.insert({c, character}); + FT_Set_Pixel_Sizes(face, 0, UNIT_LARGE_SIZE_PX); + std::unordered_map characters; + for (unsigned char c = 0; c < 128; c++) { + // load character glyph + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { + std::cerr << "ERROR::FREETYTPE: Failed to load Glyph " << c << std::endl; + return false; } - this->font_map.insert({{font, size}, characters}); + // generate texture + unsigned int texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RED, + face->glyph->bitmap.width, + face->glyph->bitmap.rows, + 0, + GL_RED, + GL_UNSIGNED_BYTE, + face->glyph->bitmap.buffer + ); + // set texture options + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // now store character for later use + Character character = { + texture, + glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), + glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), + static_cast(face->glyph->advance.x) + }; + characters.insert({c, character}); } + this->font_map.insert({font, characters}); + FT_Done_Face(face); return true; diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index d1169e2d..70e6ea01 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -31,13 +31,13 @@ DynText::DynText(glm::vec2 origin, std::string text, this->width = 0; this->height = 0; - float scale = 1.0f; + float scale = font::getScaleFactor(this->options.size); for (int i = 0; i < text.size(); i++) { - font::Character ch = this->fonts->loadChar(this->text[i], this->options.font, this->options.size); + font::Character ch = this->fonts->loadChar(this->text[i], this->options.font); this->height = std::max(this->height, static_cast(ch.size.y * scale)); if (i != text.size() - 1 && i != 0) { - this->width += (ch.advance / 64) * scale; + this->width += (ch.advance >> 6) * scale; } else { this->width += ch.size.x * scale; } @@ -64,9 +64,9 @@ void DynText::render() { // iterate through all characters for (const char& c : this->text) { - font::Character ch = this->fonts->loadChar(c, this->options.font, this->options.size); + font::Character ch = this->fonts->loadChar(c, this->options.font); - float scale = 1.0f; + float scale = font::getScaleFactor(this->options.size); float xpos = x + ch.bearing.x * scale; float ypos = y - (ch.size.y - ch.bearing.y) * scale; From 54d21bad3c8cdbac3165da7ae433dcdcb37763fd Mon Sep 17 00:00:00 2001 From: David Min Date: Mon, 6 May 2024 17:26:02 -0700 Subject: [PATCH 74/92] Removed problematic call to glUniform in StaticImg::render() --- src/client/gui/widget/staticimg.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 29419bfe..1de0df63 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -87,7 +87,6 @@ StaticImg::~StaticImg() { void StaticImg::render() { glUseProgram(StaticImg::shader); - glUniform1i(glGetUniformLocation(StaticImg::shader, "texture1"), this->texture_id); glm::mat4 model = glm::mat4(1.0f); // model = glm::translate(model, glm::vec3(origin, 0.0f)); From 70a9903314b61d5fe9a22f8e1dcbfe0252c8a2c6 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Tue, 7 May 2024 11:11:57 -0700 Subject: [PATCH 75/92] add player to camera pos also mess with model shading a bit more --- include/client/client.hpp | 1 + include/client/lightsource.hpp | 2 +- src/client/client.cpp | 33 ++++++++++++++++++++++++++++++--- src/client/lightsource.cpp | 27 +++------------------------ src/client/shaders/model.frag | 8 +++++--- src/server/server.cpp | 2 +- 6 files changed, 41 insertions(+), 32 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index 462bc5ea..306329d4 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -62,6 +62,7 @@ class Client { std::shared_ptr model_shader; std::shared_ptr light_source_shader; + std::unique_ptr player_model; std::unique_ptr bear_model; std::unique_ptr light_source; diff --git a/include/client/lightsource.hpp b/include/client/lightsource.hpp index bdacab5e..a7256107 100644 --- a/include/client/lightsource.hpp +++ b/include/client/lightsource.hpp @@ -16,7 +16,7 @@ class LightSource { public: LightSource(); - void draw(std::shared_ptr shader); + void draw(std::shared_ptr shader, glm::mat4 viewProj); void TranslateTo(const glm::vec3& new_pos); glm::vec3 lightPos; diff --git a/src/client/client.cpp b/src/client/client.cpp index 0ec27f92..50870424 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -126,7 +126,12 @@ bool Client::init() { this->bear_model = std::make_unique(bear_model_path.string()); this->bear_model->scale(0.25); + boost::filesystem::path player_model_path = graphics_assets_dir / "Fire-testing.obj"; + this->player_model = std::make_unique(player_model_path.string()); + this->player_model->scale(0.25); + this->light_source = std::make_unique(); + boost::filesystem::path lightVertFilepath = this->root_path / "src/client/shaders/lightsource.vert"; boost::filesystem::path lightFragFilepath = this->root_path / "src/client/shaders/lightsource.frag"; this->light_source_shader = std::make_shared(lightVertFilepath.string(), lightFragFilepath.string()); @@ -248,21 +253,43 @@ void Client::draw() { // Get camera position from server, update position and don't render player object (or special handling) if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { cam->updatePos(sharedObject->physics.position); - this->light_source->TranslateTo(cam->getPos()); - this->light_source->draw(this->light_source_shader); + auto lightPos = glm::vec3(-5.0f, 0.0f, 0.0f); + auto player_pos = glm::vec3(this->cam->getPos().x, this->cam->getPos().y - 20.0f, this->cam->getPos().z); + this->player_model->translateAbsolute(sharedObject->physics.position); + this->player_model->draw( + this->model_shader, + this->cam->getViewProj(), + player_pos, + lightPos, + true); continue; } switch (sharedObject->type) { case ObjectType::Enemy: { // warren bear is an enemy because why not + // auto pos = glm::vec3(0.0f, 0.0f, 0.0f); + auto lightPos = glm::vec3(-5.0f, 0.0f, 0.0f); this->bear_model->translateAbsolute(sharedObject->physics.position); this->bear_model->draw( this->model_shader, this->cam->getViewProj(), this->cam->getPos(), - this->cam->getPos(), + lightPos, true); + + this->light_source->TranslateTo(lightPos); + this->light_source->draw( + this->light_source_shader, + this->cam->getViewProj()); + + // Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f)); + // cube->translateAbsolute(lightPos); + // cube->draw(this->cube_shader, + // this->cam->getViewProj(), + // this->cam->getPos(), + // glm::vec3(), + // false); break; } case ObjectType::SolidSurface: { diff --git a/src/client/lightsource.cpp b/src/client/lightsource.cpp index 7129aaf5..363959d8 100644 --- a/src/client/lightsource.cpp +++ b/src/client/lightsource.cpp @@ -16,33 +16,12 @@ LightSource::LightSource() : model(1.0f) { glEnableVertexAttribArray(0); } -void LightSource::draw(std::shared_ptr shader) { +void LightSource::draw(std::shared_ptr shader, + glm::mat4 viewProj) { shader->use(); - // Currently 'hardcoding' camera logic in - float FOV = 45.0f; - float Aspect = 1.33f; - float NearClip = 0.1f; - float FarClip = 100.0f; - - float Distance = 10.0f; - float Azimuth = 0.0f; - float Incline = 20.0f; - - glm::mat4 world(1); - world[3][2] = Distance; - world = glm::eulerAngleY(glm::radians(-Azimuth)) * glm::eulerAngleX(glm::radians(-Incline)) * world; - - // Compute view matrix (inverse of world matrix) - glm::mat4 view = glm::inverse(world); - - // Compute perspective projection matrix - glm::mat4 project = glm::perspective(glm::radians(FOV), Aspect, NearClip, FarClip); - - // Compute final view-projection matrix - glm::mat4 viewProjMtx = project * view; // get the locations and send the uniforms to the shader - shader->setMat4("viewProj", viewProjMtx); + shader->setMat4("viewProj", viewProj); shader->setMat4("model", model); // draw the light cube object diff --git a/src/client/shaders/model.frag b/src/client/shaders/model.frag index 18572321..e08d85d3 100644 --- a/src/client/shaders/model.frag +++ b/src/client/shaders/model.frag @@ -26,13 +26,13 @@ out vec4 fragColor; void main() { // ambient - vec3 ambient = lightColor * material.ambient; + vec3 ambient = lightColor * vec3(texture(material.texture_diffuse1, TexCoords)); // diffuse vec3 norm = normalize(fragNormal); vec3 lightDir = normalize(lightPos - fragPos); float diff = max(dot(norm, lightDir), 0.0); - vec3 diffuse = lightColor * (diff * material.diffuse); + vec3 diffuse = lightColor * (diff * vec3(texture(material.texture_diffuse1, TexCoords))); // specular vec3 viewDir = normalize(viewPos - fragPos); @@ -41,5 +41,7 @@ void main() { vec3 specular = lightColor * (spec * material.specular); vec3 result = ambient + diffuse + specular; - fragColor = vec4(result, 1.0) * vec4(texture(material.texture_diffuse1, TexCoords)); + fragColor = vec4(result, 1.0); + // * vec4(texture(material.texture_diffuse1, TexCoords)); + // fragColor = vec4(0.0, 1.0, 1.0, 1.0); } diff --git a/src/server/server.cpp b/src/server/server.cpp index 5e3a9df2..204faa10 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -39,7 +39,7 @@ Server::Server(boost::asio::io_context& io_context, GameConfig config) EntityID bearID = state.objects.createObject(ObjectType::Enemy); Enemy* bear = reinterpret_cast(state.objects.getObject(bearID)); - bear->physics.shared.position = glm::vec3(0.0f, -1.3f, 0.0f); + bear->physics.shared.position = glm::vec3(0.0f, 50.0f, 0.0f); // Create a room EntityID wall1ID = state.objects.createObject(ObjectType::SolidSurface); From ca99983c39cb4ddda8920051ee42f18931bbe716 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Tue, 7 May 2024 11:32:17 -0700 Subject: [PATCH 76/92] windows fixes --- src/client/client.cpp | 4 ++-- src/client/model.cpp | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index 50870424..45835ee9 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -278,10 +278,10 @@ void Client::draw() { lightPos, true); - this->light_source->TranslateTo(lightPos); + /* this->light_source->TranslateTo(lightPos); this->light_source->draw( this->light_source_shader, - this->cam->getViewProj()); + this->cam->getViewProj());*/ // Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f)); // cube->translateAbsolute(lightPos); diff --git a/src/client/model.cpp b/src/client/model.cpp index a20c44d4..38f7200c 100644 --- a/src/client/model.cpp +++ b/src/client/model.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "assimp/material.h" #include "assimp/types.h" @@ -129,8 +130,8 @@ void Mesh::draw( glUseProgram(0); } -Model::Model(const std::string& filepath) : - directory(filepath.substr(0, filepath.find_last_of('/'))) { +Model::Model(const std::string& filepath) { + this->directory = std::filesystem::path(filepath).parent_path().string(); Assimp::Importer importer; const aiScene *scene = importer.ReadFile(filepath, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_SplitLargeMeshes | aiProcess_OptimizeMeshes); @@ -277,8 +278,8 @@ std::vector Model::loadMaterialTextures(aiMaterial* mat, const aiTextur for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); - std::string textureFilepath = this->directory + "/" + std::string(str.C_Str()); - Texture texture(textureFilepath, type); + std::filesystem::path textureFilepath = std::filesystem::path(this->directory) / std::string(str.C_Str()); + Texture texture(textureFilepath.string(), type); textures.push_back(texture); } return textures; @@ -306,9 +307,10 @@ Texture::Texture(const std::string& filepath, const aiTextureType& type) { if (!data) { std::cout << "Texture failed to load at path: " << filepath << std::endl; stbi_image_free(data); + throw std::exception(); } std::cout << "Succesfully loaded texture at " << filepath << std::endl; - GLenum format; + GLenum format = GL_RED; if (nrComponents == 1) format = GL_RED; else if (nrComponents == 3) From 1d6d90c63dbddbce0934572613ce5fec1cf1ee64 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 11:42:12 -0700 Subject: [PATCH 77/92] working variable screen size with config --- config.json | 3 ++- include/client/client.hpp | 15 ++++++++++----- include/client/constants.hpp | 3 +++ include/client/gui/gui.hpp | 3 +++ include/shared/utilities/config.hpp | 1 + src/client/client.cpp | 16 ++++++++++++++-- src/client/gui/font/font.cpp | 2 +- src/client/gui/gui.cpp | 11 ++--------- src/client/gui/widget/dyntext.cpp | 3 ++- src/client/gui/widget/staticimg.cpp | 8 -------- src/client/main.cpp | 2 +- src/shared/utilities/config.cpp | 3 ++- 12 files changed, 41 insertions(+), 29 deletions(-) diff --git a/config.json b/config.json index 9d33b6fc..41241d87 100644 --- a/config.json +++ b/config.json @@ -13,6 +13,7 @@ }, "client": { "default_name": "John Doe", - "lobby_discovery": true + "lobby_discovery": true, + "window_width": 1500 } } \ No newline at end of file diff --git a/include/client/client.hpp b/include/client/client.hpp index bf869cba..8d3fa387 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -24,14 +24,14 @@ #include "shared/network/session.hpp" #include "shared/utilities/config.hpp" -#define WINDOW_WIDTH 1500 -#define WINDOW_HEIGHT 1000 +#define WINDOW_WIDTH Client::getWindowSize().x +#define WINDOW_HEIGHT Client::getWindowSize().y // position something a "frac" of the way across the screen // e.g. WIDTH_FRAC(1, 4) -> a fourth of the way from the left // HEIGHT_FRAC(2, 3) -> two thirds of the way from the bottom -#define FRAC_WINDOW_WIDTH(num, denom) WINDOW_WIDTH * static_cast(num) / static_cast(denom) -#define FRAC_WINDOW_HEIGHT(num, denom) WINDOW_HEIGHT * static_cast(num) / static_cast(denom) +#define FRAC_WINDOW_WIDTH(num, denom) Client::window_width * static_cast(num) / static_cast(denom) +#define FRAC_WINDOW_HEIGHT(num, denom) Client::window_height * static_cast(num) / static_cast(denom) using namespace boost::asio::ip; @@ -49,8 +49,10 @@ class Client { static void mouseCallback(GLFWwindow* window, double xposIn, double yposIn); static void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods); static void charCallback(GLFWwindow* window, unsigned int codepoint); - static time_t getTimeOfLastKeystroke(); + static glm::vec2 getWindowSize(); + + static time_t getTimeOfLastKeystroke(); // Getter / Setters GLFWwindow* getWindow() { return window; } @@ -98,6 +100,9 @@ class Client { static float mouse_xpos; static float mouse_ypos; + static int window_width; + static int window_height; + static time_t time_of_last_keystroke; GameConfig config; diff --git a/include/client/constants.hpp b/include/client/constants.hpp index b0215aea..c51064ea 100644 --- a/include/client/constants.hpp +++ b/include/client/constants.hpp @@ -1,5 +1,8 @@ #pragma once +#define MIN_WINDOW_WIDTH 900 +#define MIN_WINDOW_HEIGHT 600 + // The screen width that's defined as 1:1, from which other // resolutions can calculate pixel sizes #define UNIT_WINDOW_WIDTH 1500 diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index cc8baec2..cc9584a8 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -47,6 +47,8 @@ enum class GUIState { GAME_ESC_MENU }; +#define GUI_PROJECTION_MATRIX() glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); + /** * Class which wraps around all of the GUI elements that should be rendered to the screen. * @@ -60,6 +62,7 @@ class GUI { public: static glm::mat4 projection; + /// ========================================================================= /// /// These are the functions that need to be called to setup a GUI object. Doing anything diff --git a/include/shared/utilities/config.hpp b/include/shared/utilities/config.hpp index 11ac821c..a26efb74 100644 --- a/include/shared/utilities/config.hpp +++ b/include/shared/utilities/config.hpp @@ -37,6 +37,7 @@ struct GameConfig { std::string default_name; /// @brief Whether or not the client should listen for server lobby broadcasts bool lobby_discovery; + int window_width; } client; /** diff --git a/src/client/client.cpp b/src/client/client.cpp index f7cada38..98620d94 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -13,8 +13,10 @@ #include #include "client/gui/gui.hpp" +#include "client/constants.hpp" #include + #include "client/shader.hpp" #include "shared/game/event.hpp" #include "shared/network/constants.hpp" @@ -48,6 +50,9 @@ bool Client::is_left_mouse_down = false; float Client::mouse_xpos = 0.0f; float Client::mouse_ypos = 0.0f; +int Client::window_width = UNIT_WINDOW_WIDTH; +int Client::window_height = UNIT_WINDOW_HEIGHT; + time_t Client::time_of_last_keystroke = 0; using namespace gui; @@ -63,6 +68,8 @@ Client::Client(boost::asio::io_context& io_context, GameConfig config): lobby_finder(io_context, config) { cam = new Camera(); + Client::window_width = config.client.window_width; + Client::window_height = static_cast((config.client.window_width * 2.0f) / 3.0f); if (config.client.lobby_discovery) { lobby_finder.startSearching(); @@ -102,13 +109,14 @@ bool Client::init() { /* Create a windowed mode window and its OpenGL context */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Arcana", NULL, NULL); + window = glfwCreateWindow(Client::window_width, Client::window_height, "Arcana", NULL, NULL); if (!window) { glfwTerminate(); return false; } - glfwSetWindowSizeLimits(window, WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT); + glfwSetWindowSizeLimits(window, + Client::window_width, Client::window_height, Client::window_width, Client::window_height); /* Make the window's context current */ glfwMakeContextCurrent(window); @@ -460,6 +468,10 @@ void Client::charCallback(GLFWwindow* window, unsigned int codepoint) { Client::time_of_last_keystroke = getMsSinceEpoch(); } +glm::vec2 Client::getWindowSize() { + return glm::vec2(Client::window_width, Client::window_height); +} + time_t Client::getTimeOfLastKeystroke() { return Client::time_of_last_keystroke; } \ No newline at end of file diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index 4c238c24..ec12946f 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -12,7 +12,7 @@ int getFontSizePx(Size size) { } float getScaleFactor(Size size) { - float screen_factor = WINDOW_WIDTH / UNIT_WINDOW_WIDTH; + float screen_factor = static_cast(WINDOW_WIDTH) / static_cast(UNIT_WINDOW_WIDTH); return SIZE_TO_SCALE.at(size) * screen_factor; } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index c4d26449..7f62819e 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -9,7 +9,6 @@ namespace gui { -glm::mat4 GUI::projection = glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); GUI::GUI(Client* client) { this->client = client; @@ -175,12 +174,6 @@ void GUI::_layoutTitleScreen() { start_flex->push(std::move(start_text)); this->addWidget(std::move(start_flex)); - - // auto pikachu = widget::StaticImg::make( - // glm::vec2(0.0f, 0.0f), - // images.getImg(img::ImgID::Pikachu) - // ); - // this->addWidget(std::move(pikachu)); } void GUI::_layoutLobbyBrowser() { @@ -221,7 +214,7 @@ void GUI::_layoutLobbyBrowser() { lobbies_flex->push(widget::DynText::make( "No lobbies found...", this->fonts, - widget::DynText::Options(font::Font::MENU, font::Size::SMALL, font::Color::BLACK) + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK) )); } @@ -232,7 +225,7 @@ void GUI::_layoutLobbyBrowser() { "Enter a name", this, fonts, - widget::DynText::Options(font::Font::TEXT, font::Size::SMALL, font::Color::BLACK) + widget::DynText::Options(font::Font::TEXT, font::Size::MEDIUM, font::Color::BLACK) )); } diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 70e6ea01..342a405f 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -53,7 +53,8 @@ void DynText::render() { glUseProgram(DynText::shader); // glActiveTexture(GL_TEXTURE0); - glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); + auto projection = GUI_PROJECTION_MATRIX(); + glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&projection)); auto color = font::getRGB(this->options.color); glUniform3f(glGetUniformLocation(DynText::shader, "textColor"), color.x, color.y, color.z); glBindVertexArray(VAO); diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 1de0df63..18b589f9 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -89,14 +89,6 @@ void StaticImg::render() { glUseProgram(StaticImg::shader); glm::mat4 model = glm::mat4(1.0f); - // model = glm::translate(model, glm::vec3(origin, 0.0f)); - // model = glm::translate(model, glm::vec3(0.5*width, 0.5*height, 0.0)); - // model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0, 0.0, 1.0)); - // model = glm::translate(model, glm::vec3(-0.5*width, -0.5*height, 0.0)); - // model = glm::scale(model, glm::vec3(glm::vec2(width, height), 1.0f)); - // glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "projection"), 1, false, reinterpret_cast(&GUI::projection)); - // glUniformMatrix4fv(glGetUniformLocation(StaticImg::shader, "model"), 1, false, reinterpret_cast(&model)); - // glUniform3f(glGetUniformLocation(StaticImg::shader, "spriteColor"), 1.0f, 1.0f, 1.0f); // glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, this->texture_id); diff --git a/src/client/main.cpp b/src/client/main.cpp index 46b5e91f..8077e6f1 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -27,7 +27,7 @@ void set_callbacks(GLFWwindow* window, Client* client) { glfwSetErrorCallback(error_callback); // Set the window resize callback. - // glfwSetWindowSizeCallback(window, client.resizeCallback); + // glfwSetWindowSizeCallback(window, Client::windowResizeCallback); // Set the key callback. glfwSetKeyCallback(window, Client::keyCallback); diff --git a/src/shared/utilities/config.cpp b/src/shared/utilities/config.cpp index 6a4534a6..e930f319 100644 --- a/src/shared/utilities/config.cpp +++ b/src/shared/utilities/config.cpp @@ -50,7 +50,8 @@ GameConfig GameConfig::parse(int argc, char** argv) { // cppcheck-suppress const }, .client = { .default_name = json.at("client").at("default_name"), - .lobby_discovery = json.at("client").at("lobby_discovery") + .lobby_discovery = json.at("client").at("lobby_discovery"), + .window_width = json.at("client").at("window_width") } }; } catch (nlohmann::json::exception& ex) { From b502ae9bfbc3dcf45acf5beefd8c44366c841b60 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 11:52:32 -0700 Subject: [PATCH 78/92] refactor gui shader loading --- include/client/gui/gui.hpp | 8 +------- src/client/client.cpp | 17 +---------------- src/client/gui/gui.cpp | 16 ++++++++++++++-- src/client/shaders/img.frag | 15 +++++++-------- src/client/shaders/img.vert | 18 ++++++++---------- src/client/shaders/test.frag | 13 ------------- src/client/shaders/test.vert | 14 -------------- 7 files changed, 31 insertions(+), 70 deletions(-) delete mode 100644 src/client/shaders/test.frag delete mode 100644 src/client/shaders/test.vert diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index cc9584a8..18436b01 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -21,9 +21,6 @@ class Client; /* - 1. Reduce number of textures being loaded by fonts - only load one font size + using scale factor - potentially also only load necessary ASCII characters, not random @$^#&*((*))*&*(^&) 2. move shader loading inside of gui.init() 3. dont force the user to pass in img / font loaders into widgets 4. allow semi dynamic screen size scaling, with static 3:2 aspect ratio @@ -83,11 +80,8 @@ class GUI { * @brief Initializes all of the necessary file loading for all of the GUI elements, and * registers all of the static shader variables for each of the derived widget classes * that need a shader. - * - * @param text_shader Shader to use for text rendering - * @param img_shader Shader to use for rendering images */ - bool init(GLuint text_shader, GLuint img_shader); + bool init(); /// ================================================================================ /// ===================================================================== diff --git a/src/client/client.cpp b/src/client/client.cpp index 98620d94..b827da13 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -131,25 +131,10 @@ bool Client::init() { std::cout << "shader version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl; std::cout << "shader version: " << glGetString(GL_VERSION) << std::endl; - /* Load shader programs */ - std::cout << "loading shader" << std::endl; - auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; - auto textShaderProgram = LoadShaders((shader_path / "text.vert").string(), (shader_path / "text.frag").string()); - - if (!textShaderProgram) { - std::cerr << "Failed to initialize text shader program" << std::endl; - return false; - } - - auto imgShaderProgram = LoadShaders((shader_path / "test.vert").string(), (shader_path / "test.frag").string()); - if (!imgShaderProgram) { - std::cerr << "Failed to initialize img shader program" << std::endl; - return false; - } // Init GUI (e.g. load in all fonts) // TODO: pass in shader for image loading in second param - if (!this->gui.init(textShaderProgram, imgShaderProgram)) { + if (!this->gui.init()) { std::cerr << "GUI failed to init" << std::endl; return false; } diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 7f62819e..616d5fb2 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -14,10 +14,22 @@ GUI::GUI(Client* client) { this->client = client; } -bool GUI::init(GLuint text_shader, GLuint image_shader) +bool GUI::init() { std::cout << "Initializing GUI...\n"; + auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; + auto text_shader = LoadShaders((shader_path / "text.vert").string(), (shader_path / "text.frag").string()); + if (!text_shader) { + std::cerr << "Failed to initialize text shader program" << std::endl; + return false; + } + auto image_shader = LoadShaders((shader_path / "img.vert").string(), (shader_path / "img.frag").string()); + if (!image_shader) { + std::cerr << "Failed to initialize img shader program" << std::endl; + return false; + } + this->fonts = std::make_shared(); this->capture_keystrokes = false; @@ -197,7 +209,7 @@ void GUI::_layoutLobbyBrowser() { ss << packet.lobby_name << " " << packet.slots_taken << "/" << packet.slots_avail + packet.slots_taken; auto entry = widget::DynText::make(ss.str(), this->fonts, - widget::DynText::Options(font::Font::MENU, font::Size::SMALL, font::Color::BLACK)); + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK)); entry->addOnClick([ip, this](widget::Handle handle){ std::cout << "Connecting to " << ip.address() << " ...\n"; this->client->connectAndListen(ip.address().to_string()); diff --git a/src/client/shaders/img.frag b/src/client/shaders/img.frag index 0e7beb86..d7bb3647 100644 --- a/src/client/shaders/img.frag +++ b/src/client/shaders/img.frag @@ -1,14 +1,13 @@ #version 330 core -in vec2 TexCoords -out vec4 color +out vec4 FragColor; -// Reference: The chapter on sprite rendering from -// https://learnopengl.com/book/book_pdf.pdf +in vec3 ourColor; +in vec2 TexCoord; -uniform sampler2D image; -uniform vec3 spriteColor; +// texture sampler +uniform sampler2D texture1; void main() { - color = texture(image, TexCoords); -} + FragColor = texture(texture1, TexCoord); +} \ No newline at end of file diff --git a/src/client/shaders/img.vert b/src/client/shaders/img.vert index b8a4dac8..4fd66038 100644 --- a/src/client/shaders/img.vert +++ b/src/client/shaders/img.vert @@ -1,16 +1,14 @@ #version 330 core -layout (location = 0) in vec4 vertex // +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aColor; +layout (location = 2) in vec2 aTexCoord; -// Reference: The chapter on sprite rendering from -// https://learnopengl.com/book/book_pdf.pdf - -out vec2 TexCoords; - -uniform mat4 model; -uniform mat4 projection; +out vec3 ourColor; +out vec2 TexCoord; void main() { - TexCoords = vertex.zw; - gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0); + gl_Position = vec4(aPos, 1.0); + ourColor = aColor; + TexCoord = vec2(aTexCoord.x, aTexCoord.y); } \ No newline at end of file diff --git a/src/client/shaders/test.frag b/src/client/shaders/test.frag deleted file mode 100644 index d7bb3647..00000000 --- a/src/client/shaders/test.frag +++ /dev/null @@ -1,13 +0,0 @@ -#version 330 core -out vec4 FragColor; - -in vec3 ourColor; -in vec2 TexCoord; - -// texture sampler -uniform sampler2D texture1; - -void main() -{ - FragColor = texture(texture1, TexCoord); -} \ No newline at end of file diff --git a/src/client/shaders/test.vert b/src/client/shaders/test.vert deleted file mode 100644 index 4fd66038..00000000 --- a/src/client/shaders/test.vert +++ /dev/null @@ -1,14 +0,0 @@ -#version 330 core -layout (location = 0) in vec3 aPos; -layout (location = 1) in vec3 aColor; -layout (location = 2) in vec2 aTexCoord; - -out vec3 ourColor; -out vec2 TexCoord; - -void main() -{ - gl_Position = vec4(aPos, 1.0); - ourColor = aColor; - TexCoord = vec2(aTexCoord.x, aTexCoord.y); -} \ No newline at end of file From 5cc73458c4204e25a76ee69e3136c68114f41954 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Tue, 7 May 2024 11:53:17 -0700 Subject: [PATCH 79/92] see other players and don't render yourself --- src/client/client.cpp | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index 45835ee9..4953b278 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -253,19 +253,26 @@ void Client::draw() { // Get camera position from server, update position and don't render player object (or special handling) if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { cam->updatePos(sharedObject->physics.position); - auto lightPos = glm::vec3(-5.0f, 0.0f, 0.0f); - auto player_pos = glm::vec3(this->cam->getPos().x, this->cam->getPos().y - 20.0f, this->cam->getPos().z); - this->player_model->translateAbsolute(sharedObject->physics.position); - this->player_model->draw( - this->model_shader, - this->cam->getViewProj(), - player_pos, - lightPos, - true); - continue; + //continue; } switch (sharedObject->type) { + case ObjectType::Player: { + // don't render yourself + if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { + break; + } + auto lightPos = glm::vec3(-5.0f, 0.0f, 0.0f); + auto player_pos = glm::vec3(this->cam->getPos().x, this->cam->getPos().y - 20.0f, this->cam->getPos().z); + this->player_model->translateAbsolute(sharedObject->physics.position); + this->player_model->draw( + this->model_shader, + this->cam->getViewProj(), + player_pos, + lightPos, + true); + break; + } case ObjectType::Enemy: { // warren bear is an enemy because why not // auto pos = glm::vec3(0.0f, 0.0f, 0.0f); From 5c48cbd21ee706c958ea35050e0385cdef0a74be Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 12:04:14 -0700 Subject: [PATCH 80/92] remove old code --- include/client/gui/gui.hpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 18436b01..54e6595c 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -20,14 +20,6 @@ class Client; -/* - 2. move shader loading inside of gui.init() - 3. dont force the user to pass in img / font loaders into widgets - 4. allow semi dynamic screen size scaling, with static 3:2 aspect ratio - 5. Text+Img Widget combined - 6. rename StaticImg -> Sprite[something] -*/ - namespace gui { /** @@ -57,9 +49,6 @@ enum class GUIState { */ class GUI { public: - static glm::mat4 projection; - - /// ========================================================================= /// /// These are the functions that need to be called to setup a GUI object. Doing anything From 8ef42f9d51076778ff8e320fcf43d672433eebc2 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Tue, 7 May 2024 12:24:05 -0700 Subject: [PATCH 81/92] rendering other players works?! --- src/client/client.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index 4953b278..250ef693 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -19,6 +19,7 @@ #include "shared/network/packet.hpp" #include "shared/utilities/config.hpp" + using namespace boost::asio::ip; using namespace std::chrono_literals; @@ -252,23 +253,25 @@ void Client::draw() { // Get camera position from server, update position and don't render player object (or special handling) if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { - cam->updatePos(sharedObject->physics.position); - //continue; } switch (sharedObject->type) { case ObjectType::Player: { // don't render yourself if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { + cam->updatePos(sharedObject->physics.position); break; } auto lightPos = glm::vec3(-5.0f, 0.0f, 0.0f); - auto player_pos = glm::vec3(this->cam->getPos().x, this->cam->getPos().y - 20.0f, this->cam->getPos().z); - this->player_model->translateAbsolute(sharedObject->physics.position); + // subtracting 1 from y position to render players "standing" on ground + auto player_pos = glm::vec3(sharedObject->physics.position.x, sharedObject->physics.position.y - 1.0f, sharedObject->physics.position.z); + + + this->player_model->translateAbsolute(player_pos); this->player_model->draw( this->model_shader, this->cam->getViewProj(), - player_pos, + this->cam->getPos(), lightPos, true); break; From 5e6ef07a459c154a9eb6eccb5b74d3fe46daaa65 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 14:45:08 -0700 Subject: [PATCH 82/92] resolve merge --- include/client/client.hpp | 2 -- include/client/gui/img/loader.hpp | 4 +--- include/client/gui/widget/dyntext.hpp | 3 ++- include/client/gui/widget/staticimg.hpp | 3 ++- src/client/CMakeLists.txt | 5 ++--- src/client/client.cpp | 6 +++--- src/client/gui/CMakeLists.txt | 5 +---- src/client/gui/gui.cpp | 18 ++++++++---------- src/client/gui/widget/dyntext.cpp | 10 +++++----- src/client/gui/widget/staticimg.cpp | 5 +++-- 10 files changed, 27 insertions(+), 34 deletions(-) diff --git a/include/client/client.hpp b/include/client/client.hpp index dae9019c..a9be3df4 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -122,7 +122,5 @@ class Client { /// @brief Generate endpoints the client can connect to basic_resolver_results endpoints; std::shared_ptr session; - - boost::filesystem::path root_path; }; diff --git a/include/client/gui/img/loader.hpp b/include/client/gui/img/loader.hpp index 66875a65..fe85d567 100644 --- a/include/client/gui/img/loader.hpp +++ b/include/client/gui/img/loader.hpp @@ -7,9 +7,7 @@ namespace gui::img { /** - * This class is supposed to load images. - * - * Unfortunately it doesn't work at all! Yipeeee! + * This class loads in all of our images */ class Loader { public: diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index 2b4699c8..52ec9902 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -4,6 +4,7 @@ #include #include "client/core.hpp" +#include "client/shader.hpp" #include "client/gui/widget/widget.hpp" #include "client/gui/font/font.hpp" #include "client/gui/font/loader.hpp" @@ -13,7 +14,7 @@ namespace gui::widget { class DynText : public Widget { public: using Ptr = std::unique_ptr; - static GLuint shader; + static std::unique_ptr shader; struct Options { Options(font::Font font, font::Size size, font::Color color): diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp index 5c6cd47f..2a762726 100644 --- a/include/client/gui/widget/staticimg.hpp +++ b/include/client/gui/widget/staticimg.hpp @@ -3,6 +3,7 @@ #include "client/gui/widget/widget.hpp" #include "client/gui/img/loader.hpp" #include "client/gui/img/img.hpp" +#include "client/shader.hpp" #include @@ -20,7 +21,7 @@ namespace gui::widget { class StaticImg : public Widget { public: using Ptr = std::unique_ptr; - static GLuint shader; + static std::unique_ptr shader; /** * @brief creates a StaticImg unique ptr widget diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 7c296588..6f6a72b1 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -10,7 +10,6 @@ add_subdirectory(../../dependencies/freetype ${CMAKE_BINARY_DIR}/freetype) add_subdirectory(../../dependencies/sfml ${CMAKE_BINARY_DIR}/sfml) add_subdirectory(../../dependencies/assimp ${CMAKE_BINARY_DIR}/assimp) add_subdirectory(../../dependencies/stb ${CMAKE_BINARY_DIR}/stb) -add_subdirectory(../../dependencies/sfml ${CMAKE_BINARY_DIR}/sfml) add_subdirectory(gui) @@ -22,7 +21,7 @@ set(FILES util.cpp lobbyfinder.cpp - shaders.cpp + shader.cpp model.cpp lightsource.cpp renderable.cpp @@ -88,7 +87,7 @@ target_link_libraries(${TARGET_NAME} glfw libglew_static assimp - freetypw + freetype ) target_link_libraries(${TARGET_NAME} PRIVATE sfml-audio) diff --git a/src/client/client.cpp b/src/client/client.cpp index 9cb5baa4..42f20189 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -148,7 +148,7 @@ bool Client::init() { } auto shaders_dir = getRepoRoot() / "src/client/shaders"; - auto graphics_assets_dir = this->root_path / "assets/graphics"; + auto graphics_assets_dir = getRepoRoot() / "assets/graphics"; auto cube_vert_path = shaders_dir / "cube.vert"; auto cube_frag_path = shaders_dir / "cube.frag"; @@ -168,8 +168,8 @@ bool Client::init() { this->light_source = std::make_unique(); - auto lightVertFilepath = this->root_path / "src/client/shaders/lightsource.vert"; - auto lightFragFilepath = this->root_path / "src/client/shaders/lightsource.frag"; + auto lightVertFilepath = shaders_dir / "lightsource.vert"; + auto lightFragFilepath = shaders_dir / "lightsource.frag"; this->light_source_shader = std::make_shared(lightVertFilepath.string(), lightFragFilepath.string()); return true; diff --git a/src/client/gui/CMakeLists.txt b/src/client/gui/CMakeLists.txt index 1211b3a5..33725620 100644 --- a/src/client/gui/CMakeLists.txt +++ b/src/client/gui/CMakeLists.txt @@ -15,8 +15,6 @@ set(FILES widget/staticimg.cpp gui.cpp - - ../../../dependencies/stb/stb_image.cpp ) # OpenGL @@ -24,9 +22,8 @@ set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL REQUIRED) add_library(${LIB_NAME} STATIC ${FILES}) -target_include_directories(${LIB_NAME} PRIVATE ../../../dependencies/stb) # include stb image loading header target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) +target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES} ${STB_INCLUDE_DIRS}) target_link_libraries(${LIB_NAME} PRIVATE game_shared_lib) target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static freetype) diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 616d5fb2..6be8bc94 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -19,14 +19,16 @@ bool GUI::init() std::cout << "Initializing GUI...\n"; auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; - auto text_shader = LoadShaders((shader_path / "text.vert").string(), (shader_path / "text.frag").string()); - if (!text_shader) { - std::cerr << "Failed to initialize text shader program" << std::endl; + + widget::DynText::shader = std::make_unique( + (shader_path / "text.vert").string(), (shader_path / "text.frag").string()); + if (widget::DynText::shader->getID() == 0) { return false; } - auto image_shader = LoadShaders((shader_path / "img.vert").string(), (shader_path / "img.frag").string()); - if (!image_shader) { - std::cerr << "Failed to initialize img shader program" << std::endl; + + widget::StaticImg::shader = std::make_unique( + (shader_path / "img.vert").string(), (shader_path / "img.frag").string()); + if (widget::StaticImg::shader->getID() == 0) { return false; } @@ -41,10 +43,6 @@ bool GUI::init() return false; } - // Need to register all of the necessary shaders for each widget - widget::DynText::shader = text_shader; - widget::StaticImg::shader = image_shader; - std::cout << "Initialized GUI\n"; return true; } diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 342a405f..536c835b 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -3,6 +3,7 @@ #include "client/gui/font/loader.hpp" #include "client/core.hpp" #include "client/client.hpp" +#include "client/shader.hpp" #include #include @@ -11,7 +12,7 @@ namespace gui::widget { -GLuint DynText::shader = 0; +std::unique_ptr DynText::shader = nullptr; DynText::DynText(glm::vec2 origin, std::string text, std::shared_ptr fonts, DynText::Options options): @@ -50,13 +51,12 @@ DynText::DynText(std::string text, std::shared_ptr fonts, Dyn void DynText::render() { glEnable(GL_CULL_FACE); - glUseProgram(DynText::shader); + DynText::shader->use(); - // glActiveTexture(GL_TEXTURE0); auto projection = GUI_PROJECTION_MATRIX(); - glUniformMatrix4fv(glGetUniformLocation(DynText::shader, "projection"), 1, false, reinterpret_cast(&projection)); + DynText::shader->setMat4("projection", projection); auto color = font::getRGB(this->options.color); - glUniform3f(glGetUniformLocation(DynText::shader, "textColor"), color.x, color.y, color.z); + DynText::shader->setVec3("textColor", color); glBindVertexArray(VAO); float x = this->origin.x; diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 18b589f9..25128f5b 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -1,12 +1,13 @@ #include "client/gui/gui.hpp" #include "client/client.hpp" +#include "client/shader.hpp" namespace gui::widget { -GLuint StaticImg::shader = 0; +std::unique_ptr StaticImg::shader = nullptr; StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): Widget(Type::StaticImg, origin) @@ -86,7 +87,7 @@ StaticImg::~StaticImg() { } void StaticImg::render() { - glUseProgram(StaticImg::shader); + StaticImg::shader->use(); glm::mat4 model = glm::mat4(1.0f); From a16c2ee827bb44c1a9b62201d06a1e0a48be00cc Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 14:51:44 -0700 Subject: [PATCH 83/92] fix serialize test f'ing up --- src/shared/tests/serialize_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/tests/serialize_test.cpp b/src/shared/tests/serialize_test.cpp index f822d7e7..fd979157 100644 --- a/src/shared/tests/serialize_test.cpp +++ b/src/shared/tests/serialize_test.cpp @@ -6,7 +6,7 @@ #include "server/game/servergamestate.hpp" TEST(SerializeTest, SerializeEvent) { - ServerGameState state(GamePhase::LOBBY, GameConfig{}); + ServerGameState state(GamePhase::LOBBY); state.addPlayerToLobby(1, "Player Name"); Event evt(0, EventType::LoadGameState, LoadGameStateEvent(state.generateSharedGameState())); Event evt2 = deserialize(serialize(evt)); @@ -20,7 +20,7 @@ TEST(SerializeTest, SerializeEvent) { } TEST(SerializeTest, SerializePacketEvent) { - ServerGameState state(GamePhase::LOBBY, GameConfig{}); + ServerGameState state(GamePhase::LOBBY); state.addPlayerToLobby(1, "Player Name"); Event evt(0, EventType::LoadGameState, LoadGameStateEvent(state.generateSharedGameState())); From f4bbadd20ca8cc734539bb7d98931ad41caa67e8 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 14:57:02 -0700 Subject: [PATCH 84/92] attempt to fix actions & compiling --- include/client/gui/gui.hpp | 1 + src/client/client.cpp | 1 - src/client/gui/CMakeLists.txt | 13 +++++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 54e6595c..d3cbe21f 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -29,6 +29,7 @@ namespace gui { */ enum class GUIState { NONE, + INITIAL_LOAD, TITLE_SCREEN, LOBBY_BROWSER, LOBBY, diff --git a/src/client/client.cpp b/src/client/client.cpp index 42f20189..ac6dfd7d 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -139,7 +139,6 @@ bool Client::init() { std::cout << "shader version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl; std::cout << "shader version: " << glGetString(GL_VERSION) << std::endl; - // Init GUI (e.g. load in all fonts) // TODO: pass in shader for image loading in second param if (!this->gui.init()) { diff --git a/src/client/gui/CMakeLists.txt b/src/client/gui/CMakeLists.txt index 33725620..fa19449a 100644 --- a/src/client/gui/CMakeLists.txt +++ b/src/client/gui/CMakeLists.txt @@ -23,9 +23,17 @@ find_package(OpenGL REQUIRED) add_library(${LIB_NAME} STATIC ${FILES}) target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES} ${STB_INCLUDE_DIRS}) target_link_libraries(${LIB_NAME} PRIVATE game_shared_lib) -target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${FREETYPE_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_include_directories(${LIB_NAME} PRIVATE + ${OPENGL_INCLUDE_DIRS} + ${BOOST_LIBRARY_INCLUDES} + ${STB_INCLUDE_DIRS} + glfw + glm + ${FREETYPE_INCLUDE_DIRS} + ${ASSIMP_INCLUDE_DIRS} + "${CMAKE_BINARY_DIR}/_deps/glew-src/include" +) target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static freetype) target_link_libraries(${LIB_NAME} PRIVATE @@ -35,4 +43,5 @@ target_link_libraries(${LIB_NAME} Boost::program_options Boost::serialization nlohmann_json::nlohmann_json + assimp ) \ No newline at end of file From 4ad15bd2aed4b4af43f7e96775a8100c35c05a0e Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 15:10:11 -0700 Subject: [PATCH 85/92] add loading screen --- config.json | 2 +- include/client/gui/font/font.hpp | 4 +++- include/client/gui/gui.hpp | 4 ++++ src/client/client.cpp | 7 +++++-- src/client/gui/font/font.cpp | 4 ++++ src/client/gui/gui.cpp | 30 ++++++++++++++++++++++++++++++ 6 files changed, 47 insertions(+), 4 deletions(-) diff --git a/config.json b/config.json index 7aec0a3e..751a77e9 100644 --- a/config.json +++ b/config.json @@ -13,7 +13,7 @@ "server": { "lobby_name": "Unnamed Lobby", "lobby_broadcast": true, - "max_players": 1 + "max_players": 2 }, "client": { "default_name": "John Doe", diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp index 34e80e39..03aebbc4 100644 --- a/include/client/gui/font/font.hpp +++ b/include/client/gui/font/font.hpp @@ -32,7 +32,9 @@ enum class Color { BLACK, RED, BLUE, - GRAY + GRAY, + WHITE, + TORCHLIGHT_GAMES }; /** diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index d3cbe21f..07293259 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -313,6 +313,10 @@ class GUI { /// more fine tuned GUI manipulation which may only make sense to do outside of these /// preset "layouts". /// + /** + * @brief Displays a loading screen while the game is starting up + */ + void _layoutLoadingScreen(); /** * @brief Displays the title screen layout * diff --git a/src/client/client.cpp b/src/client/client.cpp index ac6dfd7d..b13e05af 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -68,7 +68,7 @@ Client::Client(boost::asio::io_context& io_context, GameConfig config): gameState(GamePhase::TITLE_SCREEN, config), session(nullptr), gui(this), - gui_state(gui::GUIState::TITLE_SCREEN), + gui_state(gui::GUIState::INITIAL_LOAD), lobby_finder(io_context, config) { cam = new Camera(); @@ -129,7 +129,6 @@ bool Client::init() { /* Make the window's context current */ glfwMakeContextCurrent(window); - GLenum err = glewInit() ; if (GLEW_OK != err) { std::cerr << "Error loading GLEW: " << glewGetString(err) << std::endl; @@ -146,6 +145,8 @@ bool Client::init() { return false; } + this->displayCallback(); + auto shaders_dir = getRepoRoot() / "src/client/shaders"; auto graphics_assets_dir = getRepoRoot() / "assets/graphics"; @@ -171,6 +172,8 @@ bool Client::init() { auto lightFragFilepath = shaders_dir / "lightsource.frag"; this->light_source_shader = std::make_shared(lightVertFilepath.string(), lightFragFilepath.string()); + this->gui_state = GUIState::TITLE_SCREEN; + return true; } diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp index ec12946f..97af364d 100644 --- a/src/client/gui/font/font.cpp +++ b/src/client/gui/font/font.cpp @@ -34,6 +34,10 @@ glm::vec3 getRGB(Color color) { return {0.0f, 0.0f, 1.0f}; case Color::GRAY: return {0.5f, 0.5f, 0.5f}; + case Color::WHITE: + return {1.0f, 1.0f, 1.0f}; + case Color::TORCHLIGHT_GAMES: + return {0.902, 0.575, 0.055}; case Color::BLACK: default: diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 6be8bc94..fbc0c13d 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -132,6 +132,10 @@ void GUI::layoutFrame(GUIState state) { glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); this->_layoutTitleScreen(); break; + case GUIState::INITIAL_LOAD: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutLoadingScreen(); + break; case GUIState::GAME_ESC_MENU: glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); this->_layoutGameEscMenu(); @@ -153,6 +157,32 @@ void GUI::layoutFrame(GUIState state) { } } +void GUI::_layoutLoadingScreen() { + this->addWidget(widget::CenterText::make( + "made by", + font::Font::MENU, + font::Size::MEDIUM, + font::Color::WHITE, + fonts, + FRAC_WINDOW_HEIGHT(7,8) + )); + this->addWidget(widget::CenterText::make( + "Torchlight Games", + font::Font::MENU, + font::Size::XLARGE, + font::Color::TORCHLIGHT_GAMES, + fonts, + FRAC_WINDOW_HEIGHT(2,3) + )); + this->addWidget(widget::CenterText::make( + "Loading...", + font::Font::MENU, + font::Size::MEDIUM, + font::Color::WHITE, + fonts, + FRAC_WINDOW_HEIGHT(1,3) + )); +} void GUI::_layoutTitleScreen() { this->addWidget(widget::CenterText::make( From 6b70a20bdb1088f0e1461196b0e58311524eab82 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Tue, 7 May 2024 15:20:40 -0700 Subject: [PATCH 86/92] exit button calls glfwShouldClose instead of glfwDestroyWindow destroy window was giving a horrible error --- src/client/gui/gui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index fbc0c13d..89e407ff 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -331,7 +331,7 @@ void GUI::_layoutGameEscMenu() { widget->changeColor(font::Color::RED); }); exit_game_txt->addOnClick([this](widget::Handle handle) { - glfwDestroyWindow(this->client->getWindow()); + glfwSetWindowShouldClose(this->client->getWindow(), GL_TRUE); }); auto flex = widget::Flexbox::make( glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 2)), From ce716a7ba11200a8fb70d686bc85f0da7d36f3af Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 15:56:17 -0700 Subject: [PATCH 87/92] fix cube min/max --- config.json | 2 +- src/client/cube.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index 751a77e9..7aec0a3e 100644 --- a/config.json +++ b/config.json @@ -13,7 +13,7 @@ "server": { "lobby_name": "Unnamed Lobby", "lobby_broadcast": true, - "max_players": 2 + "max_players": 1 }, "client": { "default_name": "John Doe", diff --git a/src/client/cube.cpp b/src/client/cube.cpp index 163ef89e..ef0a295e 100644 --- a/src/client/cube.cpp +++ b/src/client/cube.cpp @@ -4,8 +4,9 @@ Cube::Cube(glm::vec3 newColor) { // create a vertex buffer for positions and normals // insert the data into these buffers // initialize model matrix - glm::vec3 cubeMin = glm::vec3(-1.0f, -1.0f, -1.0f); - glm::vec3 cubeMax = glm::vec3(1.0f, 1.0f, 1.0f); + glm::vec3 cubeMin = glm::vec3(-0.5f, -0.5f, -0.5f); + glm::vec3 cubeMax = glm::vec3(0.5f, 0.5f, 0.5f); + // The color of the cube. Try setting it to something else! color = newColor; From 1b9f9c16f741ca52a1a8fdbda391811cf20041a9 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 16:13:27 -0700 Subject: [PATCH 88/92] fix lint --- CMakeLists.txt | 2 ++ include/client/gui/widget/dyntext.hpp | 4 ++-- include/client/gui/widget/staticimg.hpp | 2 +- include/client/gui/widget/textinput.hpp | 2 +- src/client/gui/gui.cpp | 2 +- src/client/gui/imgs/loader.cpp | 13 +++++------- src/client/gui/widget/dyntext.cpp | 4 ++-- src/client/gui/widget/staticimg.cpp | 28 +------------------------ src/client/gui/widget/textinput.cpp | 4 ++-- src/server/game/servergamestate.cpp | 9 ++++---- 10 files changed, 22 insertions(+), 48 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d5400af..d238068a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,8 @@ add_custom_target(lint --inline-suppr # return error exit code 1 on failures --error-exitcode=1 + # ignore certain warnings that are annoying + --suppress=unusedVariable # tell it about our include directory -I ${CMAKE_SOURCE_DIR}/include # only check include and src diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp index 52ec9902..e6f189a8 100644 --- a/include/client/gui/widget/dyntext.hpp +++ b/include/client/gui/widget/dyntext.hpp @@ -40,8 +40,8 @@ class DynText : public Widget { return std::make_unique(std::forward(params)...); } - DynText(glm::vec2 origin, std::string text, std::shared_ptr loader, Options options); - DynText(std::string text, std::shared_ptr loader, Options options); + DynText(glm::vec2 origin, const std::string& text, std::shared_ptr loader, Options options); + DynText(const std::string& text, std::shared_ptr loader, Options options); void render() override; diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp index 2a762726..7e5fa6b6 100644 --- a/include/client/gui/widget/staticimg.hpp +++ b/include/client/gui/widget/staticimg.hpp @@ -37,7 +37,7 @@ class StaticImg : public Widget { } StaticImg(glm::vec2 origin, gui::img::Img img); - StaticImg(gui::img::Img img); + explicit StaticImg(gui::img::Img img); ~StaticImg(); void render() override; diff --git a/include/client/gui/widget/textinput.hpp b/include/client/gui/widget/textinput.hpp index 3be609eb..54c0df6b 100644 --- a/include/client/gui/widget/textinput.hpp +++ b/include/client/gui/widget/textinput.hpp @@ -38,7 +38,7 @@ class TextInput : public Widget { } TextInput(glm::vec2 origin, - std::string placeholder, + const std::string& placeholder, gui::GUI* gui, std::shared_ptr fonts, DynText::Options options); diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 89e407ff..b9b8169d 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -10,7 +10,7 @@ namespace gui { -GUI::GUI(Client* client) { +GUI::GUI(Client* client): capture_keystrokes(false) { this->client = client; } diff --git a/src/client/gui/imgs/loader.cpp b/src/client/gui/imgs/loader.cpp index d7141226..364d9b2e 100644 --- a/src/client/gui/imgs/loader.cpp +++ b/src/client/gui/imgs/loader.cpp @@ -2,6 +2,7 @@ #include "client/core.hpp" #include +#include #include "stb_image.h" @@ -11,7 +12,7 @@ bool Loader::init() { std::cout << "Loading images...\n"; for (auto img_id : GET_ALL_IMG_IDS()) { - if (!this->_loadImg(img_id)) { + if (!this->_loadImg(img_id)) { // cppcheck-suppress useStlAlgorithm return false; } } @@ -39,21 +40,17 @@ bool Loader::_loadImg(ImgID img_id) { auto path = getImgFilepath(img_id); std::cout << "Loading " << path << "...\n"; unsigned char* img_data = stbi_load(path.c_str(), &width, &height, &channels, 0); - std::cout << img_data << std::endl; if (stbi_failure_reason()) std::cout << "failure: " << stbi_failure_reason() << std::endl; if (img_data == 0 || width == 0 || height == 0) { - std::cerr << "Error loading " << path << "! " << img_data << - ", " << width << ", " << height << "\n" << std::endl; + std::cerr << "Error loading " << path << std::endl; return false; } - if (img_data) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data); - glGenerateMipmap(GL_TEXTURE_2D); - } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data); + glGenerateMipmap(GL_TEXTURE_2D); // unbind texture glBindTexture(GL_TEXTURE_2D, 0); diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp index 536c835b..43e2dbc7 100644 --- a/src/client/gui/widget/dyntext.cpp +++ b/src/client/gui/widget/dyntext.cpp @@ -14,7 +14,7 @@ namespace gui::widget { std::unique_ptr DynText::shader = nullptr; -DynText::DynText(glm::vec2 origin, std::string text, +DynText::DynText(glm::vec2 origin, const std::string& text, std::shared_ptr fonts, DynText::Options options): text(text), options(options), fonts(fonts), Widget(Type::DynText, origin) { @@ -45,7 +45,7 @@ DynText::DynText(glm::vec2 origin, std::string text, } } -DynText::DynText(std::string text, std::shared_ptr fonts, DynText::Options options): +DynText::DynText(const std::string& text, std::shared_ptr fonts, DynText::Options options): DynText({0.0f, 0.0f}, text, fonts, options) {} void DynText::render() { diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp index 25128f5b..a1d363cc 100644 --- a/src/client/gui/widget/staticimg.cpp +++ b/src/client/gui/widget/staticimg.cpp @@ -10,33 +10,8 @@ namespace gui::widget { std::unique_ptr StaticImg::shader = nullptr; StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): - Widget(Type::StaticImg, origin) + Widget(Type::StaticImg, origin), img(img) { - // // configure VAO/VBO - // unsigned int VBO; - // // there might be some mismatch here because this might be assuming that the top left - // // corner is 0,0 when we are specifying origin by bottom left coordinate - // float vertices[] = { - // // pos // tex - // 0.0f, 1.0f, 0.0f, 1.0f, - // 1.0f, 0.0f, 1.0f, 0.0f, - // 0.0f, 0.0f, 0.0f, 0.0f, - // 0.0f, 1.0f, 0.0f, 1.0f, - // 1.0f, 1.0f, 1.0f, 1.0f, - // 1.0f, 0.0f, 1.0f, 0.0f - // }; - // glGenVertexArrays(1, &quadVAO); - // glGenBuffers(1, &VBO); - - // glBindBuffer(GL_ARRAY_BUFFER, VBO); - // glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - // glBindVertexArray(quadVAO); - // glEnableVertexAttribArray(0); - // glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float),(void*)0); - // glBindBuffer(GL_ARRAY_BUFFER, 0); - // glBindVertexArray(0); - float vertices[] = { // positions // colors // texture coords 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right @@ -71,7 +46,6 @@ StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); - this->width = img.width; this->height = img.height; this->texture_id = img.texture_id; diff --git a/src/client/gui/widget/textinput.cpp b/src/client/gui/widget/textinput.cpp index d72c0929..13894eb0 100644 --- a/src/client/gui/widget/textinput.cpp +++ b/src/client/gui/widget/textinput.cpp @@ -12,11 +12,11 @@ namespace gui::widget { std::string TextInput::prev_input = ""; TextInput::TextInput(glm::vec2 origin, - std::string placeholder, + const std::string& placeholder, gui::GUI* gui, std::shared_ptr fonts, DynText::Options options): - Widget(Type::TextInput, origin) + Widget(Type::TextInput, origin), gui(gui) { std::string text_to_display; font::Color color_to_display; diff --git a/src/server/game/servergamestate.cpp b/src/server/game/servergamestate.cpp index 320932f4..a50a00d9 100644 --- a/src/server/game/servergamestate.cpp +++ b/src/server/game/servergamestate.cpp @@ -187,9 +187,9 @@ void ServerGameState::updateMovement() { if (object == nullptr) continue; - bool collided = false; - bool collidedX = false; - bool collidedZ = false; + bool collided = false; // cppcheck-suppress variableScope + bool collidedX = false; // cppcheck-suppress variableScope + bool collidedZ = false; // cppcheck-suppress variableScope if (object->physics.movable) { // Check for collision at position to move, if so, dont change position @@ -419,7 +419,8 @@ void ServerGameState::loadMaze() { file.close(); // Verify that there's at least one spawn point - assert(this->grid.getSpawnPoints().size() > 0); + size_t num_spawn_points = this->grid.getSpawnPoints().size(); + assert(num_spawn_points > 0); // Step 5: Add floor and ceiling SolidSurfaces. From 4cb9cbd4f46a399d57ce9e5d8fa68bb1de965cb0 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 16:19:22 -0700 Subject: [PATCH 89/92] make player eye level a constant --- include/client/constants.hpp | 4 +++- src/client/client.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/include/client/constants.hpp b/include/client/constants.hpp index c51064ea..57432c67 100644 --- a/include/client/constants.hpp +++ b/include/client/constants.hpp @@ -6,4 +6,6 @@ // The screen width that's defined as 1:1, from which other // resolutions can calculate pixel sizes #define UNIT_WINDOW_WIDTH 1500 -#define UNIT_WINDOW_HEIGHT 1000 \ No newline at end of file +#define UNIT_WINDOW_HEIGHT 1000 + +#define PLAYER_EYE_LEVEL 2.0f \ No newline at end of file diff --git a/src/client/client.cpp b/src/client/client.cpp index 4334aa7a..4676e0f7 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -318,7 +318,7 @@ void Client::draw() { // don't render yourself if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { glm::vec3 pos = sharedObject->physics.position; - pos.y += 1.5f; + pos.y += PLAYER_EYE_LEVEL; cam->updatePos(pos); break; } From 439ab1c47793c6b69a4803506a1c27617dd838a4 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 16:57:04 -0700 Subject: [PATCH 90/92] add warren bear spawning --- config.json | 2 +- include/server/game/gridcell.hpp | 1 + maps/maze5.maze | 2 +- src/client/client.cpp | 2 - src/client/main.cpp | 2 +- src/server/game/gridcell.cpp | 2 + src/server/game/servergamestate.cpp | 7 +++ src/server/server.cpp | 66 ----------------------------- 8 files changed, 13 insertions(+), 71 deletions(-) diff --git a/config.json b/config.json index bb9a445e..9d1dc885 100644 --- a/config.json +++ b/config.json @@ -18,6 +18,6 @@ "client": { "default_name": "John Doe", "lobby_discovery": true, - "window_width": 2000 + "window_width": 1500 } } \ No newline at end of file diff --git a/include/server/game/gridcell.hpp b/include/server/game/gridcell.hpp index 02e236d2..b0c29072 100644 --- a/include/server/game/gridcell.hpp +++ b/include/server/game/gridcell.hpp @@ -4,6 +4,7 @@ enum class CellType { Empty, Wall, Spawn, + Enemy, Unknown }; diff --git a/maps/maze5.maze b/maps/maze5.maze index e87094c9..2b32698b 100644 --- a/maps/maze5.maze +++ b/maps/maze5.maze @@ -1,5 +1,5 @@ ############################# -#@......#...#@............#.# +#@......#...#@............#E# #######...#.#...@...##.##.#.# #.......###.........#.@.#...# ############################# \ No newline at end of file diff --git a/src/client/client.cpp b/src/client/client.cpp index 4676e0f7..2dbc2651 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -311,8 +311,6 @@ void Client::draw() { continue; } - std::cout << "shared object " << i << ": position: " << glm::to_string(sharedObject->physics.position) << std::endl; - switch (sharedObject->type) { case ObjectType::Player: { // don't render yourself diff --git a/src/client/main.cpp b/src/client/main.cpp index 9bd812ae..d531369f 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -76,7 +76,7 @@ int main(int argc, char* argv[]) // Setup OpenGL settings. set_opengl_settings(window); - boost::filesystem::path soundFilepath = getRepoRoot() / "assets/sounds/piano.wav"; + boost::filesystem::path soundFilepath = getRepoRoot() / "assets/sounds/speedrun.mp3"; sf::SoundBuffer buffer; if (!buffer.loadFromFile(soundFilepath.string())) diff --git a/src/server/game/gridcell.cpp b/src/server/game/gridcell.cpp index cb0e3f7c..c73ac81d 100644 --- a/src/server/game/gridcell.cpp +++ b/src/server/game/gridcell.cpp @@ -11,6 +11,8 @@ CellType charToCellType(char c) { return CellType::Wall; case '@': return CellType::Spawn; + case 'E': + return CellType::Enemy; default: return CellType::Unknown; } diff --git a/src/server/game/servergamestate.cpp b/src/server/game/servergamestate.cpp index a50a00d9..ebed4711 100644 --- a/src/server/game/servergamestate.cpp +++ b/src/server/game/servergamestate.cpp @@ -472,6 +472,13 @@ void ServerGameState::loadMaze() { GridCell* cell = this->grid.getCell(col, row); switch (cell->type) { + case CellType::Enemy: { + SpecificID enemyID = this->objects.createObject(ObjectType::Enemy); + + Enemy* enemy = this->objects.getEnemy(enemyID); + enemy->physics.shared.position = this->grid.gridCellCenterPosition(cell); + break; + } case CellType::Wall: { // Create a new Wall object SpecificID wallID = diff --git a/src/server/server.cpp b/src/server/server.cpp index d3bacc88..7ab2e7d5 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -35,72 +35,6 @@ Server::Server(boost::asio::io_context& io_context, GameConfig config) world_eid(0), state(ServerGameState(GamePhase::LOBBY, config)) { - /* - EntityID id = state.objects.createObject(ObjectType::Object); - Object* cube = (Object*)state.objects.getObject(id); - cube->physics.shared.position = glm::vec3(0.0f, 0.0f, 5.0f); - cube->physics.shared.corner = glm::vec3(-4.0f, -4.0f, 3.5f); - cube->physics.boundary = new BoxCollider(cube->physics.shared.corner, glm::vec3(8.0f, 8.0f, 3.0f)); - cube->physics.movable = false; - - /* - EntityID floorID = state.objects.createObject(ObjectType::SolidSurface); - SolidSurface* floor = (SolidSurface*)state.objects.getObject(floorID); - floor->shared.dimensions = glm::vec3(20.0f, 0.1f, 20.0f); - floor->physics.shared.corner = glm::vec3(-10.0f, -0.1f, -10.0f); - floor->physics.shared.position = glm::vec3(0.0f, -0.05f, 0.0f); - floor->physics.boundary = new BoxCollider(floor->physics.shared.corner, floor->shared.dimensions); - floor->physics.movable = false;*/ - - /* - // Create a room - /* - EntityID wall1ID = state.objects.createObject(ObjectType::SolidSurface); - EntityID wall2ID = state.objects.createObject(ObjectType::SolidSurface); - EntityID wall3ID = state.objects.createObject(ObjectType::SolidSurface); - EntityID wall4ID = state.objects.createObject(ObjectType::SolidSurface); - EntityID floorID = state.objects.createObject(ObjectType::SolidSurface); - - // Specify wall positions (RECREATED TO MATCH AXIS) - // Configuration: 18 (z) x 20 (x) room example - // (z-axis) - // ##1## - // # # - // 2 3 (x-axis) - // # # - // ##4## - - SolidSurface* wall1 = (SolidSurface*)state.objects.getObject(wall1ID); - SolidSurface* wall2 = (SolidSurface*)state.objects.getObject(wall2ID); - SolidSurface* wall3 = (SolidSurface*)state.objects.getObject(wall3ID); - SolidSurface* wall4 = (SolidSurface*)state.objects.getObject(wall4ID); - SolidSurface* floor = (SolidSurface*)state.objects.getObject(floorID); - - // Wall1 has dimensions (20, 4, 1); and position (0, 0, -19.5) - wall1->shared.dimensions = glm::vec3(20, 4, 1); - wall1->physics.shared.position = glm::vec3(0, 0, -19.5); - wall1->physics.movable = false; - - // Wall2 has dimensions (1, 4, 18) and position (-19.5, 0, 0) - wall2->shared.dimensions = glm::vec3(1, 4, 18); - wall2->physics.shared.position = glm::vec3(-19.5, 0, 0); - wall2->physics.movable = false; - - // Wall3 has dimensions (1, 4, 18) and position (19.5, 0, 0) - wall3->shared.dimensions = glm::vec3(1, 4, 18); - wall3->physics.shared.position = glm::vec3(19.5, 0, 0); - wall3->physics.movable = false; - - // Wall4 has dimensions (20, 4, 1) and position (0, 0, 19.5) - wall4->shared.dimensions = glm::vec3(20, 4, 1); - wall4->physics.shared.position = glm::vec3(0, 0, 19.5); - wall4->physics.movable = false; - - // floor has dimensions (20, 0.1, 20) and position (0, -1.3, 0) - floor->shared.dimensions = glm::vec3(20.0f, 0.1f, 20.0f); - floor->physics.shared.position = glm::vec3(0.0f, -1.3f, 0.0f); - floor->physics.movable = false;*/ - _doAccept(); // start asynchronously accepting if (config.server.lobby_broadcast) { From ff7cacaee2fa9401e92eb2360dacb16ca0175e00 Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 17:17:21 -0700 Subject: [PATCH 91/92] add spawnpoint to default maze so hopefully it works in shared test action? --- maps/default_maze.maze | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps/default_maze.maze b/maps/default_maze.maze index 945c9b46..b516b2c4 100644 --- a/maps/default_maze.maze +++ b/maps/default_maze.maze @@ -1 +1 @@ -. \ No newline at end of file +@ \ No newline at end of file From 8e29da65fc0f94331613325344b8ac42e73989ce Mon Sep 17 00:00:00 2001 From: Tyler Lentz Date: Tue, 7 May 2024 17:20:50 -0700 Subject: [PATCH 92/92] fix troll speedrun sound for meme --- src/client/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/main.cpp b/src/client/main.cpp index d531369f..9bd812ae 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -76,7 +76,7 @@ int main(int argc, char* argv[]) // Setup OpenGL settings. set_opengl_settings(window); - boost::filesystem::path soundFilepath = getRepoRoot() / "assets/sounds/speedrun.mp3"; + boost::filesystem::path soundFilepath = getRepoRoot() / "assets/sounds/piano.wav"; sf::SoundBuffer buffer; if (!buffer.loadFromFile(soundFilepath.string()))