diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d77ba11f9a102053cd85d5e50bbe9c785ea1cb98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +.vscode +imgui.ini diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..ee86091d2d5b528452bcf4dafe0e156f29f55cde --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "libs/spdlog"] + path = libs/spdlog + url = https://github.com/gabime/spdlog/ +[submodule "libs/imgui"] + path = libs/imgui + url = https://github.com/ocornut/imgui diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..6e0c1a4b8c47d0f7e973a5aa99af84dbd8c46661 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.10) +project(QPong VERSION 0.0.1 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_BUILD_TYPE Release) +set(CMAKE_CXX_FLAGS "-O3") + +option(LOG_DEBUG_INFO "Enable logging t console" OFF) +if(LOG_DEBUG_INFO) + add_compile_definitions(ENABLE_LOG_DEBUG_INFO) +endif(LOG_DEBUG_INFO) + +# Check for PkgConfig +find_package(PkgConfig REQUIRED) + +# GUI/Window +add_subdirectory(GuiCore) + +# QPong App sources +add_subdirectory(QPongApp) + +# Libraries the project depends on: +add_subdirectory(libs/spdlog) diff --git a/GuiCore/Application.cpp b/GuiCore/Application.cpp new file mode 100644 index 0000000000000000000000000000000000000000..35ccc77582faf1a09a9dc513d4684d56a791aa95 --- /dev/null +++ b/GuiCore/Application.cpp @@ -0,0 +1,111 @@ +/// @file Application.cpp +/// @author Armin Co +/// + +#include <GLFW/glfw3.h> + +#include "Application.hpp" +#include "Log.hpp" +#include "ApplicationEvent.hpp" + + +namespace GuiCore +{ + Application* Application::s_instance = nullptr; + + Application::Application(const std::string &name, uint32_t width, uint32_t height) + { + if (s_instance == nullptr) + { + Log::setup(); + } + s_instance = this; + m_window = std::unique_ptr<IWindow>(IWindow::create({name, width, height})); + m_window->setEventCallback([this](auto &&... args) -> decltype(auto) { return this->onEvent(std::forward<decltype(args)>(args)...); }); + + m_guiLayer = std::make_shared<ImGuiLayer>(); + pushLayer(m_guiLayer); + } + + void Application::pushLayer(std::shared_ptr<Layer> layer) + { + m_layerStack.pushLayer(layer); + } + + void Application::popLayer(std::shared_ptr<Layer> layer) + { + m_layerStack.popLayer(layer); + } + + void Application::onEvent(Event &event) + { + Log::get()->trace("{}", event.toString()); + EventDispatcher dispatcher(event); + dispatcher.dispatch<WindowCloseEvent>( + [this](auto &&... args) -> decltype(auto) { return this->onWindowClose(std::forward<decltype(args)>(args)...); }); + dispatcher.dispatch<WindowResizeEvent>( + [this](auto &&... args) -> decltype(auto) { return this->onWindowResize(std::forward<decltype(args)>(args)...); }); + + for (auto it = m_layerStack.rbegin(); it != m_layerStack.rend(); ++it) + { + if (event.handled) + { + break; + } + (*it)->onEvent(event); + } + } + + void Application::run() + { + while (m_isRunnig) + { + float time = static_cast<float>(glfwGetTime()); + Timestep timeStep = time - m_lastFrameTime; + m_lastFrameTime = time; + + if (!m_minimzed) + { + for (auto layer : m_layerStack) + { + layer->onUpdate(timeStep); + } + } + + m_guiLayer->begin(); + for (auto layer: m_layerStack) + { + layer->onGuiRender(); + } + m_guiLayer->end(); + + m_window->onUpdate(); + } + } + + bool Application::onWindowClose(Event &event) + { + m_isRunnig = false; + return true; + } + + bool Application::onWindowResize(Event &event) + { + WindowResizeEvent *rsEvent = static_cast<WindowResizeEvent*>(&event); + Log::get()->trace("Window minimized, {0}, {1}", rsEvent->getWidth(), rsEvent->getHeight()); + glViewport(0,0, rsEvent->getWidth(), rsEvent->getHeight()); + + return false; + } + + Application& Application::get() + { + return *s_instance; + } + + IWindow& Application::getWindow() + { + return *m_window; + } +} + diff --git a/GuiCore/Application.hpp b/GuiCore/Application.hpp new file mode 100644 index 0000000000000000000000000000000000000000..ad48c239feda5139a856edd86811142a668e8bbf --- /dev/null +++ b/GuiCore/Application.hpp @@ -0,0 +1,47 @@ +/// @file Application.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_APPLICATION_HPP +#define GUI_CORE_APPLICATION_HPP + +#include <string> +#include <memory> + +#include "Event.hpp" +#include "IWindow.hpp" +#include "LayerStack.hpp" +#include "ImGuiLayer.hpp" +#include "ApplicationEvent.hpp" + +namespace GuiCore +{ + class Application + { + public: + Application(const std::string &name = "Application", uint32_t width = 1280, uint32_t height = 720); + virtual ~Application() {} + + void run(); + void onEvent(Event &e); + void pushLayer(std::shared_ptr<Layer> layer); + void popLayer(std::shared_ptr<Layer> layer); + IWindow& getWindow(); + bool onWindowClose(Event &e); + bool onWindowResize(Event &e); + + static Application& get(); + + private: + std::unique_ptr<IWindow> m_window; + bool m_isRunnig = true; + bool m_minimzed = false; + LayerStack m_layerStack; + float m_lastFrameTime = 0.0f; + std::shared_ptr<ImGuiLayer> m_guiLayer; + + static Application *s_instance; + }; +} + +#endif \ No newline at end of file diff --git a/GuiCore/ApplicationEvent.hpp b/GuiCore/ApplicationEvent.hpp new file mode 100644 index 0000000000000000000000000000000000000000..b71ec60974387776611c6d6020b003c57fcd3381 --- /dev/null +++ b/GuiCore/ApplicationEvent.hpp @@ -0,0 +1,49 @@ +/// @file ApplicationEvent.hpp +/// @author Armin Co +/// + +#ifndef APPLICATION_EVENT_HPP +#define APPLICATION_EVENT_HPP + +#include <sstream> +#include "Event.hpp" + +namespace GuiCore +{ + class WindowResizeEvent : public Event + { + public: + WindowResizeEvent(uint32_t width, uint32_t height) + : m_width{width} + , m_height{height} + { + } + + uint32_t getWidth() const {return m_width;} + uint32_t getHeight() const {return m_height;} + + std::string toString() const override + { + std::stringstream ss; + ss << "WindowResizeEvent: " << m_width << ", " << m_height; + return ss.str(); + } + + EVENT_CLASS_TYPE(WindowResize) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + + private: + uint32_t m_width; + uint32_t m_height; + }; + + class WindowCloseEvent : public Event + { + public: + WindowCloseEvent() = default; + EVENT_CLASS_TYPE(WindowClose) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + }; +} + +#endif \ No newline at end of file diff --git a/GuiCore/CMakeLists.txt b/GuiCore/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..38492720572ddb2a19b8c15a664c54716efa18dc --- /dev/null +++ b/GuiCore/CMakeLists.txt @@ -0,0 +1,61 @@ +find_package( Freetype REQUIRED) + +# GuiCore library +add_library(GuiCore + Application.cpp + Input.cpp + Layer.cpp + LayerStack.cpp + Log.cpp + Timer.cpp + Timestep.cpp + Glyphs.cpp +) + +target_include_directories( GuiCore PUBLIC + ../libs/spdlog/include/ + ${FREETYPE_INCLUDE_DIRS} +) + +target_link_libraries( GuiCore + spdlog + ${FREETYPE_LIBRARIES} +) +add_compile_definitions(IMGUI_IMPL_OPENGL_LOADER_GL3W) + +# ImGui is a dependency of GuiCore +add_library( ImGui + Shader.cpp + ImGuiLayer.cpp + ../libs/imgui/imgui.cpp + ../libs/imgui/imgui_draw.cpp + ../libs/imgui/imgui_widgets.cpp + ../libs/imgui/imgui_tables.cpp + ../libs/imgui/backends/imgui_impl_glfw.cpp + ../libs/imgui/backends/imgui_impl_opengl3.cpp + ../libs/imgui/examples/libs/gl3w/GL/gl3w.c +) + +target_include_directories( ImGui PUBLIC + ../libs + ../libs/imgui # for glfw + ../libs/imgui/backends + ../libs/imgui/examples/libs/gl3w + ../libs/spdlog/include +) + + +target_link_libraries( GuiCore + ImGui +) + +# Check for GLFW3 and link +pkg_check_modules(GLFW glfw3) +target_include_directories(GuiCore PUBLIC ${GLFW_INCLUDE_DIRS}) +target_link_libraries(GuiCore ${GLFW_LIBRARIES}) + + +# Check for OpenGL +set(OpenGL_GL_PREFERENCE GLVND) +find_package(OpenGL REQUIRED) +target_link_libraries(GuiCore OpenGL::GL ${CMAKE_DL_LIBS}) diff --git a/GuiCore/Event.hpp b/GuiCore/Event.hpp new file mode 100644 index 0000000000000000000000000000000000000000..c1390bd22f14913b3c29e5ae15a2f3b710350fff --- /dev/null +++ b/GuiCore/Event.hpp @@ -0,0 +1,83 @@ +/// @file Event.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_EVENT_HPP +#define GUI_CORE_EVENT_HPP + +#include <string> +#include "Log.hpp" + +#define BIT(x) (1 << x) + +namespace GuiCore +{ + enum class EventType + { + None = 0, + WindowClose, WindowResize, WindowFocus, WindowLostFocus, WindowMoved, + AppTick, AppUpdate, AppRender, + KeyPressed, KeyReleased, KeyTyped, + MouseButtonPressed, MouseButtonReleased, MouseMoved, MouseScrolled + }; + + enum EventCategory + { + None = 0, + EventCategoryApplication = BIT(0), + EventCategoryInput = BIT(1), + EventCategoryKeyboard = BIT(2), + EventCategoryMouse = BIT(3), + EventCategoryMouseButton = BIT(4) + }; + +#define EVENT_CLASS_TYPE(type) static EventType getStaticType() { return EventType::type; }\ + virtual EventType getEventType() const override { return getStaticType(); }\ + virtual const char* getName() const override { return #type; } +#define EVENT_CLASS_CATEGORY(category) virtual int getCategoryFlags() const override { return category; } + + class Event + { + public: + bool handled = false; + + virtual EventType getEventType() const = 0; + virtual const char * getName() const = 0; + virtual int getCategoryFlags() const = 0; + virtual std::string toString() const {return getName();} + + inline bool isInCategory(EventCategory category) + { + return getCategoryFlags() & category; + } + }; + + class EventDispatcher + { + public: + EventDispatcher(Event &event) + : m_event{event} + { + } + + template<typename T, typename F> + bool dispatch(const F& function) + { + if (m_event.getEventType() == T::getStaticType()) + { + m_event.handled = function(static_cast<T&>(m_event)); + return true; + } + return false; + } + private: + Event &m_event; + }; + + inline std::ostream& operator<<(std::ostream& os, const Event &e) + { + return os << e.toString(); + } +} + +#endif \ No newline at end of file diff --git a/GuiCore/Glyphs.cpp b/GuiCore/Glyphs.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9d30992c002a8cc8ffbfe67f74bd3b5359cd7b08 --- /dev/null +++ b/GuiCore/Glyphs.cpp @@ -0,0 +1,130 @@ +/// @file Glyphs.cpp +/// @author Armin Co +/// + +#include "Glyphs.hpp" +#include <GLFW/glfw3.h> +#include "Log.hpp" +#include <glm/gtc/type_ptr.hpp> + + +extern "C" { +#include <ft2build.h> +#include FT_FREETYPE_H +} + + +std::map<char, GuiCore::Character> GuiCore::Glyphs::s_characters; +GuiCore::Shader* GuiCore::Glyphs::m_shader; +GLuint GuiCore::Glyphs::m_vertexArray; +GLuint GuiCore::Glyphs::m_vertexBuffer; +glm::mat4 GuiCore::Glyphs::projection = glm::ortho(-3.2f, 3.2f, -1.8f, 1.8f, -1.0f, 1.0f); + +void GuiCore::Glyphs::setup() +{ + m_shader = GuiCore::Shader::fromGLSLTextFiles( + "assets/shaders/glyph.vert.glsl", + "assets/shaders/glyph.frag.glsl" + ); + + glGenVertexArrays(1, &m_vertexArray); + glGenBuffers(1, &m_vertexBuffer); + glBindVertexArray(m_vertexArray); + glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer); + 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); + + FT_Library ft; + if (FT_Init_FreeType(&ft)) + { + GuiCore::Log::get()->error("ERROR::FREETYPE: Could not init FreeType Library"); + } + + FT_Face face; + if (FT_New_Face(ft, "assets/Font.ttf", 0, &face)) + { + GuiCore::Log::get()->error("ERROR::FREETYPE: Failed to load font"); + } + FT_Set_Pixel_Sizes(face, 0, 48); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + for (unsigned char c = 0; c < 128; c++) + { + // load character glyph + auto error = FT_Load_Char(face, c, FT_LOAD_RENDER); + if (error) + { + GuiCore::Log::get()->error("ERROR::FREETYTPE: Failed to load Glyph {0} {1}", c, error); + continue; + } + // 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<uint32_t>(face->glyph->advance.x) + }; + s_characters.insert(std::pair<char, Character>(c, character)); + } +} + +void GuiCore::Glyphs::renderText(std::string text, float x, float y, float scale, glm::vec3 color) +{ + glUseProgram(m_shader->getRendererID()); + glUniformMatrix4fv(glGetUniformLocation(m_shader->getRendererID(), "projection"), 1, GL_FALSE, glm::value_ptr(projection)); + glUniform3f(glGetUniformLocation(m_shader->getRendererID(), "textColor"), color.r, color.g, color.b); + glActiveTexture(GL_TEXTURE0); + glBindVertexArray(m_vertexArray); + + std::string::const_iterator c; + for (c = text.begin(); c != text.end(); c++) + { + Character ch = s_characters[*c]; + + float xPos = x + ch.bearing.x * scale; + float yPos = y - (ch.size.y - ch.bearing.y) * scale; + + float w = ch.size.x * scale; + float h = ch.size.y * scale; + + 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 } + }; + glBindTexture(GL_TEXTURE_2D, ch.textureId); + glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glDrawArrays(GL_TRIANGLES, 0, 6); + x += (ch.advance >> 6) * scale; + } + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); +} \ No newline at end of file diff --git a/GuiCore/Glyphs.hpp b/GuiCore/Glyphs.hpp new file mode 100644 index 0000000000000000000000000000000000000000..75c395393f183f99d8feb1415a4aef4fc441494d --- /dev/null +++ b/GuiCore/Glyphs.hpp @@ -0,0 +1,39 @@ +/// @file Glyphs.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_GLYPHS_HPP +#define GUI_CORE_GLYPHS_HPP + +#include <map> +#include <glm/glm.hpp> + + +#include "Shader.hpp" + +namespace GuiCore +{ +struct Character { + unsigned int textureId; // ID handle of the 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 +}; + +class Glyphs +{ +public: + static void setup(); + static void renderText(std::string text, float x, float y, float scale, glm::vec3 color); + +private: + static std::map<char, Character> s_characters; + static Shader *m_shader; + static GLuint m_vertexArray; + static GLuint m_vertexBuffer; + static glm::mat4 projection; +}; + +} + +#endif \ No newline at end of file diff --git a/GuiCore/IWindow.hpp b/GuiCore/IWindow.hpp new file mode 100644 index 0000000000000000000000000000000000000000..4dbd713c0bbebd65808a263043e34488cfd29aae --- /dev/null +++ b/GuiCore/IWindow.hpp @@ -0,0 +1,46 @@ +/// @file IWindow.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_WINDOW_HPP +#define GUI_CORE_WINDOW_HPP + +#include <string> +#include <functional> + +#include "Event.hpp" + +namespace GuiCore +{ + struct WindowProperties + { + std::string title; + uint32_t width; + uint32_t height; + + WindowProperties(const std::string &t = "Application", uint32_t w = 1920, uint32_t h = 1080) + : title{t} + , width{w} + , height{h} + { + } + }; + + class IWindow + { + public: + virtual ~IWindow() {} + virtual void onUpdate() = 0; + virtual uint32_t getWidth() const = 0; + virtual uint32_t getHeight() const = 0; + + virtual void setEventCallback(const std::function<void(GuiCore::Event&)>) = 0; + virtual void setVSync(bool enabled) = 0; + virtual bool isVSyncEnabled() const = 0; + virtual void* getNativeWindow() const = 0; + + static IWindow* create(const WindowProperties &props = WindowProperties()); + }; +} + +#endif \ No newline at end of file diff --git a/GuiCore/ImGuiLayer.cpp b/GuiCore/ImGuiLayer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..411c6259ff1d98efd9158e792ef536cbd9606bc7 --- /dev/null +++ b/GuiCore/ImGuiLayer.cpp @@ -0,0 +1,76 @@ +/// @file ImGuiLayer.cpp +/// @author Armin Co +/// + +#include <GLFW/glfw3.h> + +#include <imgui/imgui.h> +#include <imgui/backends/imgui_impl_glfw.h> +#include <imgui/backends/imgui_impl_opengl3.h> + +#include "Application.hpp" +#include "ImGuiLayer.hpp" +#include "Log.hpp" + +using namespace GuiCore; + +ImGuiLayer::ImGuiLayer() + : Layer("ImGuiLayer") +{ +} + +ImGuiLayer::~ImGuiLayer() +{ +} + +void ImGuiLayer::onAttach() +{ + GuiCore::Log::get()->trace("onAttach ImGuiLayer"); + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + + + ImGui::StyleColorsDark(); + + Application& app = Application::get(); + + GLFWwindow *window = static_cast<GLFWwindow*>(app.getWindow().getNativeWindow()); + ImGui_ImplGlfw_InitForOpenGL(window, true); + ImGui_ImplOpenGL3_Init("#version 330"); +} + + +void ImGuiLayer::onDetach() +{ + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); +} + +void ImGuiLayer::begin() +{ + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); +} + +void ImGuiLayer::end() +{ + ImGuiIO& io = ImGui::GetIO(); + Application& app = Application::get(); + io.DisplaySize = ImVec2((float)app.getWindow().getWidth(), (float)app.getWindow().getHeight()); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); +} + +void ImGuiLayer::onEvent(Event &event) +{ + +} + +bool ImGuiLayer::onMouseButtonPressed(Event &event) +{ + return false; +} diff --git a/GuiCore/ImGuiLayer.hpp b/GuiCore/ImGuiLayer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..1156f8a23c81309dfc33f4ff9f36e6b3acaaf6e3 --- /dev/null +++ b/GuiCore/ImGuiLayer.hpp @@ -0,0 +1,35 @@ +/// @file ImGuiLayer.hpp +/// @author Armin Co +/// + +#ifndef IM_GUI_LAYER_HPP +#define IM_GUI_LAYER_HPP + +#include "Layer.hpp" +#include "Event.hpp" + +namespace GuiCore +{ + + class ImGuiLayer : public Layer + { + public: + ImGuiLayer(); + virtual ~ImGuiLayer(); + + virtual void onAttach() override; + virtual void onDetach() override; + + void begin(); + void end(); + + virtual void onEvent(Event &event); + bool onMouseButtonPressed(Event &event); + + private: + float m_time = 0.0f; + }; + +} + +#endif \ No newline at end of file diff --git a/GuiCore/Input.cpp b/GuiCore/Input.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2f1e7a13d217a677798c21ec8f1cdf2ebb13d577 --- /dev/null +++ b/GuiCore/Input.cpp @@ -0,0 +1,48 @@ +/// @file Input.cpp +/// @author Armin Co +/// + +#include "Input.hpp" +#include "Application.hpp" + +#include <GLFW/glfw3.h> + +namespace GuiCore +{ + Input* Input::s_instance = new Input(); + + bool Input::isKeyPressed(int keyCode) + { + auto window = static_cast<GLFWwindow*>(Application::get().getWindow().getNativeWindow()); + auto state = glfwGetKey(window, keyCode); + return state == GLFW_PRESS || state == GLFW_REPEAT; + } + + bool Input::isMouseButtonPressed(int button) + { + auto window = static_cast<GLFWwindow*>(Application::get().getWindow().getNativeWindow()); + auto state = glfwGetMouseButton(window, button); + return state == GLFW_PRESS; + } + + std::pair<float, float> Input::getMousePosition() + { + auto window = static_cast<GLFWwindow*>(Application::get().getWindow().getNativeWindow()); + double xPos {0}; + double yPos {0}; + glfwGetCursorPos(window, &xPos, &yPos); + return {static_cast<float>(xPos), static_cast<float>(yPos)}; + } + + float Input::getMouseX() + { + auto[x, y] = getMousePosition(); + return x; + } + + float Input::getMouseY() + { + auto[x, y] = getMousePosition(); + return y; + } +} \ No newline at end of file diff --git a/GuiCore/Input.hpp b/GuiCore/Input.hpp new file mode 100644 index 0000000000000000000000000000000000000000..a311d20a84991950bf8cb7b0e90d8518c99c32d5 --- /dev/null +++ b/GuiCore/Input.hpp @@ -0,0 +1,66 @@ +/// @file Input.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_INPUT_HPP +#define GUI_CORE_INPUT_HPP + +#include <utility> + +namespace GuiCore +{ + class Input + { + protected: + Input() = default; + public: + Input(const Input &) = delete; + Input& operator=(const Input&) = delete; + + static bool isKeyPressed(int keyCode); + + static bool isMouseButtonPressed(int button); + static std::pair<float, float> getMousePosition(); + static float getMouseX(); + static float getMouseY(); + private: + static Input* s_instance; + }; +} + +namespace Key +{ + constexpr int A {65}; + constexpr int B {66}; + constexpr int C {67}; + constexpr int D {68}; + constexpr int E {69}; + constexpr int F {70}; + constexpr int G {71}; + constexpr int H {72}; + constexpr int I {73}; + constexpr int J {74}; + constexpr int K {75}; + constexpr int L {76}; + constexpr int M {77}; + constexpr int N {78}; + constexpr int O {79}; + constexpr int P {80}; + constexpr int Q {81}; + constexpr int R {82}; + constexpr int S {83}; + constexpr int T {84}; + constexpr int U {85}; + constexpr int V {86}; + constexpr int W {87}; + constexpr int X {88}; + constexpr int Z {89}; + constexpr int Y {90}; + + constexpr int Right {262}; + constexpr int Left {263}; + constexpr int Down {264}; + constexpr int Up {265}; +} + +#endif \ No newline at end of file diff --git a/GuiCore/Layer.cpp b/GuiCore/Layer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..152287f27ad277e5c9bde934bc0f923589e90f75 --- /dev/null +++ b/GuiCore/Layer.cpp @@ -0,0 +1,23 @@ +/// @file Layer.cpp +/// @author Armin Co +/// + +#include "Layer.hpp" + +namespace GuiCore +{ + Layer::Layer(const std::string &debugName) + : m_debugName{debugName} + { + } + + Layer::~Layer() + { + } + + const std::string &Layer::getName() const + { + return m_debugName; + } +} + diff --git a/GuiCore/Layer.hpp b/GuiCore/Layer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..73687a1cb8568c263caed48cb8b99a03066b7809 --- /dev/null +++ b/GuiCore/Layer.hpp @@ -0,0 +1,35 @@ +/// @file Layer.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_LAYER_HPP +#define GUI_CORE_LAYER_HPP + +#include <string> + +#include "Event.hpp" +#include "Timestep.hpp" + +namespace GuiCore +{ + class Layer + { + public: + Layer(const std::string &name = "Layer"); + virtual ~Layer(); + + virtual void onAttach() {} + virtual void onDetach() {} + virtual void onUpdate(Timestep ts) {} + virtual void onGuiRender() {} + virtual void onEvent(Event &event){} + + const std::string &getName() const; + + protected: + std::string m_debugName; + }; + +} + +#endif \ No newline at end of file diff --git a/GuiCore/LayerStack.cpp b/GuiCore/LayerStack.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8dd6c65241e8e7be9d2595ee613e5a51489cd9b9 --- /dev/null +++ b/GuiCore/LayerStack.cpp @@ -0,0 +1,70 @@ +/// @file LayerStack.cpp +/// @author Armin Co +/// + +#include <algorithm> + +#include "LayerStack.hpp" + +namespace GuiCore +{ + LayerStack::LayerStack() + { + } + + LayerStack::~LayerStack() + { + for (auto layer : m_layers) + { + layer->~Layer(); + } + } + + void LayerStack::pushLayer(std::shared_ptr<Layer> layer) + { + m_layers.emplace(lastLayer(), layer); + ++m_layerIndex; + layer->onAttach(); + } + + void LayerStack::popLayer(std::shared_ptr<Layer> layer) + { + auto it = std::find(firstLayer(), lastLayer(), layer); + if (it != lastLayer()) + { + layer->onDetach(); + m_layers.erase(it); + --m_layerIndex; + } + } + + Layers::iterator LayerStack::begin() + { + return m_layers.begin(); + } + + Layers::iterator LayerStack::end() + { + return m_layers.end(); + } + + Layers::iterator LayerStack::firstLayer() + { + return m_layers.begin(); + } + + Layers::iterator LayerStack::lastLayer() + { + return m_layers.begin() + m_layerIndex; + } + + Layers::reverse_iterator LayerStack::rbegin() + { + return m_layers.rbegin(); + } + + Layers::reverse_iterator LayerStack::rend() + { + return m_layers.rend(); + } +} diff --git a/GuiCore/LayerStack.hpp b/GuiCore/LayerStack.hpp new file mode 100644 index 0000000000000000000000000000000000000000..3e5bb14839777a7455594830343ac226771ad82f --- /dev/null +++ b/GuiCore/LayerStack.hpp @@ -0,0 +1,40 @@ +/// @file LayerStack.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_LAYERSTACK_HPP +#define GUI_CORE_LAYERSTACK_HPP + +#include <vector> +#include <memory> + +#include "Layer.hpp" + +namespace GuiCore +{ + using Layers = std::vector<std::shared_ptr<Layer>>; + + class LayerStack + { + public: + LayerStack(); + ~LayerStack(); + + void pushLayer(std::shared_ptr<Layer> layer); + void popLayer(std::shared_ptr<Layer> layer); + + Layers::iterator begin(); + Layers::iterator end(); + Layers::reverse_iterator rbegin(); + Layers::reverse_iterator rend(); + + private: + Layers::iterator firstLayer(); + Layers::iterator lastLayer(); + + Layers m_layers; + uint32_t m_layerIndex{0}; + }; +} + +#endif \ No newline at end of file diff --git a/GuiCore/Log.cpp b/GuiCore/Log.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d4482d30044eedb86a77a1c7a60f9bfada24d019 --- /dev/null +++ b/GuiCore/Log.cpp @@ -0,0 +1,28 @@ +/// @file Log.cpp +/// @author Armin Co +/// + +#include "spdlog/sinks/stdout_color_sinks.h" + +#include "Log.hpp" + +namespace GuiCore +{ + std::shared_ptr<spdlog::logger> Log::s_logger; + + void Log::setup() + { + spdlog::set_pattern("%^[%T] %n: %v%$"); + s_logger = spdlog::stdout_color_mt("GuiCore"); +#ifdef ENABLE_LOG_DEBUG_INFO + s_logger->set_level(spdlog::level::trace); +#else + s_logger->set_level(spdlog::level::info); +#endif + } + + std::shared_ptr<spdlog::logger> Log::get() + { + return s_logger; + } +} diff --git a/GuiCore/Log.hpp b/GuiCore/Log.hpp new file mode 100644 index 0000000000000000000000000000000000000000..f962214ff8524b2615a8e61988dbc71b92677123 --- /dev/null +++ b/GuiCore/Log.hpp @@ -0,0 +1,25 @@ +/// @file Log.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_LOG_HPP +#define GUI_CORE_LOG_HPP + +#include <memory> + +#include <spdlog/spdlog.h> + +namespace GuiCore +{ + class Log + { + public: + static void setup(); + static std::shared_ptr<spdlog::logger> get(); + + private: + static std::shared_ptr<spdlog::logger> s_logger; + }; +} + +#endif \ No newline at end of file diff --git a/GuiCore/Shader.cpp b/GuiCore/Shader.cpp new file mode 100644 index 0000000000000000000000000000000000000000..65d24ee9a302a0f0389467253cdc0c8cdedf4d5c --- /dev/null +++ b/GuiCore/Shader.cpp @@ -0,0 +1,119 @@ +/// @file Shader.cpp +/// @author Armin Co +/// + +#include <fstream> +#include <sstream> + +#include "Shader.hpp" +#include "Log.hpp" + + + +namespace GuiCore +{ + static std::string readFileAsString(const std::string &filepath) + { + std::string result; + std::ifstream in(filepath, std::ios::in); + if (in.is_open()) + { + std::stringstream ss; + ss << in.rdbuf(); + result = ss.str(); + } + else + { + Log::get()->trace("Shader::readFileAsString"); + Log::get()->error("Could not open file '{0}'", filepath); + } + return result; + } + + Shader::~Shader() + { + glDeleteProgram(m_rendererID); + } + + GLuint Shader::compileShader(GLenum type, const std::string &source) + { + GLuint shader = glCreateShader(type); + + const GLchar *sourceCStr = source.c_str(); + Log::get()->trace("Shader::compileShader"); + // Log::get()->debug("{0}", sourceCStr); + glShaderSource(shader, 1, &sourceCStr, nullptr); + + glCompileShader(shader); + + GLint isCompiled = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled); + if (isCompiled == GL_FALSE) + { + GLint maxLength = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength); + + std::vector<GLchar> infoLog(maxLength); + glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]); + + glDeleteShader(shader); + + Log::get()->trace("Shader::compileShader"); + Log::get()->error("{0}", infoLog.data()); + } + return shader; + } + + Shader* Shader::fromGLSLTextFiles(const std::string& vertexShaderPath, const std::string &fragmentShaderPath) + { + Shader *shader = new Shader(); + shader->loadFromGLSLTextFiles(vertexShaderPath, fragmentShaderPath); + return shader; + } + + void Shader::loadFromGLSLTextFiles(const std::string &vertexShaderPath, const std::string &fragmentShaderPath) + { + std::string vertexSource = readFileAsString(vertexShaderPath); + std::string fragmentSource = readFileAsString(fragmentShaderPath); + + GLuint program = glCreateProgram(); + + GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexSource); + glAttachShader(program, vertexShader); + GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentSource); + glAttachShader(program, fragmentShader); + + glLinkProgram(program); + + GLint isLinked = 0; + glGetProgramiv(program, GL_LINK_STATUS, (int*)&isLinked); + if (isLinked == GL_FALSE) + { + GLint maxLength = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength); + + std::vector<GLchar> infoLog(maxLength); + glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]); + + glDeleteProgram(program); + + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + + Log::get()->trace("Shader::loadFromGLSLTextFiles"); + Log::get()->error("{0}", infoLog.data()); + } + + glDetachShader(program, vertexShader); + glDetachShader(program, fragmentShader); + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + + m_rendererID = program; + } + + GLuint Shader::getRendererID() + { + return m_rendererID; + } +} diff --git a/GuiCore/Shader.hpp b/GuiCore/Shader.hpp new file mode 100644 index 0000000000000000000000000000000000000000..6963e4bab96674101d20f7eadd0f47084c9bf3b1 --- /dev/null +++ b/GuiCore/Shader.hpp @@ -0,0 +1,29 @@ +/// @file Shader.hpp +/// @author Armin Co +/// + +#ifndef OPEN_GL_SHADER_HPP +#define OPEN_GL_SHADER_HPP + +#include <string> +#include <GL/gl3w.h> +#include <GLFW/glfw3.h> +namespace GuiCore +{ + class Shader + { + public: + ~Shader(); + + GLuint getRendererID(); + static Shader* fromGLSLTextFiles(const std::string& vertexShaderPath, const std::string &fragmentShaderPath); + private: + Shader() = default; + void loadFromGLSLTextFiles(const std::string &vertexShaderPath, const std::string &fragmentShaderPath); + GLuint compileShader(GLenum type, const std::string &source); + private: + GLuint m_rendererID; + }; +} + +#endif \ No newline at end of file diff --git a/GuiCore/Timer.cpp b/GuiCore/Timer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4d070e37f1298a6ea57f3f5df6923652dbfd2cc2 --- /dev/null +++ b/GuiCore/Timer.cpp @@ -0,0 +1,34 @@ +/// @file Timer.cpp +/// @author Armin Co +/// + +#include "Timer.hpp" +#include "Log.hpp" + +using namespace GuiCore; + +Timer::Timer(const char* name) + : m_name{name} +{ + m_startTimepoint = std::chrono::high_resolution_clock::now(); +} + +Timer::~Timer() +{ + stop(); +} + +double Timer::stop() +{ + auto endTimepoint = std::chrono::high_resolution_clock::now(); + + auto start = std::chrono::time_point_cast<std::chrono::microseconds>(m_startTimepoint).time_since_epoch().count(); + auto end = std::chrono::time_point_cast<std::chrono::microseconds>(endTimepoint).time_since_epoch().count(); + + auto duration = end - start; + double ms = duration * 0.001; + + Log::get()->debug("{0}: {1}ms", m_name, ms); + + return ms; +} \ No newline at end of file diff --git a/GuiCore/Timer.hpp b/GuiCore/Timer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..5508fe1e0fce97eb160ef611a628203e29b61c75 --- /dev/null +++ b/GuiCore/Timer.hpp @@ -0,0 +1,32 @@ +/// @file Timer.hpp +/// @author Armmin Co +/// + +#ifndef GUI_CORE_TIMER_HPP +#define GUI_CORE_TIMER_HPP + +#include <chrono> + +namespace GuiCore +{ + +class Timer +{ +public: + /// @brief Start timer + /// + Timer(const char* name = "Timer"); + + /// @brief Stops timer. + ~Timer(); + + double stop(); +private: + std::chrono::time_point< std::chrono::high_resolution_clock > m_startTimepoint; + const char* m_name; +}; + +} + + +#endif \ No newline at end of file diff --git a/GuiCore/Timestep.cpp b/GuiCore/Timestep.cpp new file mode 100644 index 0000000000000000000000000000000000000000..aabd704e303cf320b44a8e9b18f06577c822cf3e --- /dev/null +++ b/GuiCore/Timestep.cpp @@ -0,0 +1,23 @@ +/// @file Timestep.cpp +/// @author Armin Co +/// + +#include "Timestep.hpp" + +namespace GuiCore +{ + Timestep::Timestep(float time) + : m_time {time} + { + } + + float Timestep::getSeconds() const + { + return m_time; + } + + float Timestep::getMilliseconds() const + { + return m_time * 1000.0f; + } +} \ No newline at end of file diff --git a/GuiCore/Timestep.hpp b/GuiCore/Timestep.hpp new file mode 100644 index 0000000000000000000000000000000000000000..6eb4053e3da5d9f2955fb80b523668613b8bba6e --- /dev/null +++ b/GuiCore/Timestep.hpp @@ -0,0 +1,23 @@ +/// @file Timestep.hpp +/// @author Armin Co +/// + +#ifndef GUI_CORE_TIMESTEP_HPP +#define GUI_CORE_TIMESTEP_HPP + +namespace GuiCore +{ + class Timestep + { + public: + Timestep(float time = 0.0f); + + float getSeconds() const; + float getMilliseconds() const; + + private: + float m_time; + }; +} + +#endif \ No newline at end of file diff --git a/QPongApp/CMakeLists.txt b/QPongApp/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..99d4a3cc077976f3b64dbce5d42dfc07e3bb778f --- /dev/null +++ b/QPongApp/CMakeLists.txt @@ -0,0 +1,25 @@ +add_executable( QPongApp + GameLayer.cpp + Log.cpp + MainMenuLayer.cpp + QPongApp.cpp + Window.cpp +) + +target_include_directories( QPongApp PUBLIC + . + .. + ../libs + ../libs/imgui/examples/libs/gl3w +) + +target_link_libraries( QPongApp PUBLIC + GuiCore + fftw3 + fftw3_omp +) + +add_custom_command(TARGET QPongApp + POST_BUILD + COMMAND ln -sf ${PROJECT_SOURCE_DIR}/assets ${CMAKE_BINARY_DIR}/QPongApp +) \ No newline at end of file diff --git a/QPongApp/GameLayer.cpp b/QPongApp/GameLayer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c2bcbd0b159d0bc854f923f88cb20023c94a8917 --- /dev/null +++ b/QPongApp/GameLayer.cpp @@ -0,0 +1,697 @@ +/// @file GameLayer.cpp +/// @author Armin Co +/// + +#include <GL/gl3w.h> +#include <GLFW/glfw3.h> + +#include <imgui/imgui.h> + +#include <glm/gtc/type_ptr.hpp> +#include <glm/gtc/matrix_transform.hpp> + +#include <GuiCore/Log.hpp> +#include <GuiCore/Input.hpp> +#include <GuiCore/Glyphs.hpp> +#include <GuiCore/Timer.hpp> + +#include "GameLayer.hpp" +#include "Log.hpp" + +#include <string> +#include <thread> + + + +double QPong::GameLayer::GameLayer::Bat::s_speedStepSize = 0.005; + +template <typename T> +const T pow2(const T v) +{ + return v * v; +} + +QPong::GameLayer::GameLayer::Bat::Bat(double x, double y, int keyUp, int keyDown) + : m_x{x} + , m_y{y} + , m_vx{0.0} + , m_vy{0.0} + , m_height{0.21} + , m_width{0.0} + , m_keyUp{keyUp} + , m_keyDown{keyDown} +{ + +} + +void QPong::GameLayer::GameLayer::Bat::onUpdate(GuiCore::Timestep ts) +{ + constexpr double speed = 20.0; + if (GuiCore::Input::isKeyPressed(m_keyDown)) + { + m_vy += s_speedStepSize * speed; + } + else if (GuiCore::Input::isKeyPressed(m_keyUp)) + { + m_vy -= s_speedStepSize * speed; + } + else + { + m_vy = 0.0; + } + + constexpr double maxSpeedFactor = 300.0; + if (m_vy > s_speedStepSize * maxSpeedFactor) + { + m_vy = s_speedStepSize * maxSpeedFactor; + } + if (m_vy < -s_speedStepSize * maxSpeedFactor) + { + m_vy = -s_speedStepSize * maxSpeedFactor; + } + m_y += (m_vy * ts.getSeconds()); + + constexpr double maxY {1.3}; + if (m_y > maxY) + { + m_y = maxY; + } + if (m_y < -maxY) + { + m_y = -maxY; + } +} + +double QPong::GameLayer::Bat::getPotential(double x, double y) const +{ + y -= m_y; + double dist = 0.0; + double scale = 0.0000001; + + if (y >= m_height) + { + dist = pow2(x - m_x) + pow2(y - m_height) + m_width; + } + else if (y <= (-m_height)) + { + dist = pow2(x - m_x) + pow2((-y) - m_height) + m_width; + } + else + { + dist = pow2(m_x - x) + m_width; + } + return 1.0 / (pow2(dist) ) * scale; +} + +double QPong::GameLayer::Bat::getX() const +{ + return m_x; +} + +QPong::GameLayer::GameLayer(QPong::GameLayer::Properties props) + :Layer("GameLayer") + , m_vertexShaderPath{"assets/shaders/particle.vert.glsl"} + , m_fragmentShaderPath{"assets/shaders/particle.frag.glsl"} + , m_arraySize{props.resolution} + , m_propagate{false} + , m_leftBat(-0.8, 0.0, Key::W, Key::S) + , m_rightBat(0.8, 0.0, Key::Up, Key::Down) + , m_speedScale{3.0f} + , m_randomMomentum {true} +{ + srand (time(NULL)); + m_arrayElements = m_arraySize * m_arraySize; + m_numberOfQuads = (m_arraySize - 1) * (m_arraySize - 1); + m_numberOfIndices = m_numberOfQuads * 6; + + m_initialParticleProperties.position.x = 0.0; + m_initialParticleProperties.position.y = 0.0; + m_initialParticleProperties.position.z = 0.0; + m_initialParticleProperties.momentum.x = 1.0; + m_initialParticleProperties.momentum.y = 0.5; + m_initialParticleProperties.momentum.z = 0.00; + m_initialParticleProperties.pointUnsharpness = 60.0; + m_initialParticleProperties.startValue = 0.01; + m_initialParticleProperties.hbar = 0.0002; +} + +void QPong::GameLayer::calculatePointPositions(float *positions) +{ + constexpr float size{3.175f}; + constexpr float xOffset{0.0f}; + constexpr float yOffset{-0.175f}; + + constexpr float width{size}; + constexpr float xMin{-width / 2.0}; + constexpr float xMax{width / 2.0}; + constexpr float height{width}; + constexpr float yMin{-height / 2.0}; + constexpr float yMax{(height / 2.0)}; + + float dx = (xMax - xMin) / (m_arraySize - 1.0f); // "spacing" + float dy = (yMax - yMin) / (m_arraySize - 1.0f); + + uint32_t i {0}; // array index + for (int yId{0}; yId < m_arraySize; ++yId) + { + for (int xId{0}; xId < m_arraySize; ++xId) + { + positions[i++] = xMin + xOffset + (static_cast<float>(xId) * dx); // x-Pos + positions[i++] = yMax + yOffset - (static_cast<float>(yId) * dy); // y-Pos + positions[i++] = 0.0f; // z-Pos + } + } +} + +void QPong::GameLayer::calculateMomentumViewPositions(float *positions) +{ + constexpr float xOffset {1.5f}; // has to be the same as in the shader + float xMin {2.2f + xOffset}; + float xMax {0.2f + xOffset}; + float yMin {-1.0f}; + float yMax {1.0f}; + float dx = (xMax - xMin) / (m_arraySize - 1.0f); // "spacing" + float dy = (yMax - yMin) / (m_arraySize - 1.0f); + { + uint32_t i {0}; // array index + for (int yId{0}; yId < m_arraySize; ++yId) + { + for (int xId{0}; xId < m_arraySize; ++xId) + { + positions[i++] = xMin + (static_cast<float>(xId) * dx); // x-Pos + positions[i++] = yMax - (static_cast<float>(yId) * dy); // y-Pos + positions[i++] = 0.0f; // z-Pos + } + } + } +} + +void QPong::GameLayer::calculateTriangleIndices(uint32_t *indices) +{ + uint32_t i{0}; + for (int row {0}; row < (m_arraySize - 1); ++row) + { + for (int k {0}; k < (m_arraySize - 1); ++k) + { + int offset = row * m_arraySize; + indices[i++] = offset + k; + indices[i++] = offset + k + 1; + indices[i++] = offset + k + m_arraySize; + indices[i++] = offset + k + 1; + indices[i++] = offset + k + m_arraySize; + indices[i++] = offset + k + m_arraySize + 1; + } + } +} + +void QPong::GameLayer::calculateBorderPotential(float *potential) +{ + double y {0.0}; + for (int i {0}; i < m_arrayElements; ++i) + { + y = m_yAt[i]; + float pot = pow2(y) * pow2(y) * pow2(y) * pow2(y) * pow2(y) * pow2(y) * pow2(y) * 0.02; + potential[i] = pot; + } +} + +double QPong::GameLayer::posAt(int pos) const +{ + return 2.0 * pos / m_arraySize - 1.0; +} + +double QPong::GameLayer::xAtIndex(int i) const +{ + return posAt(i % m_arraySize); +} + +double QPong::GameLayer::yAtIndex(int i) const +{ + return posAt(i / m_arraySize); +} + + +double QPong::GameLayer::potentialAt(int pos) const +{ + if (pos > m_arraySize / 2) + { + pos -= m_arraySize; + } + return pos * M_PI * m_initialParticleProperties.hbar; +} + +double QPong::GameLayer::pxAtIndex(int i) const +{ + return potentialAt(i % m_arraySize); +} + +double QPong::GameLayer::pyAtIndex(int i) const +{ + return potentialAt(i / m_arraySize); +} + + + +void QPong::GameLayer::initialiseParticleMomentum() +{ + constexpr std::complex<double> ci {0.0, 1.0}; // complex number + const double hbar = m_initialParticleProperties.hbar; + const double A0 = m_initialParticleProperties.startValue; + const double px0 = m_initialParticleProperties.momentum.x / 20.0; + const double py0 = m_initialParticleProperties.momentum.y / 20.0; + const double x0 = m_initialParticleProperties.position.x; + const double y0 = m_initialParticleProperties.position.y; + const double lambda = m_initialParticleProperties.pointUnsharpness; + + double sum {0.0}; + m_particleAbsouluteSumStart = 0.0; + double y{0.0}; + double x{0.0}; + for (auto i {0}; i < m_arrayElements; ++i) + { + x = m_xAt[i]; + y = m_yAt[i]; + m_psi[i] = A0 * exp( ci / hbar * ( x * px0 + y * py0) - lambda * (pow2(x-x0) + pow2(y-y0))); + sum += pow2(std::real(m_psi[i])) + pow2(std::imag(m_psi[i])); + } + for (auto i {0}; i < m_arrayElements; ++i) + { + m_psi[i] *= (10.0 * (m_arraySize / 512.0)) / sqrt(sum); + m_particleAbsouluteSumStart += pow2(std::real(m_psi[i])) + pow2(std::imag(m_psi[i])); + } + Log::get()->trace("Particle initialised"); +} + +double QPong::GameLayer::absorbParticle(int i, double c) +{ + double normPrevious = pow2(std::real(m_psi[i])) + pow2(std::imag(m_psi[i])); + double absorb = pow2(std::cos(c * 10)); + m_psi[i] *= absorb; + double delta = normPrevious - (pow2(std::real(m_psi[i])) + pow2(std::imag(m_psi[i]))); + return delta; +} + +void QPong::GameLayer::applyPotentials(int indexBegin, int indexEnd, double dt) +{ + constexpr std::complex<double> ci {0.0, 1.0}; + const double hbar = m_initialParticleProperties.hbar; + double x {0.0}; + double y {0.0}; + const double potential = 1.0; + for (int i {indexBegin}; i < indexEnd; ++i) + { + double potSum = 1.0; + if (m_potentialsEnabled) + { + potSum = 0.0; + x = m_xAt[i]; + y = m_yAt[i]; + if (m_removeParticleEnabled) + { + if (x < m_leftBat.getX()) + { + double distanceToEdge = -m_leftBat.getX() + x; + double c = distanceToEdge * (M_PI / 2.0) / (1.0 + m_leftBat.getX()); + m_playerTwo.currentlyScored += absorbParticle(i, c); + } + if (x>m_rightBat.getX()) + { + double distanceToEdge = x - m_rightBat.getX(); + double c = distanceToEdge * (M_PI / 2.0) / (1.0 + m_rightBat.getX()); + m_playerOne.currentlyScored += absorbParticle(i, c); + } + } + double potLeftBat = m_leftBat.getPotential(x, y); + double potRightBat = m_rightBat.getPotential(x, y); + potSum = potLeftBat + potRightBat + m_staticBorderPotential[i]; + } + auto psi = m_psi[i] * exp(-ci * dt/hbar * potential * potSum); + m_psi[i] = psi; + if (m_potentialsEnabled) + { + m_dynamicPotential[i] = potSum; + } + } +} + +void QPong::GameLayer::moveForward(int indexBegin, int indexEnd, double dt) +{ + constexpr std::complex<double> ci {0.0, 1.0}; + const double hbar = m_initialParticleProperties.hbar; + double px {0.0}; + double py {0.0}; + constexpr double m = 1.0; + const double N = 1.0 / static_cast<double>(m_arrayElements); + for (int i{indexBegin}; i<indexEnd; ++i) + { + px = m_pxAt[i]; + py = m_pyAt[i]; + double E = (pow2(px) + pow2(py)) / (2.0 * m); + m_psi[i] *= exp(-ci * dt / hbar * E) * N; + } +} + +void QPong::GameLayer::updateMomentumView(int indexBegin, int indexEnd) +{ + int x {0}; + int y {0}; + for (int i {indexBegin}; i< indexEnd; ++i) + { + x = i % m_arraySize; + y = i / m_arraySize; + auto halfArraySize = m_arraySize / 2; + + if (y > halfArraySize) + { + y = y - halfArraySize; + } + else + { + y = y + halfArraySize; + } + + if (x > halfArraySize) + { + x = x - halfArraySize; + } + else + { + x = x + halfArraySize; + } + m_momentum[i] = m_psi[(y * m_arraySize )- x] * 100.0; + } +} + +void QPong::GameLayer::propagateStep(double dt) +{ + applyPotentials(0, m_arrayElements, dt); + + { + GuiCore::Timer t("fftw_execute(forward)"); + fftw_execute(m_planForward); + } + + moveForward(0, m_arrayElements, dt); + + updateMomentumView(0, m_arrayElements); + { + GuiCore::Timer t("fftw_execute(backwards"); + fftw_execute(m_planBackwards); + } +} + +void QPong::GameLayer::onAttach() +{ + GuiCore::Timer timer("GameLayer::onAttach()"); + GuiCore::Glyphs::setup(); + + m_xAt = new double[m_arrayElements]; + m_yAt = new double[m_arrayElements]; + m_pxAt = new double[m_arrayElements]; + m_pyAt = new double[m_arrayElements]; + + for (int i {0}; i < m_arrayElements; ++i) + { + m_xAt[i] = xAtIndex(i); + m_yAt[i] = yAtIndex(i); + m_pxAt[i] = pxAtIndex(i); + m_pyAt[i] = pyAtIndex(i); + } + + m_renderMomentum = false; + m_removeParticleEnabled = true; + m_potentialsEnabled = true; + m_playerOne.totalScore = 0; + m_playerTwo.totalScore = 0; + + m_shader = GuiCore::Shader::fromGLSLTextFiles( + m_vertexShaderPath, + m_fragmentShaderPath + ); + + // static verticie positions + m_particleViewPositions = new float[m_arrayElements * 3]; + calculatePointPositions(m_particleViewPositions); + + // Vertex array to store all buffers + glGenVertexArrays(1, &m_vertexArray); + glBindVertexArray(m_vertexArray); + + // Buffer of all point locations + glGenBuffers(1, &m_pointsBuffer); + glBindBuffer(GL_ARRAY_BUFFER, m_pointsBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * (m_arrayElements), m_particleViewPositions, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, 0); + + // Setup indies buffer + uint32_t *indices = new uint32_t[m_numberOfIndices]; + calculateTriangleIndices(indices); + + glGenBuffers(1, &m_indicesBuffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indicesBuffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint32_t) * m_numberOfIndices, indices, GL_STATIC_DRAW); + delete(indices); + + // Setup particle buffer + m_memorySize = sizeof(fftw_complex) * m_arrayElements; + m_psi = static_cast<std::complex<double>*>(fftw_malloc(m_memorySize)); + m_momentum = static_cast<std::complex<double>*>(fftw_malloc(m_memorySize)); + + glGenBuffers(1, &m_colorBuffer); + glBindBuffer(GL_ARRAY_BUFFER, m_colorBuffer); + glBufferData(GL_ARRAY_BUFFER, m_memorySize, nullptr, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_DOUBLE, GL_FALSE, sizeof(fftw_complex), 0); + + // Setup momentum view + m_momentumViewPositions = new float[m_arrayElements * 3]; + calculateMomentumViewPositions(m_momentumViewPositions); + + // Initialise Borders + m_staticBorderPotential = new float[m_arrayElements]; + calculateBorderPotential(m_staticBorderPotential); + m_dynamicPotential = new float[m_arrayElements]; + glGenBuffers(1, &m_potentialBuffer); + glBindBuffer(GL_ARRAY_BUFFER, m_potentialBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * m_arrayElements, m_staticBorderPotential, GL_STATIC_DRAW); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, sizeof(float), 0); + + // Set orthografic view projectin "camera" which will not change. + m_viewProjectionPosition = glm::ortho(-3.2f, 3.2f, -1.8f, 1.8f, -1.0f, 1.0f); + m_viewProjectionLocation = glGetUniformLocation(m_shader->getRendererID(), "u_viewProjection"); + m_showPotentialsLocation = glGetUniformLocation(m_shader->getRendererID(), "u_showPotential"); + m_isMomentumEnabled = glGetUniformLocation(m_shader->getRendererID(), "u_isMomentumEnabled"); + + fftw_init_threads(); + m_planForward = fftw_plan_dft_2d(m_arraySize, m_arraySize, (fftw_complex *)m_psi, (fftw_complex *)m_psi, FFTW_FORWARD, FFTW_MEASURE); + m_planBackwards = fftw_plan_dft_2d(m_arraySize, m_arraySize, (fftw_complex *)m_psi, (fftw_complex *)m_psi, FFTW_BACKWARD, FFTW_MEASURE); + + reset(); +} + +void QPong::GameLayer::onDetach() +{ + glDeleteVertexArrays(1, &m_vertexArray); + glDeleteBuffers(1, &m_pointsBuffer); + glDeleteBuffers(1, &m_indicesBuffer); + glDeleteBuffers(1, &m_colorBuffer); + fftw_free(m_psi); + delete(m_momentum); + delete(m_dynamicPotential); + delete(m_staticBorderPotential); + delete(m_xAt); + delete(m_yAt); + delete(m_pxAt); + delete(m_pyAt); +} + +void QPong::GameLayer::onEvent(GuiCore::Event& event) +{ +} + +void QPong::GameLayer::reset() +{ + if (m_randomMomentum) + { + int xRand = rand() % 100; + int yRand = rand() % 100; + int positive = rand() % 2; + m_initialParticleProperties.momentum.x = (1.0f - (2.0f * positive)) * (1.0 + (float)xRand / 120.0f); + positive = rand() % 2; + m_initialParticleProperties.momentum.y = (1.0f - (2.0f * positive)) * (1.0 + (float)yRand / 120.0f); + } + initialiseParticleMomentum(); + m_restart = false; + m_gameCounts = true; + m_playerOne.currentlyScored = 0.0; + m_playerTwo.currentlyScored = 0.0; + +} + +void QPong::GameLayer::onUpdate(GuiCore::Timestep ts) +{ + if (m_restart) + { + reset(); + } + if (m_propagate) + { + m_leftBat.onUpdate(ts); + m_rightBat.onUpdate(ts); + propagateStep(ts.getSeconds() * m_speedScale); + + if (m_gameCounts) + { + if (m_playerOne.currentlyScored > 35.0) + { + m_playerOne.totalScore++; + m_gameCounts = false; + m_propagate = false; + } + if (m_playerTwo.currentlyScored > 35.0) + { + m_playerTwo.totalScore++; + m_gameCounts = false; + m_propagate = false; + } + } + } + + // Particle + glBindBuffer(GL_ARRAY_BUFFER, m_pointsBuffer); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * m_arrayElements * 3, m_particleViewPositions); + + glBindBuffer(GL_ARRAY_BUFFER, m_colorBuffer); + glBufferSubData(GL_ARRAY_BUFFER, 0, m_memorySize, m_psi); + + glBindBuffer(GL_ARRAY_BUFFER, m_potentialBuffer); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * m_arrayElements, m_dynamicPotential); + + glUseProgram(m_shader->getRendererID()); + glUniform1i(m_showPotentialsLocation, static_cast<int>(m_potentialsEnabled)); + glUniform1i(m_isMomentumEnabled, static_cast<int>(m_renderMomentum)); + glUniformMatrix4fv(m_viewProjectionLocation, 1, GL_FALSE, &m_viewProjectionPosition[0][0]); + glBindVertexArray(m_vertexArray); + glDrawElements(GL_TRIANGLES, m_numberOfIndices, GL_UNSIGNED_INT, nullptr); + + // Momentum + if (m_renderMomentum) + { + glBindBuffer(GL_ARRAY_BUFFER, m_pointsBuffer); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * m_arrayElements * 3, m_momentumViewPositions); + + glBindBuffer(GL_ARRAY_BUFFER, m_colorBuffer); + glBufferSubData(GL_ARRAY_BUFFER, 0, m_memorySize, m_momentum); + + glUseProgram(m_shader->getRendererID()); + glUniform1i(m_showPotentialsLocation, 0); + glBindVertexArray(m_vertexArray); + glDrawElements(GL_TRIANGLES, m_numberOfIndices, GL_UNSIGNED_INT, nullptr); + } + + std::stringstream totalScore; + totalScore << m_playerOne.totalScore << " : " << m_playerTwo.totalScore; + GuiCore::Glyphs::renderText(totalScore.str(), -0.32f, 1.45f, 0.0075f, {1.0, 1.0, 1.0}); +} + +void QPong::GameLayer::onGuiRender() +{ + // GUI settings for the particle + ImGui::Begin("Particle Settings"); + + // Select initial momentum for particle at restart + ImGui::DragFloat2("Momentum [x,y]", glm::value_ptr(m_initialParticleProperties.momentum), 0.01f, -3.0f, 3.0f); + ImGui::Checkbox("Random Momentum", &m_randomMomentum); + + float pointUnsharpness = m_initialParticleProperties.pointUnsharpness; + if (ImGui::DragFloat("Sharpness", &pointUnsharpness, 1.0f, 5.0f, 1000.0f)) + { + m_initialParticleProperties.pointUnsharpness = pointUnsharpness; + } + // Select hbar, changes apply on the fly. + float hbar = m_initialParticleProperties.hbar; + if (ImGui::DragFloat("hbar", &hbar, 0.000001f, 0.000001f, 1.0f, "%.6f")) + { + m_initialParticleProperties.hbar = hbar; + } + + // This value increases or decreases the time step made every iteration. + ImGui::DragFloat("Speed Factor", &m_speedScale, 0.1, 0.1, 20.0); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Change the size of the timestep (dt)\nfor every iteration of the simulation."); + } + + // Decrease the + if (ImGui::Button("Less threads")) + { + if (m_renderingThreadsNumber > 1) + { + bool isPowerOf2 = false; + while (!isPowerOf2) + { + m_renderingThreadsNumber--; + for (int i = 0; i < 10; i++) + { + if (m_renderingThreadsNumber == pow(2, i)) + { + isPowerOf2 = true; + break; + } + } + } + } + } + ImGui::SameLine(); + if (ImGui::Button("More Threads")) + { + if (m_renderingThreadsNumber < 200) + { + bool isPowerOf2 = false; + while (!isPowerOf2) + { + m_renderingThreadsNumber++; + for (int i = 0; i < 10; i++) + { + if (m_renderingThreadsNumber == pow(2, i)) + { + isPowerOf2 = true; + break; + } + } + } + } + } + ImGui::SameLine(); + ImGui::Text("%d", m_renderingThreadsNumber); + ImGui::Checkbox("Render Momentum", &m_renderMomentum); + ImGui::Checkbox("Enable Potentials", &m_potentialsEnabled); + ImGui::Checkbox("Enable Particle absorption", &m_removeParticleEnabled); + // Start/Stop propagating + if (m_propagate) + { + if (ImGui::Button("Pause")) + { + m_propagate = false; + } + } + else + { + if (ImGui::Button("Play")) + { + m_propagate = true; + } + } + // Restart game + if (ImGui::Button("Restart")) + { + m_restart = true; + } + ImGui::Text("Player 1: %d", m_playerOne.totalScore); ImGui::SameLine(); + ImGui::Text(" | Live Score: %.1f\%%", m_playerOne.currentlyScored); + + ImGui::Text("Player 2: %d", m_playerTwo.totalScore); ImGui::SameLine(); + ImGui::Text(" | Live Score: %.1f\%%", m_playerTwo.currentlyScored); + ImGui::End(); +} \ No newline at end of file diff --git a/QPongApp/GameLayer.hpp b/QPongApp/GameLayer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..296c08a7e18504fa4a4746a6353ab5fdfdd11fef --- /dev/null +++ b/QPongApp/GameLayer.hpp @@ -0,0 +1,146 @@ +/// @file GameLayer.hpp +/// @author Armin Co +/// + +#ifndef QPONG_GAME_LAYER_HPP +#define QPONG_GAME_LAYER_HPP + +#include <glm/glm.hpp> + +#include <GuiCore/Layer.hpp> +#include <GuiCore/Shader.hpp> +#include <GuiCore/Timestep.hpp> + +#include <complex> +#include <fftw3.h> + +namespace QPong +{ +class GameLayer : public GuiCore::Layer +{ +public: + struct Properties + { + int resolution; + Properties(int res) : resolution {res}{}; + }; + + struct ParticleProps + { + glm::vec3 position; + glm::vec3 momentum; + double pointUnsharpness; + double startValue; + double hbar; + }; + + struct Player + { + double currentlyScored; + int totalScore; + }; + + class Bat + { + public: + Bat(double x, double y, int keyUp, int keyDown); + double getX() const; + double getY() const; + void onUpdate(GuiCore::Timestep ts); + double getPotential(double x, double y) const; + private: + double m_x; + double m_y; + double m_vx; + double m_vy; + double m_height; + double m_width; + int m_keyUp; + int m_keyDown; + static double s_speedStepSize; + }; + + GameLayer(Properties props); + virtual ~GameLayer(){}; + virtual void onAttach() override; + virtual void onDetach() override; + virtual void onUpdate(GuiCore::Timestep ts) override; + virtual void onGuiRender() override; + virtual void onEvent(GuiCore::Event &event) override; + +private: + void reset(); + + void calculatePointPositions(float *points); + void calculateMomentumViewPositions(float *points); + void calculateTriangleIndices(uint32_t *indices); + void calculateBorderPotential(float *potential); + void initialiseParticleMomentum(); + void propagateStep(double dt); + void applyPotentials(int indexBegin, int indexEnd, double dt); + void moveForward(int indexBegin, int indexEnd, double dt); + void updateMomentumView(int indexBegin, int indexEnd); + double absorbParticle(int i, double c); + // positions at array index + inline double posAt(int pos) const; + inline double xAtIndex(int i) const; + inline double yAtIndex(int i) const; + inline double potentialAt(int pos) const; + inline double pxAtIndex(int i) const; + inline double pyAtIndex(int i) const; + + GuiCore::Shader *m_shader; + std::string m_vertexShaderPath; + std::string m_fragmentShaderPath; + + int m_arraySize; + int m_arrayElements; + int m_numberOfQuads; + int m_numberOfIndices; + + GLuint m_vertexArray; ///< OpenGL object that "knows" all buffers. + GLuint m_pointsBuffer; ///< Buffer with the location for each element in the particle array + GLuint m_indicesBuffer; ///< Indicies to draw trinagles from the points to fill the "canvas" + GLuint m_colorBuffer; ///< Reference for OpenGL to load the paritcle data to the GPU + GLuint m_potentialBuffer; ///< Borders and bats + + float *m_particleViewPositions; + float *m_momentumViewPositions; + float *m_staticBorderPotential; + float *m_dynamicPotential; + + double *m_xAt; + double *m_yAt; + double *m_pxAt; + double *m_pyAt; + + glm::mat4 m_viewProjectionPosition; + int m_viewProjectionLocation; + GLuint m_showPotentialsLocation; + GLuint m_isMomentumEnabled; + + ParticleProps m_initialParticleProperties; + double m_particleAbsouluteSumStart; + fftw_plan m_planForward; + fftw_plan m_planBackwards; + + Bat m_leftBat; + Bat m_rightBat; + Player m_playerOne; + Player m_playerTwo; + bool m_gameCounts; + + bool m_restart; + bool m_propagate; + bool m_renderMomentum; + bool m_randomMomentum; + bool m_removeParticleEnabled; + bool m_potentialsEnabled; + int m_renderingThreadsNumber {8}; + size_t m_memorySize {0}; + float m_speedScale {1.0f}; + std::complex<double> *m_psi; + std::complex<double> *m_momentum; +}; +} +#endif \ No newline at end of file diff --git a/QPongApp/Log.cpp b/QPongApp/Log.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f410f33ea88a41311f9e1f8925c3068c1958e071 --- /dev/null +++ b/QPongApp/Log.cpp @@ -0,0 +1,23 @@ +/// @file Log.cpp +/// @author Armin Co +/// + +#include "Log.hpp" + +#include <spdlog/sinks/stdout_color_sinks.h> + +using namespace QPong; + +std::shared_ptr<spdlog::logger> Log::s_logger; + +void Log::setup() +{ + spdlog::set_pattern("%^[%T] %n: %v%$"); + s_logger = spdlog::stdout_color_mt("QPong"); + s_logger->set_level(spdlog::level::info); +} + +std::shared_ptr<spdlog::logger> Log::get() +{ + return s_logger; +} diff --git a/QPongApp/Log.hpp b/QPongApp/Log.hpp new file mode 100644 index 0000000000000000000000000000000000000000..5a196a1f3a74ebb969c72c55060670de1f68c841 --- /dev/null +++ b/QPongApp/Log.hpp @@ -0,0 +1,25 @@ +/// @file Log.hpp +/// @author Armin Co +/// + +#ifndef QPONG_LOG_HPP +#define QPONG_LOG_HPP + +#include <memory> + +#include <spdlog/spdlog.h> + +namespace QPong +{ + class Log + { + public: + static void setup(); + static std::shared_ptr<spdlog::logger> get(); + + private: + static std::shared_ptr<spdlog::logger> s_logger; + }; +} + +#endif \ No newline at end of file diff --git a/QPongApp/MainMenuLayer.cpp b/QPongApp/MainMenuLayer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..75bd232c009e55911b465768543f3b3f2bd7c55d --- /dev/null +++ b/QPongApp/MainMenuLayer.cpp @@ -0,0 +1,136 @@ +/// @file MainMenuLayer.cpp +/// @author Armin Co +/// + +#include "MainMenuLayer.hpp" + +#include <imgui/imgui.h> +#include <GuiCore/Application.hpp> + +#include "GameLayer.hpp" +#include <glm/gtc/type_ptr.hpp> + +using namespace QPong; + +MainMenuLayer::MainMenuLayer() + : m_selectedResolution {512} + , m_powOfResolution {9} + , m_forceIndividualResolution {false} + , m_resetOnce {false} + , m_gameAttached {false} +{ +} + +void MainMenuLayer::onAttach() +{ + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + m_backgroundColor = {0.2f, 0.2f, 0.2f}; + createNewGame(); +} + +void MainMenuLayer::onDetach() +{ + +} + +void MainMenuLayer::createNewGame() +{ + m_resetOnce = false; + if (m_gameAttached) + { + GuiCore::Application::get().popLayer(m_gameLayer); + m_gameAttached = false; + } + m_gameLayer = std::make_shared<GameLayer>(m_selectedResolution); + GuiCore::Application::get().pushLayer(m_gameLayer); + m_gameAttached = true; +} + +void MainMenuLayer::onUpdate(GuiCore::Timestep ts) +{ + m_fps = 1000.0f / ts.getMilliseconds(); + glClearColor(m_backgroundColor.r, m_backgroundColor.g, m_backgroundColor.b, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + if (m_resetOnce) + { + createNewGame(); + } +} + +#include <math.h> + +void MainMenuLayer::guiShowFPS() +{ + ImGui::Text("FPS: %3.1f", m_fps); +} + +void MainMenuLayer::guiSelectResolution() +{ + constexpr int minPowOfResolution {4}; + constexpr int maxPowOfResolution {12}; + + if (!m_forceIndividualResolution) + { + std::stringstream ssRes; + ssRes << "Array resoultion: "; + ssRes << m_selectedResolution << "x" << m_selectedResolution << std::endl; + ImGui::Text(ssRes.str().c_str()); + + if (ImGui::Button("Lower Resolution")) + { + m_powOfResolution--; + } + ImGui::SameLine(); + if (ImGui::Button("Higher Resolution")) + { + m_powOfResolution++; + } + m_powOfResolution = (m_powOfResolution < minPowOfResolution) ? minPowOfResolution : m_powOfResolution; + m_powOfResolution = (m_powOfResolution > maxPowOfResolution) ? maxPowOfResolution : m_powOfResolution; + + m_selectedResolution = std::pow<int>(2, m_powOfResolution); + } + else + { + constexpr int maxResolution = std::pow<int>(2, maxPowOfResolution); + ImGui::DragInt("Forced Resolution", &m_selectedResolution, 1, 32, maxResolution); + } + ImGui::Checkbox("Force other resolution", &m_forceIndividualResolution); + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Warning!\nSelecting other resoultions can decrease the performance significantly."); + } +} + +void MainMenuLayer::guiResetGame() +{ + if (ImGui::Button("Reset Game")){ m_resetOnce = true; } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Reinitialise Simulation with selected array resolution."); + } +} + + +void MainMenuLayer::onGuiRender() +{ + // Main menu controls and info + ImGui::Begin("Main Menu"); + + guiShowFPS(); + guiSelectResolution(); + guiResetGame(); + + ImGui::ColorEdit3("Background Color", glm::value_ptr(m_backgroundColor)); + + ImGui::End(); +} + +void MainMenuLayer::onEvent(GuiCore::Event &event) +{ + +} diff --git a/QPongApp/MainMenuLayer.hpp b/QPongApp/MainMenuLayer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..4105159e42d4c6abc8c553d854b35d61df78e97a --- /dev/null +++ b/QPongApp/MainMenuLayer.hpp @@ -0,0 +1,48 @@ +/// @file MainMenuLayer.hpp +/// @author Armin Co +/// + +#ifndef QPONG_MAIN_MENU_LAYER_HPP +#define QPONG_MAIN_MENU_LAYER_HPP + +#include <GuiCore/Layer.hpp> +#include <GuiCore/Timestep.hpp> + +#include <glm/glm.hpp> + +namespace QPong +{ +/// @brief The main menu holds the controls to restart the game +/// with different simulation settings. +/// +class MainMenuLayer : public GuiCore::Layer +{ +public: + MainMenuLayer(); + virtual ~MainMenuLayer(){}; + virtual void onAttach() override; + virtual void onDetach() override; + virtual void onUpdate(GuiCore::Timestep ts) override; + virtual void onGuiRender() override; + virtual void onEvent(GuiCore::Event &event) override; + +private: + void createNewGame(); + + void guiShowFPS(); + void guiSelectResolution(); + void guiResetGame(); + + // Resolution + int m_powOfResolution; ///< choose only from resolutions with 2^pow + int m_selectedResolution; ///< selected res in the gui + bool m_forceIndividualResolution; ///< Override resoultion + + bool m_resetOnce; ///< restart simulation if set + std::shared_ptr<GuiCore::Layer> m_gameLayer; ///< holds ref to the game + bool m_gameAttached; + float m_fps; + glm::vec3 m_backgroundColor; +}; +} +#endif \ No newline at end of file diff --git a/QPongApp/QPongApp.cpp b/QPongApp/QPongApp.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1c6a6b70c6021564763b072a72aa860f07b5dc89 --- /dev/null +++ b/QPongApp/QPongApp.cpp @@ -0,0 +1,31 @@ +/// @file QPongApp.cpp +/// @author Armin Co +/// + +#include <memory> + +#include <GuiCore/Application.hpp> + +#include "MainMenuLayer.hpp" +#include "GameLayer.hpp" +#include "Log.hpp" +class App : public GuiCore::Application +{ +public: + App() + : Application("QPong Application") + { + QPong::Log::setup(); + pushLayer(std::make_shared<QPong::MainMenuLayer>()); + } +}; + +int main() +{ + std::unique_ptr<App> app = std::make_unique<App>(); + + QPong::Log::get()->info("Have fun!"); + + app->run(); + return 0; +} \ No newline at end of file diff --git a/QPongApp/Window.cpp b/QPongApp/Window.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5e20adb3de76a10731a4bb297efff1a9f683eadf --- /dev/null +++ b/QPongApp/Window.cpp @@ -0,0 +1,128 @@ +/// @file Window.cpp +/// @author Armin Co +/// + + +#include <GL/gl3w.h> +#include <GuiCore/Log.hpp> +#include <GuiCore/ApplicationEvent.hpp> + +#include "Window.hpp" + + +static bool s_GLFW_Initialized = false; + +static void GLFWErrorCallback(int error, const char *description) +{ + /// @todo add GuiCore::Logger +} + +GuiCore::IWindow* GuiCore::IWindow::create(const WindowProperties &properties) +{ + return new Window(properties); +} + +Window::Window(const GuiCore::WindowProperties &properties) +{ + setup(properties); +} + +Window::~Window() +{ + shutdown(); +} + +void Window::setup(const GuiCore::WindowProperties &properties) +{ + m_data.title = properties.title; + m_data.width = properties.width; + m_data.height = properties.height; + + if (!s_GLFW_Initialized) + { + glfwSetErrorCallback(GLFWErrorCallback); + auto success = glfwInit(); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + + s_GLFW_Initialized = true; + } + + m_window = glfwCreateWindow(properties.width, properties.height, m_data.title.c_str(), nullptr, nullptr); + + glfwMakeContextCurrent(m_window); + glfwSetWindowUserPointer(m_window, &m_data); + + setVSync(true); + + gl3wInit(); + + glfwSetWindowSizeCallback(m_window, [](GLFWwindow * window, int width, int height) + { + Data &data = *(Data*)glfwGetWindowUserPointer(window); + data.width = width; + data.height = height; + + GuiCore::WindowResizeEvent event(width, height); + data.eventCallback(event); + }); + + using namespace GuiCore; + glfwSetWindowCloseCallback(m_window, [](GLFWwindow* window) + { + Data &data = *(Data*)glfwGetWindowUserPointer(window); + WindowCloseEvent event; + data.eventCallback(event); + }); +} + +void Window::shutdown() +{ + glfwDestroyWindow(m_window); +} + +void Window::onUpdate() +{ + glfwPollEvents(); + glfwSwapBuffers(m_window); +} + +void Window::setVSync(bool enabled) +{ + if (enabled) + { + glfwSwapInterval(1); + } + else + { + glfwSwapInterval(0); + } + m_data.vSyncEnabled = enabled; +} + +bool Window::isVSyncEnabled() const +{ + return m_data.vSyncEnabled; +} + +uint32_t Window::getWidth() const +{ + return m_data.width; +} + +uint32_t Window::getHeight() const +{ + return m_data.height; +} + +void* Window::getNativeWindow() const +{ + return m_window; +} + +void Window::setEventCallback(const std::function<void(GuiCore::Event&)> callback) +{ + GuiCore::Log::get()->trace("Window::setEventCallback"); + m_data.eventCallback = callback; +} \ No newline at end of file diff --git a/QPongApp/Window.hpp b/QPongApp/Window.hpp new file mode 100644 index 0000000000000000000000000000000000000000..8935dc4bf86327900adc4f535b39fdcc09c3afc8 --- /dev/null +++ b/QPongApp/Window.hpp @@ -0,0 +1,48 @@ +/// @file Window.hpp +/// @author Armin Co +/// + +#ifndef QPONG_APP_WINDOW_HPP +#define QPONG_APP_WINDOW_HPP + + +#include <GLFW/glfw3.h> +#include <GuiCore/IWindow.hpp> + +#include <GuiCore/Event.hpp> + +class Window : public GuiCore::IWindow +{ +public: + Window(const GuiCore::WindowProperties &properties); + virtual ~Window(); + + void onUpdate() override; + + uint32_t getWidth() const override; + uint32_t getHeight() const override; + + virtual void setEventCallback(const std::function<void(GuiCore::Event&)>) override; + void setVSync(bool enabled) override; + bool isVSyncEnabled() const override; + + void* getNativeWindow() const override; + +private: + virtual void setup(const GuiCore::WindowProperties &properties); + virtual void shutdown(); + + GLFWwindow* m_window; + + struct Data + { + std::string title; + uint32_t width; + uint32_t height; + bool vSyncEnabled; + + std::function<void(GuiCore::Event&)> eventCallback; + }; + Data m_data; +}; +#endif \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000000000000000000000000000000000000..6ec03a6f60161fb0bd80b72bf6365dd95451536f --- /dev/null +++ b/Readme.md @@ -0,0 +1,29 @@ +# QPong + +## Clone +The QPong repository includes two submodules (spdlog and imgui) which can be easily pulled while cloning with: +``` +git clone https://gitlab.cvh-server.de/aco/qpong --recursive +``` + +## Build +The required packages are listed in the `dependencies` file. +On Debian based systems you can run the following command to install all the required packages. +``` +cd qpong +cat dependencies | xargs sudo apt install +``` +Please let me know if you needed to install any additional packages on your specific system. + + +``` +mkdir build && cd build +cmake .. +make -j14 +cd .. +``` + +## Run +``` +./build/QPongApp/QPongApp +``` \ No newline at end of file diff --git a/assets/Font.ttf b/assets/Font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6652fa7007b13ac3f6f3954dd6d1d39ba8564aae Binary files /dev/null and b/assets/Font.ttf differ diff --git a/assets/shaders/glyph.frag.glsl b/assets/shaders/glyph.frag.glsl new file mode 100644 index 0000000000000000000000000000000000000000..abb0c7d56d62636669542def466588957f7dad64 --- /dev/null +++ b/assets/shaders/glyph.frag.glsl @@ -0,0 +1,11 @@ +#version 330 core +in vec2 textureCoordinates; +out vec4 color; + +uniform sampler2D text; +uniform vec3 textColor; + +void main() +{ + color = vec4(textColor, texture(text, textureCoordinates).r) ; +} \ No newline at end of file diff --git a/assets/shaders/glyph.vert.glsl b/assets/shaders/glyph.vert.glsl new file mode 100644 index 0000000000000000000000000000000000000000..db2695ffb8f3a297108d9dceeb96b6e6d93d255f --- /dev/null +++ b/assets/shaders/glyph.vert.glsl @@ -0,0 +1,11 @@ +#version 330 core +layout (location = 0) in vec4 vertex; +out vec2 textureCoordinates; + +uniform mat4 projection; + +void main() +{ + gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); + textureCoordinates = vertex.zw; +} \ No newline at end of file diff --git a/assets/shaders/particle.frag.glsl b/assets/shaders/particle.frag.glsl new file mode 100644 index 0000000000000000000000000000000000000000..e3749afd34cf18193600ea9cd2e4f71b470fa899 --- /dev/null +++ b/assets/shaders/particle.frag.glsl @@ -0,0 +1,7 @@ +#version 330 core +in vec3 fragmentColor; +out vec4 pixelColor; +void main() +{ + pixelColor = vec4(fragmentColor, 1.0); +} \ No newline at end of file diff --git a/assets/shaders/particle.vert.glsl b/assets/shaders/particle.vert.glsl new file mode 100644 index 0000000000000000000000000000000000000000..61a8b04c98e6a7002a7de9fef988a48bda95c7fa --- /dev/null +++ b/assets/shaders/particle.vert.glsl @@ -0,0 +1,40 @@ +#version 330 core +// position to draw +layout (location = 0) in vec3 v_position; +// value of the particle to calculate color +layout (location = 1) in vec2 v_particle_complex; +// potential at this position +layout (location = 2) in float potential; + +// orthografic projection matrix +uniform mat4 u_viewProjection; +// "bool" show potential or not +uniform int u_showPotential; +// "bool" if momentum view is enabled +uniform int u_isMomentumEnabled; + +// color for the fragment shader +out vec3 fragmentColor; + +void main() +{ + // Move the particle view to the left if the momentum is shown too. + vec3 pos = vec3(v_position.x - (1.5f * u_isMomentumEnabled), v_position.y, v_position.z); + + // Determine final postion + gl_Position = u_viewProjection * vec4(pos, 1.0f); + + // Calculate "color" value of the momentum + float pot = potential * 120.0 * u_showPotential; + + // Generate color from data + float real = v_particle_complex[0]; + float imag = v_particle_complex[1]; + float real2 = real * real; + float imag2 = imag * imag; + + fragmentColor = vec3( + sqrt(real2) * 5.0 + pot, // red + sqrt(real2) * 5.0 + sqrt(imag2) * 5.0 + pot / 2.0, // green + sqrt(imag2) * 5.0); // blue +} \ No newline at end of file diff --git a/dependencies b/dependencies new file mode 100644 index 0000000000000000000000000000000000000000..71359605660f5f6f10e76008ee9665179c117471 --- /dev/null +++ b/dependencies @@ -0,0 +1,8 @@ +build-essential +cmake +libfreetype-dev +libopengl-dev +libglx-dev +libglfw3-dev +libglm-dev +libfftw3-dev