diff options
author | hnOsmium0001 <[email protected]> | 2022-04-25 20:22:07 -0700 |
---|---|---|
committer | hnOsmium0001 <[email protected]> | 2022-04-25 20:22:07 -0700 |
commit | f54370de7e4214cb7813d26b1a39a8f6e42b7b56 (patch) | |
tree | 20913b4099b77af933fcd2ebb4e73f53b366ad8f | |
parent | c8ebee643f23c34ff57f69f8dfcf1903b59ea9d1 (diff) |
Initial work on rendering sprites to screen
48 files changed, 2754 insertions, 840 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index ea2b3f4..73e95cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES target_compile_definitions(${PROJECT_NAME} PRIVATE RAPIDJSON_HAS_STDSTRING=1 IMGUI_DISABLE_OBSOLETE_FUNCTIONS + BRUSSEL_DEV_ENV=1 ) target_include_directories(${PROJECT_NAME} PRIVATE diff --git a/source/App.cpp b/source/App.cpp index ac5b319..dc38342 100644 --- a/source/App.cpp +++ b/source/App.cpp @@ -2,44 +2,102 @@ #include <utility> -void App::Init() { - if (mInitialized) return; - mInitialized = true; - - mCurrentWorld = std::make_unique<GameWorld>(); - auto& worldRoot = mCurrentWorld->GetRoot(); +App::App() { + auto& worldRoot = mWorld.GetRoot(); constexpr int kPlayerCount = 2; for (int i = 0; i < kPlayerCount; ++i) { - auto player = new Player(mCurrentWorld.get(), i); + auto player = new Player(&mWorld, i); worldRoot.AddChild(player); mPlayers.push_back(player); + }; + +#if defined(BRUSSEL_DEV_ENV) + SetGameRunning(false); + SetEditorVisible(true); +#else + SetGameRunning(true); +#endif + + // TODO this renders nothing, fix + mGameCamera.Move(glm::vec3(0, 0, 1)); + mGameCamera.LookAt(glm::vec3(0, 0, 0)); + mGameCamera.SetHasPerspective(false); + mEditorCamera.Move(glm::vec3(0, 0, 1)); + mEditorCamera.LookAt(glm::vec3(0, 0, 0)); + mGameCamera.SetHasPerspective(false); +} + +App::~App() { +} + +bool App::IsGameRunning() const { + return mGameRunning; +} + +void App::SetGameRunning(bool running) { + if (mGameRunning != running) { + mGameRunning = running; + if (running) { + mWorld.Awaken(); + } else { + mWorld.Resleep(); + } } +} - mCurrentWorld->Awaken(); - mEditor = std::make_unique<EditorInstance>(this, mCurrentWorld.get()); +bool App::IsEditorVisible() const { + return mEditorVisible; } -void App::Shutdown() { - if (!mInitialized) return; - mInitialized = false; - mEditor = nullptr; - mCurrentWorld->Resleep(); - mCurrentWorld = nullptr; - mPlayers.clear(); +void App::SetEditorVisible(bool visible) { + if (mEditorVisible != visible) { + mEditorVisible = visible; + if (visible) { + if (mEditor == nullptr) { + mEditor = std::make_unique<EditorInstance>(this, &mWorld); + } + } + } } void App::Show() { - if (mEditorShown) { + if (mEditorVisible) { mEditor->Show(); } } void App::Update() { + if (IsGameRunning()) { + mWorld.Update(); + } } -void App::Draw() { - mCurrentWorld->Draw(); +void App::Draw(float currentTime, float deltaTime) { + Camera* camera; + if (IsGameRunning()) { + camera = &mGameCamera; + } else { + camera = &mEditorCamera; + } + mRenderer.BeginFrame(*camera, currentTime, deltaTime); + + PodVector<GameObject*> stack; + stack.push_back(&mWorld.GetRoot()); + + while (!stack.empty()) { + auto obj = stack.back(); + stack.pop_back(); + + for (auto child : obj->GetChildren()) { + stack.push_back(child); + } + + auto renderObjects = obj->GetRenderObjects(); + mRenderer.Draw(renderObjects.data(), renderObjects.size()); + } + + mRenderer.EndFrame(); } void App::HandleMouse(int button, int action) { @@ -60,15 +118,10 @@ void App::HandleKey(GLFWkeyboard* keyboard, int key, int action) { switch (key) { case GLFW_KEY_F3: { if (action == GLFW_PRESS) { - mEditorShown = !mEditorShown; + SetEditorVisible(!IsEditorVisible()); } return; } - - case GLFW_KEY_F11: { - // TODO fullscreen - return; - } } for (auto& player : mPlayers) { diff --git a/source/App.hpp b/source/App.hpp index fbdbd43..26857db 100644 --- a/source/App.hpp +++ b/source/App.hpp @@ -1,8 +1,10 @@ #pragma once +#include "Camera.hpp" #include "EditorCore.hpp" #include "Player.hpp" #include "PodVector.hpp" +#include "Renderer.hpp" #include "World.hpp" #define GLFW_INCLUDE_NONE @@ -20,19 +22,32 @@ private: std::deque<KeyCaptureCallback> mKeyCaptureCallbacks; PodVector<Player*> mPlayers; std::unique_ptr<EditorInstance> mEditor; - std::unique_ptr<GameWorld> mCurrentWorld; - bool mEditorShown = true; - bool mInitialized = false; + GameWorld mWorld; + Camera mGameCamera; + Camera mEditorCamera; + Renderer mRenderer; + // NOTE: should only be true when mEditor != nullptr + bool mEditorVisible = false; + bool mGameRunning = false; public: - void Init(); - void Shutdown(); + App(); + ~App(); + + EditorInstance* GetEditor() { return mEditor.get(); } + GameWorld* GetWorld() { return &mWorld; } + + bool IsGameRunning() const; + void SetGameRunning(bool running); + + bool IsEditorVisible() const; + void SetEditorVisible(bool visible); // Do ImGui calls void Show(); // Do regular calls void Update(); - void Draw(); + void Draw(float currentTime, float deltaTime); void HandleMouse(int button, int action); void HandleMouseMotion(double xOff, double yOff); diff --git a/source/AppConfig.hpp b/source/AppConfig.hpp index 8d69fad..b6c1fab 100644 --- a/source/AppConfig.hpp +++ b/source/AppConfig.hpp @@ -8,6 +8,10 @@ constexpr std::string_view kAppName = "ProjectBrussel"; // Since kAppName is initialized by a C string literal, we know it's null termianted constexpr const char* kAppNameC = kAppName.data(); +inline float mainWidnowWidth; +inline float mainWindowHeight; +inline float mainWindowAspectRatio; + // Duplicate each as path and string so that on non-UTF-8 platforms (e.g. Windows) we can easily do string manipulation on the paths // NOTE: even though non-const, these should not be modified outside of main() inline std::filesystem::path dataDirPath; diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index f37d4a9..b4b6e3b 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -1,12 +1,15 @@ target_sources(${PROJECT_NAME} PRIVATE App.cpp - CpuMesh.cpp + Camera.cpp + CommonVertexIndex.cpp EditorAccessories.cpp EditorAttachmentImpl.cpp + EditorCommandPalette.cpp EditorCore.cpp EditorNotification.cpp EditorUtils.cpp + FuzzyMatch.cpp GameObject.cpp GraphicsTags.cpp Image.cpp @@ -22,6 +25,7 @@ PRIVATE Sprite.cpp Texture.cpp Uid.cpp + VertexIndex.cpp World.cpp ) diff --git a/source/Camera.cpp b/source/Camera.cpp new file mode 100644 index 0000000..90acd8f --- /dev/null +++ b/source/Camera.cpp @@ -0,0 +1,33 @@ +#include "Camera.hpp" + +#include "AppConfig.hpp" + +#include <glm/gtc/matrix_transform.hpp> + +void Camera::Move(glm::vec3 pos) { + this->pos = pos; +} + +void Camera::LookAt(glm::vec3 pos) { + this->lookAt = pos; +} + +void Camera::SetHasPerspective(bool perspective) { + this->perspective = perspective; +} + +glm::mat4 Camera::CalcViewMatrix() const { + return glm::lookAt(pos, lookAt, glm::vec3(0, 1, 0)); +} + +glm::mat4 Camera::CalcProjectionMatrix() const { + if (perspective) { + return glm::perspective(90.0f, AppConfig::mainWindowAspectRatio, 0.1f, 1000.0f); + } else { + return glm::ortho( + pos.x - AppConfig::mainWidnowWidth / 2, + pos.x + AppConfig::mainWidnowWidth / 2, + pos.y - AppConfig::mainWindowHeight / 2, + pos.y + AppConfig::mainWindowHeight / 2); + } +} diff --git a/source/Camera.hpp b/source/Camera.hpp new file mode 100644 index 0000000..d203622 --- /dev/null +++ b/source/Camera.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include <glm/glm.hpp> + +class Camera { +public: + glm::vec3 pos; + glm::vec3 lookAt; + bool perspective = false; + +public: + void Move(glm::vec3 pos); + void LookAt(glm::vec3 pos); + + bool HasPerspective() const { return perspective; } + void SetHasPerspective(bool perspective); + + glm::mat4 CalcViewMatrix() const; + glm::mat4 CalcProjectionMatrix() const; +}; diff --git a/source/CommonVertexIndex.cpp b/source/CommonVertexIndex.cpp new file mode 100644 index 0000000..786274e --- /dev/null +++ b/source/CommonVertexIndex.cpp @@ -0,0 +1,152 @@ +#include "CommonVertexIndex.hpp" + +template <class TNumber> +static void AssignIndices(TNumber indices[6], TNumber startIdx) { + // Triangle #1 + indices[0] = startIdx + 1; // Top right + indices[1] = startIdx + 0; // Top left + indices[2] = startIdx + 3; // Bottom left + // Triangle #2 + indices[3] = startIdx + 1; // Top right + indices[4] = startIdx + 3; // Bottom left + indices[5] = startIdx + 2; // Bottom right +} + +template <class TNumber> +static void AssignIndices(TNumber indices[6], TNumber topLeft, TNumber topRight, TNumber bottomRight, TNumber bottomLeft) { + // Triangle #1 + indices[0] = topRight; + indices[1] = topLeft; + indices[2] = bottomLeft; + // Triangle #2 + indices[3] = topRight; + indices[4] = bottomLeft; + indices[5] = bottomRight; +} + +template <class TVertex> +static void AssignPositions(TVertex vertices[4], const Rect<float>& rect) { + // Top left + vertices[0].x = rect.x0(); + vertices[0].y = rect.y0(); + // Top right + vertices[1].x = rect.x1(); + vertices[1].y = rect.y0(); + // Bottom right + vertices[2].x = rect.x1(); + vertices[2].y = rect.y1(); + // Bottom left + vertices[3].x = rect.x0(); + vertices[3].y = rect.y1(); +} + +template <class TVertex> +static void AssignPositions(TVertex vertices[4], glm::vec2 bl, glm::vec2 tr) { + // Top left + vertices[0].x = bl.x; + vertices[0].y = tr.y; + // Top right + vertices[1].x = tr.x; + vertices[1].y = tr.y; + // Bottom right + vertices[2].x = tr.x; + vertices[2].y = bl.y; + // Bottom left + vertices[3].x = bl.x; + vertices[3].y = bl.y; +} + +template <class TVertex> +static void AssignDepths(TVertex vertices[4], float z) { + for (int i = 0; i < 4; ++i) { + auto& vert = vertices[i]; + vert.z = z; + } +} + +template <class TVertex> +static void AssignTexCoords(TVertex vertices[4], const Subregion& texcoords) { + // Top left + vertices[0].u = texcoords.u0; + vertices[0].v = texcoords.v1; + // Top right + vertices[1].u = texcoords.u1; + vertices[1].v = texcoords.v1; + // Bottom right + vertices[2].u = texcoords.u1; + vertices[2].v = texcoords.v0; + // Bottom left + vertices[3].u = texcoords.u0; + vertices[3].v = texcoords.v0; +} + +template <class TVertex> +static void AssignColors(TVertex vertices[4], RgbaColor color) { + for (int i = 0; i < 4; ++i) { + auto& vert = vertices[i]; + vert.r = color.r; + vert.g = color.g; + vert.b = color.b; + vert.a = color.a; + } +} + +void Index_U16::Assign(uint16_t indices[6], uint16_t startIdx) { + ::AssignIndices(indices, startIdx); +} + +void Index_U16::Assign(uint16_t indices[6], uint16_t startIdx, uint16_t topLeft, uint16_t topRight, uint16_t bottomRight, uint16_t bottomLeft) { + ::AssignIndices<uint16_t>(indices, startIdx + topLeft, startIdx + topRight, startIdx + bottomRight, startIdx + bottomLeft); +} + +void Index_U16::Assign(uint16_t indices[6], uint16_t topLeft, uint16_t topRight, uint16_t bottomRight, uint16_t bottomLeft) { + ::AssignIndices<uint16_t>(indices, topLeft, topRight, bottomRight, bottomLeft); +} + +void Index_U32::Assign(uint32_t indices[6], uint32_t startIdx) { + ::AssignIndices(indices, startIdx); +} + +void Index_U32::Assign(uint32_t indices[6], uint32_t startIdx, uint32_t topLeft, uint32_t topRight, uint32_t bottomRight, uint32_t bottomLeft) { + ::AssignIndices<uint32_t>(indices, startIdx + topLeft, startIdx + topRight, startIdx + bottomRight, startIdx + bottomLeft); +} + +void Index_U32::Assign(uint32_t indices[6], uint32_t topLeft, uint32_t topRight, uint32_t bottomRight, uint32_t bottomLeft) { + ::AssignIndices<uint32_t>(indices, topLeft, topRight, bottomRight, bottomLeft); +} + +void Vertex_PC::Assign(Vertex_PC vertices[4], const Rect<float>& rect) { + ::AssignPositions(vertices, rect); +} + +void Vertex_PC::Assign(Vertex_PC vertices[4], glm::vec2 bottomLeft, glm::vec2 topRight) { + ::AssignPositions(vertices, bottomLeft, topRight); +} + +void Vertex_PC::Assign(Vertex_PC vertices[4], float z) { + ::AssignDepths(vertices, z); +} + +void Vertex_PC::Assign(Vertex_PC vertices[4], RgbaColor color) { + ::AssignColors(vertices, color); +} + +void Vertex_PTC::Assign(Vertex_PTC vertices[4], const Rect<float>& rect) { + ::AssignPositions(vertices, rect); +} + +void Vertex_PTC::Assign(Vertex_PTC vertices[4], glm::vec2 bottomLeft, glm::vec2 topRight) { + ::AssignPositions(vertices, bottomLeft, topRight); +} + +void Vertex_PTC::Assign(Vertex_PTC vertices[4], float z) { + ::AssignDepths(vertices, z); +} + +void Vertex_PTC::Assign(Vertex_PTC vertices[4], const Subregion& texcoords) { + ::AssignTexCoords(vertices, texcoords); +} + +void Vertex_PTC::Assign(Vertex_PTC vertices[4], RgbaColor color) { + ::AssignColors(vertices, color); +} diff --git a/source/CommonVertexIndex.hpp b/source/CommonVertexIndex.hpp new file mode 100644 index 0000000..c15e658 --- /dev/null +++ b/source/CommonVertexIndex.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "Color.hpp" +#include "Rect.hpp" +#include "Texture.hpp" + +#include <cstdint> + +// Suffixes: +// - _P_osition +// - _T_exture coordiantes +// - _C_olor +// - _N_ormal +// When an number is attached to some suffix, it means there are N number of this element + +struct Index_U16 { + uint16_t value; + + static void Assign(uint16_t indices[6], uint16_t startIdx); + static void Assign(uint16_t indices[6], uint16_t startIdx, uint16_t topLeft, uint16_t topRight, uint16_t bottomRight, uint16_t bottomLeft); + static void Assign(uint16_t indices[6], uint16_t topLeft, uint16_t topRight, uint16_t bottomRight, uint16_t bottomLeft); +}; + +struct Index_U32 { + uint32_t value; + + static void Assign(uint32_t indices[6], uint32_t startIdx); + static void Assign(uint32_t indices[6], uint32_t startIdx, uint32_t topLeft, uint32_t topRight, uint32_t bottomRight, uint32_t bottomLeft); + static void Assign(uint32_t indices[6], uint32_t topLeft, uint32_t topRight, uint32_t bottomRight, uint32_t bottomLeft); +}; + +struct Vertex_PC { + float x, y, z; + uint8_t r, g, b, a; + + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PC vertices[4], const Rect<float>& rect); + /// Assign position in regular cartesian coordinate space (x increases from left to right, y increases from top to bottom). + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PC vertices[4], glm::vec2 bottomLeft, glm::vec2 topRight); + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PC vertices[4], float z); + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PC vertices[4], RgbaColor color); +}; + +struct Vertex_PTC { + float x, y, z; + float u, v; + uint8_t r, g, b, a; + + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PTC vertices[4], const Rect<float>& rect); + /// Assign position in regular cartesian coordinate space (x increases from left to right, y increases from top to bottom). + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PTC vertices[4], glm::vec2 bottomLeft, glm::vec2 topRight); + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PTC vertices[4], float z); + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PTC vertices[4], const Subregion& uvs); + /// Assumes the 4 vertices come in TL, TR, BR, BL order. + static void Assign(Vertex_PTC vertices[4], RgbaColor color); +}; + +struct Vertex_PTNC { + float x, y, z; + float nx, ny, nz; + float u, v; + uint8_t r, g, b, a; +}; diff --git a/source/CpuMesh.cpp b/source/CpuMesh.cpp deleted file mode 100644 index 15b0f54..0000000 --- a/source/CpuMesh.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "CpuMesh.hpp" - -#include <cstring> - -StandardCpuMesh::StandardCpuMesh() - : mGpuMesh(new GpuMesh()) { - mGpuMesh->vertFormat = gVformatStandard; - mGpuMesh->vertBufBindings.SetBinding(0, new GpuVertexBuffer()); - mGpuMesh->vertBufBindings.SetBinding(1, new GpuVertexBuffer()); - mGpuMesh->indexBuf.Attach(new GpuIndexBuffer()); -} - -StandardCpuMesh::~StandardCpuMesh() { - delete mData; -} - -void StandardCpuMesh::CreateCpuData() { - if (!mData) { - mData = new StandardCpuMeshData(); - } -} - -GpuVertexBuffer* StandardCpuMesh::GetPosBuffer() const { - return mGpuMesh->vertBufBindings.bindings[0].Get(); -} - -GpuVertexBuffer* StandardCpuMesh::GetExtraBuffer() const { - return mGpuMesh->vertBufBindings.bindings[1].Get(); -} - -bool StandardCpuMesh::UpdatePositions(glm::vec3* pos, size_t count, size_t startVertIndex) { - if (mData) { - std::memcpy(&mData->vertPositions[startVertIndex], pos, count * sizeof(glm::vec3)); - } - auto posBuf = GetPosBuffer(); - glBindBuffer(GL_ARRAY_BUFFER, posBuf->handle); - glBufferSubData(GL_ARRAY_BUFFER, startVertIndex * mGpuMesh->vertFormat->vertexSize, count * sizeof(glm::vec3), pos); - return true; -} - -bool StandardCpuMesh::UpdateColors(RgbaColor* color, size_t count, size_t starVertIndex) { - if (!mData) return false; - // TODO -} - -bool StandardCpuMesh::UpdateNormals(glm::vec2* normals, size_t count, size_t startVertIndex) { - if (!mData) return false; - // TODO -} - -bool StandardCpuMesh::UpdateIndices(uint32_t* indices, size_t count, size_t startVertIndex) { - if (!mData) return false; - // TODO -} diff --git a/source/CpuMesh.hpp b/source/CpuMesh.hpp deleted file mode 100644 index 9fcb00c..0000000 --- a/source/CpuMesh.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "Color.hpp" -#include "Mesh.hpp" -#include "PodVector.hpp" -#include "RcPtr.hpp" - -#include <cstddef> -#include <cstdint> -#include <glm/glm.hpp> -#include <memory> - -struct StandardVertex { - float x, y, z; - float u, v; - uint8_t r, g, b, a; -}; - -struct StandardVertexExtra { - float u, v; - uint8_t r, g, b, a; -}; - -class StandardCpuMeshData { -public: - PodVector<glm::vec3> vertPositions; - PodVector<StandardVertexExtra> vertExtra; - PodVector<uint32_t> index; - size_t vertexCount; - size_t triangleCount; -}; - -class StandardCpuMesh { -private: - StandardCpuMeshData* mData = nullptr; - RcPtr<GpuMesh> mGpuMesh; - -public: - StandardCpuMesh(); - ~StandardCpuMesh(); - - GpuVertexBuffer* GetPosBuffer() const; - GpuVertexBuffer* GetExtraBuffer() const; - GpuMesh* GetGpuMesh() const { return mGpuMesh.Get(); } - - void CreateCpuData(); - bool UpdatePositions(glm::vec3* pos, size_t count, size_t startVertIndex); - bool UpdateColors(RgbaColor* color, size_t count, size_t starVertIndex); - bool UpdateNormals(glm::vec2* normals, size_t count, size_t startVertIndex); - bool UpdateIndices(uint32_t* indices, size_t count, size_t startVertIndex); -}; diff --git a/source/EditorAttachmentImpl.cpp b/source/EditorAttachmentImpl.cpp index 75d93cb..5193f84 100644 --- a/source/EditorAttachmentImpl.cpp +++ b/source/EditorAttachmentImpl.cpp @@ -7,14 +7,14 @@ EditorAttachment::EditorAttachment() { std::unique_ptr<EditorAttachment> EaGameObject::Create(GameObject* object) { EditorAttachment* result; - using namespace Tags; - switch (object->GetTypeTag()) { - case GOT_Player: result = new EaPlayer(); break; - case GOT_LevelWrapper: result = new EaLevelWrapper(); break; + auto kind = object->GetKind(); + switch (kind) { + case GameObject::KD_Player: result = new EaPlayer(); break; + case GameObject::KD_LevelWrapper: result = new EaLevelWrapper(); break; default: result = new EditorAttachment(); break; } - result->name = NameOf(object->GetTypeTag()); + result->name = GameObject::ToString(kind); return std::unique_ptr<EditorAttachment>(result); } diff --git a/source/EditorAttachmentImpl.hpp b/source/EditorAttachmentImpl.hpp index 258e987..e9dbe6f 100644 --- a/source/EditorAttachmentImpl.hpp +++ b/source/EditorAttachmentImpl.hpp @@ -2,7 +2,9 @@ #include "EditorAttachment.hpp" #include "GameObject.hpp" +#include "Material.hpp" #include "Player.hpp" +#include "Sprite.hpp" #include <memory> @@ -13,6 +15,8 @@ public: class EaPlayer : public EditorAttachment { public: + RcPtr<IresSpritesheet> confSprite; + RcPtr<IresMaterial> confMaterial; }; class EaLevelWrapper : public EditorAttachment { @@ -24,14 +28,3 @@ public: std::string nameEditingScratch; bool isEditingName = false; }; - -class EaShader : public EditorAttachment { -public: - Shader* shader; -}; - -class EaMaterial : public EditorAttachment { -public: - std::string editingScratch; - bool isEditingName = false; -}; diff --git a/source/EditorCommandPalette.cpp b/source/EditorCommandPalette.cpp new file mode 100644 index 0000000..02ff65a --- /dev/null +++ b/source/EditorCommandPalette.cpp @@ -0,0 +1,704 @@ +#include "EditorCommandPalette.hpp" + +#include "FuzzyMatch.hpp" + +#include <imgui.h> +#include <algorithm> +#include <cstring> +#include <limits> +#include <utility> + +#define IMGUI_DEFINE_MATH_OPERATORS +#include <imgui_internal.h> + +namespace ImCmd { +// ================================================================= +// Private forward decls +// ================================================================= + +struct StackFrame; +class ExecutionManager; + +struct SearchResult; +class SearchManager; + +struct CommandOperationRegister; +struct CommandOperationUnregister; +struct CommandOperation; +struct Context; + +struct ItemExtraData; +struct Instance; + +// ================================================================= +// Private interface +// ================================================================= + +struct StackFrame { + std::vector<std::string> Options; + int SelectedOption = -1; +}; + +class ExecutionManager { +private: + Instance* m_Instance; + Command* m_ExecutingCommand = nullptr; + std::vector<StackFrame> m_CallStack; + +public: + ExecutionManager(Instance& instance) + : m_Instance{ &instance } {} + + int GetItemCount() const; + const char* GetItem(int idx) const; + void SelectItem(int idx); + + void PushOptions(std::vector<std::string> options); +}; + +struct SearchResult { + int ItemIndex; + int Score; + int MatchCount; + uint8_t Matches[32]; +}; + +class SearchManager { +private: + Instance* m_Instance; + +public: + std::vector<SearchResult> SearchResults; + char SearchText[std::numeric_limits<uint8_t>::max() + 1]; + +public: + SearchManager(Instance& instance) + : m_Instance{ &instance } { + std::memset(SearchText, 0, sizeof(SearchText)); + } + + int GetItemCount() const; + const char* GetItem(int idx) const; + + bool IsActive() const; + + void SetSearchText(const char* text); + void ClearSearchText(); + void RefreshSearchResults(); +}; + +struct CommandOperationRegister { + Command Candidate; +}; + +struct CommandOperationUnregister { + const char* Name; +}; + +struct CommandOperation { + enum OpType { + OpType_Register, + OpType_Unregister, + }; + + OpType Type; + int Index; +}; + +struct Context { + ImGuiStorage Instances; + Instance* CurrentCommandPalette = nullptr; + std::vector<Command> Commands; + std::vector<CommandOperationRegister> PendingRegisterOps; + std::vector<CommandOperationUnregister> PendingUnregisterOps; + std::vector<CommandOperation> PendingOps; + ImFont* Fonts[ImCmdTextType_COUNT] = {}; + ImU32 FontColors[ImCmdTextType_COUNT] = {}; + int CommandStorageLocks = 0; + bool HasFontColorOverride[ImCmdTextType_COUNT] = {}; + bool IsExecuting = false; + bool IsTerminating = false; + + struct + { + bool ItemSelected = false; + } LastCommandPaletteStatus; + + struct + { + const char* NewSearchText = nullptr; + bool FocusSearchBox = false; + } NextCommandPaletteActions; + + void RegisterCommand(Command command) { + auto location = std::lower_bound( + Commands.begin(), + Commands.end(), + command, + [](const Command& a, const Command& b) -> bool { + return strcmp(a.Name, b.Name) < 0; + }); + Commands.insert(location, std::move(command)); + } + + bool UnregisterCommand(const char* name) { + struct Comparator { + bool operator()(const Command& command, const char* str) const { + return strcmp(command.Name, str) < 0; + } + + bool operator()(const char* str, const Command& command) const { + return strcmp(str, command.Name) < 0; + } + }; + + auto range = std::equal_range(Commands.begin(), Commands.end(), name, Comparator{}); + Commands.erase(range.first, range.second); + + return range.first != range.second; + } + + bool CommitOps() { + if (IsCommandStorageLocked()) { + return false; + } + + for (auto& operation : PendingOps) { + switch (operation.Type) { + case CommandOperation::OpType_Register: { + auto& op = PendingRegisterOps[operation.Index]; + RegisterCommand(std::move(op.Candidate)); + } break; + + case CommandOperation::OpType_Unregister: { + auto& op = PendingUnregisterOps[operation.Index]; + UnregisterCommand(op.Name); + } break; + } + } + + bool had_action = !PendingOps.empty(); + PendingRegisterOps.clear(); + PendingUnregisterOps.clear(); + PendingOps.clear(); + + return had_action; + } + + bool IsCommandStorageLocked() const { + return CommandStorageLocks > 0; + } +}; + +struct ItemExtraData { + bool Hovered = false; + bool Held = false; +}; + +struct Instance { + ExecutionManager Session; + SearchManager Search; + std::vector<ItemExtraData> ExtraData; + + int CurrentSelectedItem = 0; + + struct + { + bool RefreshSearch = false; + bool ClearSearch = false; + } PendingActions; + + Instance() + : Session(*this) + , Search(*this) {} +}; + +static Context gContext; + +// ================================================================= +// Private implementation +// ================================================================= + +int ExecutionManager::GetItemCount() const { + if (m_ExecutingCommand) { + return static_cast<int>(m_CallStack.back().Options.size()); + } else { + return static_cast<int>(gContext.Commands.size()); + } +} + +const char* ExecutionManager::GetItem(int idx) const { + if (m_ExecutingCommand) { + return m_CallStack.back().Options[idx].c_str(); + } else { + return gContext.Commands[idx].Name; + } +} + +template <class... Ts> +static void InvokeSafe(const std::function<void(Ts...)>& func, Ts... args) { + if (func) { + func(std::forward<Ts>(args)...); + } +} + +void ExecutionManager::SelectItem(int idx) { + auto cmd = m_ExecutingCommand; + size_t initial_call_stack_height = m_CallStack.size(); + if (cmd == nullptr) { + cmd = m_ExecutingCommand = &gContext.Commands[idx]; + ++gContext.CommandStorageLocks; + + gContext.IsExecuting = true; + InvokeSafe(m_ExecutingCommand->InitialCallback); // Calls ::Prompt() + gContext.IsExecuting = false; + } else { + m_CallStack.back().SelectedOption = idx; + + gContext.IsExecuting = true; + InvokeSafe(cmd->SubsequentCallback, idx); // Calls ::Prompt() + gContext.IsExecuting = false; + } + + size_t final_call_stack_height = m_CallStack.size(); + if (initial_call_stack_height == final_call_stack_height) { + + gContext.IsTerminating = true; + InvokeSafe(m_ExecutingCommand->TerminatingCallback); // Shouldn't call ::Prompt() + gContext.IsTerminating = false; + + m_ExecutingCommand = nullptr; + m_CallStack.clear(); + --gContext.CommandStorageLocks; + + // If the executed command involved subcommands... + if (final_call_stack_height > 0) { + m_Instance->PendingActions.ClearSearch = true; + m_Instance->CurrentSelectedItem = 0; + } + + gContext.LastCommandPaletteStatus.ItemSelected = true; + } else { + // Something new is prompted + // It doesn't make sense for "current selected item" to persists through completely different set of options + m_Instance->PendingActions.ClearSearch = true; + m_Instance->CurrentSelectedItem = 0; + } +} + +void ExecutionManager::PushOptions(std::vector<std::string> options) { + m_CallStack.push_back({}); + auto& frame = m_CallStack.back(); + + frame.Options = std::move(options); + + m_Instance->PendingActions.ClearSearch = true; +} + +int SearchManager::GetItemCount() const { + return static_cast<int>(SearchResults.size()); +} + +const char* SearchManager::GetItem(int idx) const { + int actualIdx = SearchResults[idx].ItemIndex; + return m_Instance->Session.GetItem(actualIdx); +} + +bool SearchManager::IsActive() const { + return SearchText[0] != '\0'; +} + +void SearchManager::SetSearchText(const char* text) { + // Note: must detect clang first because clang-cl.exe defines both _MSC_VER and __clang__, but only accepts #pragma clang +#if defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#elif defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdeprecated-declarations" +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4996) +#endif + // Copy at most IM_ARRAYSIZE(SearchText) chars from `text` to `SearchText` + std::strncpy(SearchText, text, IM_ARRAYSIZE(SearchText)); +#if defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(__clang__) +# pragma clang diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + RefreshSearchResults(); +} + +void SearchManager::ClearSearchText() { + std::memset(SearchText, 0, IM_ARRAYSIZE(SearchText)); + SearchResults.clear(); +} + +void SearchManager::RefreshSearchResults() { + m_Instance->CurrentSelectedItem = 0; + SearchResults.clear(); + + int item_count = m_Instance->Session.GetItemCount(); + for (int i = 0; i < item_count; ++i) { + const char* text = m_Instance->Session.GetItem(i); + SearchResult result; + if (FuzzyMatch::Search(SearchText, text, result.Score, result.Matches, IM_ARRAYSIZE(result.Matches), result.MatchCount)) { + result.ItemIndex = i; + SearchResults.push_back(result); + } + } + + std::sort( + SearchResults.begin(), + SearchResults.end(), + [](const SearchResult& a, const SearchResult& b) -> bool { + // We want the biggest element first + return a.Score > b.Score; + }); +} + +// ================================================================= +// API implementation +// ================================================================= + +void AddCommand(Command command) { + if (gContext.IsCommandStorageLocked()) { + gContext.PendingRegisterOps.push_back(CommandOperationRegister{ std::move(command) }); + CommandOperation op; + op.Type = CommandOperation::OpType_Register; + op.Index = static_cast<int>(gContext.PendingRegisterOps.size()) - 1; + gContext.PendingOps.push_back(op); + } else { + gContext.RegisterCommand(std::move(command)); + } + + if (auto current = gContext.CurrentCommandPalette) { + current->PendingActions.RefreshSearch = true; + } +} + +void RemoveCommand(const char* name) { + if (gContext.IsCommandStorageLocked()) { + gContext.PendingUnregisterOps.push_back(CommandOperationUnregister{ name }); + CommandOperation op; + op.Type = CommandOperation::OpType_Unregister; + op.Index = static_cast<int>(gContext.PendingUnregisterOps.size()) - 1; + gContext.PendingOps.push_back(op); + } else { + gContext.UnregisterCommand(name); + } + + if (auto current = gContext.CurrentCommandPalette) { + current->PendingActions.RefreshSearch = true; + } +} + +void SetStyleFont(ImCmdTextType type, ImFont* font) { + gContext.Fonts[type] = font; +} + +void SetStyleColor(ImCmdTextType type, ImU32 color) { + gContext.FontColors[type] = color; + gContext.HasFontColorOverride[type] = true; +} + +void ClearStyleColor(ImCmdTextType type) { + gContext.HasFontColorOverride[type] = false; +} + +void SetNextCommandPaletteSearch(const char* text) { + IM_ASSERT(text != nullptr); + gContext.NextCommandPaletteActions.NewSearchText = text; +} + +void SetNextCommandPaletteSearchBoxFocused() { + gContext.NextCommandPaletteActions.FocusSearchBox = true; +} + +void ShowCommandPalette(const char* name) { + auto& gi = *[&]() { + auto id = ImHashStr(name); + if (auto ptr = gContext.Instances.GetVoidPtr(id)) { + return reinterpret_cast<Instance*>(ptr); + } else { + auto instance = new Instance(); + gContext.Instances.SetVoidPtr(id, instance); + return instance; + } + }(); + + float width = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x; + float search_result_window_height = 400.0f; // TODO config + + // BEGIN this command palette + gContext.CurrentCommandPalette = &gi; + ImGui::PushID(name); + + gContext.LastCommandPaletteStatus = {}; + + // BEGIN processing PendingActions + bool refresh_search = gi.PendingActions.RefreshSearch; + refresh_search |= gContext.CommitOps(); + + if (auto text = gContext.NextCommandPaletteActions.NewSearchText) { + refresh_search = false; + if (text[0] == '\0') { + gi.Search.ClearSearchText(); + } else { + gi.Search.SetSearchText(text); + } + } else if (gi.PendingActions.ClearSearch) { + refresh_search = false; + gi.Search.ClearSearchText(); + } + + if (refresh_search) { + gi.Search.RefreshSearchResults(); + } + + gi.PendingActions = {}; + // END procesisng PendingActions + + if (gContext.NextCommandPaletteActions.FocusSearchBox) { + // Focus the search box when user first brings command palette window up + // Note: this only affects the next frame + ImGui::SetKeyboardFocusHere(0); + } + ImGui::SetNextItemWidth(width); + if (ImGui::InputText("##SearchBox", gi.Search.SearchText, IM_ARRAYSIZE(gi.Search.SearchText))) { + // Search string updated, update search results + gi.Search.RefreshSearchResults(); + } + + ImGui::BeginChild("SearchResults", ImVec2(width, search_result_window_height)); + + auto window = ImGui::GetCurrentWindow(); + auto draw_list = window->DrawList; + + auto font_regular = gContext.Fonts[ImCmdTextType_Regular]; + if (!font_regular) { + font_regular = ImGui::GetDrawListSharedData()->Font; + } + auto font_highlight = gContext.Fonts[ImCmdTextType_Highlight]; + if (!font_highlight) { + font_highlight = ImGui::GetDrawListSharedData()->Font; + } + + ImU32 text_color_regular; + ImU32 text_color_highlight; + if (gContext.HasFontColorOverride[ImCmdTextType_Regular]) { + text_color_regular = gContext.FontColors[ImCmdTextType_Regular]; + } else { + text_color_regular = ImGui::GetColorU32(ImGuiCol_Text); + } + if (gContext.HasFontColorOverride[ImCmdTextType_Highlight]) { + text_color_highlight = gContext.FontColors[ImCmdTextType_Highlight]; + } else { + text_color_highlight = ImGui::GetColorU32(ImGuiCol_Text); + } + + auto item_hovered_color = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + auto item_active_color = ImGui::GetColorU32(ImGuiCol_HeaderActive); + auto item_selected_color = ImGui::GetColorU32(ImGuiCol_Header); + + int item_count; + if (gi.Search.IsActive()) { + item_count = gi.Search.GetItemCount(); + } else { + item_count = gi.Session.GetItemCount(); + } + + if (gi.ExtraData.size() < item_count) { + gi.ExtraData.resize(item_count); + } + + // Flag used to delay item selection until after the loop ends + bool select_focused_item = false; + for (int i = 0; i < item_count; ++i) { + auto id = window->GetID(static_cast<int>(i)); + + ImVec2 size{ + ImGui::GetContentRegionAvail().x, + ImMax(font_regular->FontSize, font_highlight->FontSize), + }; + ImRect rect{ + window->DC.CursorPos, + window->DC.CursorPos + ImGui::CalcItemSize(size, 0.0f, 0.0f), + }; + + bool& hovered = gi.ExtraData[i].Hovered; + bool& held = gi.ExtraData[i].Held; + if (held && hovered) { + draw_list->AddRectFilled(rect.Min, rect.Max, item_active_color); + } else if (hovered) { + draw_list->AddRectFilled(rect.Min, rect.Max, item_hovered_color); + } else if (gi.CurrentSelectedItem == i) { + draw_list->AddRectFilled(rect.Min, rect.Max, item_selected_color); + } + + if (gi.Search.IsActive()) { + // Iterating search results: draw text with highlights at matched chars + + auto& search_result = gi.Search.SearchResults[i]; + auto text = gi.Search.GetItem(i); + + auto text_pos = window->DC.CursorPos; + int range_begin; + int range_end; + int last_range_end = 0; + + auto DrawCurrentRange = [&]() { + if (range_begin != last_range_end) { + // Draw normal text between last highlighted range end and current highlighted range start + auto begin = text + last_range_end; + auto end = text + range_begin; + draw_list->AddText(text_pos, text_color_regular, begin, end); + + auto segment_size = font_regular->CalcTextSizeA(font_regular->FontSize, std::numeric_limits<float>::max(), 0.0f, begin, end); + text_pos.x += segment_size.x; + } + + auto begin = text + range_begin; + auto end = text + range_end; + draw_list->AddText(font_highlight, font_highlight->FontSize, text_pos, text_color_highlight, begin, end); + + auto segment_size = font_highlight->CalcTextSizeA(font_highlight->FontSize, std::numeric_limits<float>::max(), 0.0f, begin, end); + text_pos.x += segment_size.x; + }; + + IM_ASSERT(search_result.MatchCount >= 1); + range_begin = search_result.Matches[0]; + range_end = range_begin; + + int last_char_idx = -1; + for (int j = 0; j < search_result.MatchCount; ++j) { + int char_idx = search_result.Matches[j]; + + if (char_idx == last_char_idx + 1) { + // These 2 indices are equal, extend our current range by 1 + ++range_end; + } else { + DrawCurrentRange(); + last_range_end = range_end; + range_begin = char_idx; + range_end = char_idx + 1; + } + + last_char_idx = char_idx; + } + + // Draw the remaining range (if any) + if (range_begin != range_end) { + DrawCurrentRange(); + } + + // Draw the text after the last range (if any) + draw_list->AddText(text_pos, text_color_regular, text + range_end); // Draw until \0 + } else { + // Iterating everything else: draw text as-is, there is no highlights + + auto text = gi.Session.GetItem(i); + auto text_pos = window->DC.CursorPos; + draw_list->AddText(text_pos, text_color_regular, text); + } + + ImGui::ItemSize(rect); + if (!ImGui::ItemAdd(rect, id)) { + continue; + } + if (ImGui::ButtonBehavior(rect, id, &hovered, &held)) { + gi.CurrentSelectedItem = i; + select_focused_item = true; + } + } + + if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow))) { + gi.CurrentSelectedItem = ImMax(gi.CurrentSelectedItem - 1, 0); + } else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow))) { + gi.CurrentSelectedItem = ImMin(gi.CurrentSelectedItem + 1, item_count - 1); + } + if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || select_focused_item) { + if (gi.Search.IsActive() && !gi.Search.SearchResults.empty()) { + auto idx = gi.Search.SearchResults[gi.CurrentSelectedItem].ItemIndex; + gi.Session.SelectItem(idx); + } else { + gi.Session.SelectItem(gi.CurrentSelectedItem); + } + } + + ImGui::EndChild(); + + gContext.NextCommandPaletteActions = {}; + + ImGui::PopID(); + gContext.CurrentCommandPalette = nullptr; + // END this command palette +} + +bool IsAnyItemSelected() { + return gContext.LastCommandPaletteStatus.ItemSelected; +} + +void RemoveCache(const char* name) { + auto& instances = gContext.Instances; + auto id = ImHashStr(name); + if (auto ptr = instances.GetVoidPtr(id)) { + auto instance = reinterpret_cast<Instance*>(ptr); + instances.SetVoidPtr(id, nullptr); + delete instance; + } +} + +void RemoveAllCaches() { + auto& instances = gContext.Instances; + for (auto& entry : instances.Data) { + auto instance = reinterpret_cast<Instance*>(entry.val_p); + entry.val_p = nullptr; + delete instance; + } + instances = {}; +} + +void SetNextWindowAffixedTop(ImGuiCond cond) { + auto viewport = ImGui::GetMainViewport()->Size; + + // Center window horizontally, align top vertically + ImGui::SetNextWindowPos(ImVec2(viewport.x / 2, 0), cond, ImVec2(0.5f, 0.0f)); +} + +void ShowCommandPaletteWindow(const char* name, bool* p_open) { + auto viewport = ImGui::GetMainViewport()->Size; + + SetNextWindowAffixedTop(); + ImGui::SetNextWindowSize(ImVec2(viewport.x * 0.3f, 0.0f)); + ImGui::Begin(name, nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar); + + if (ImGui::IsWindowAppearing()) { + SetNextCommandPaletteSearchBoxFocused(); + } + + ShowCommandPalette(name); + + if (IsAnyItemSelected()) { + *p_open = false; + } + if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) { + // Close popup when user unfocused the command palette window (clicking elsewhere) + *p_open = false; + } + + ImGui::End(); +} + +void Prompt(std::vector<std::string> options) { + IM_ASSERT(gContext.CurrentCommandPalette != nullptr); + IM_ASSERT(gContext.IsExecuting); + IM_ASSERT(!gContext.IsTerminating); + + auto& gi = *gContext.CurrentCommandPalette; + gi.Session.PushOptions(std::move(options)); +} +} // namespace ImCmd diff --git a/source/EditorCommandPalette.hpp b/source/EditorCommandPalette.hpp new file mode 100644 index 0000000..1603f41 --- /dev/null +++ b/source/EditorCommandPalette.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include <imgui.h> +#include <cstddef> +#include <cstdint> +#include <functional> +#include <string> +#include <vector> + +enum ImCmdTextType { + ImCmdTextType_Regular, + ImCmdTextType_Highlight, + ImCmdTextType_COUNT, +}; + +namespace ImCmd { +struct Command { + const char* Name; + std::function<void()> InitialCallback; + std::function<void(int selected_option)> SubsequentCallback; + std::function<void()> TerminatingCallback; +}; + +// Command management +void AddCommand(Command command); +void RemoveCommand(const char* name); + +// Styling +void SetStyleFont(ImCmdTextType type, ImFont* font); +void SetStyleColor(ImCmdTextType type, ImU32 color); +void ClearStyleColor(ImCmdTextType type); //< Clear the style color for the given type, defaulting to ImGuiCol_Text + +// Command palette widget +void SetNextCommandPaletteSearch(const char* text); +void SetNextCommandPaletteSearchBoxFocused(); +void ShowCommandPalette(const char* name); +bool IsAnyItemSelected(); + +void RemoveCache(const char* name); +void RemoveAllCaches(); + +// Command palette widget in a window helper +void SetNextWindowAffixedTop(ImGuiCond cond = 0); +void ShowCommandPaletteWindow(const char* name, bool* p_open); + +// Command responses, only call these in command callbacks (except TerminatingCallback) +void Prompt(std::vector<std::string> options); + +} // namespace ImCmd diff --git a/source/EditorCore.cpp b/source/EditorCore.cpp index 59da2b3..601a850 100644 --- a/source/EditorCore.cpp +++ b/source/EditorCore.cpp @@ -2,17 +2,20 @@ #include "App.hpp" #include "AppConfig.hpp" -#include "CpuMesh.hpp" #include "EditorAccessories.hpp" #include "EditorAttachmentImpl.hpp" +#include "EditorCommandPalette.hpp" #include "EditorNotification.hpp" #include "EditorUtils.hpp" -#include "GameObjectTags.hpp" +#include "GameObject.hpp" #include "Level.hpp" #include "Macros.hpp" #include "Mesh.hpp" #include "Player.hpp" +#include "SceneThings.hpp" #include "ScopeGuard.hpp" +#include "VertexIndex.hpp" +#include "YCombinator.hpp" #define GLFW_INCLUDE_NONE #include <GLFW/glfw3.h> @@ -217,6 +220,84 @@ void PushKeyCodeRecorder(App* app, int* writeKey, bool* writeKeyStatus) { return false; }); } + +struct GobjTreeNodeShowInfo { + EditorInstance* in_editor; + GameObject* out_openPopup = nullptr; +}; + +void GobjTreeNode(GobjTreeNodeShowInfo& showInfo, GameObject* object) { + auto& inspector = showInfo.in_editor->GetInspector(); + + auto attachment = object->GetEditorAttachment(); + if (!attachment) { + attachment = EaGameObject::Create(object).release(); + object->SetEditorAttachment(attachment); // NOTE: takes ownership + } + + ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_DefaultOpen | + ImGuiTreeNodeFlags_OpenOnDoubleClick | + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_SpanAvailWidth | + ImGuiTreeNodeFlags_NoTreePushOnOpen; + if (inspector.selectedItPtr == object) { + flags |= ImGuiTreeNodeFlags_Selected; + } + + ImGui::PushID(reinterpret_cast<uintptr_t>(object)); + // BEGIN tree node + + bool opened = ImGui::TreeNodeEx(attachment->name.c_str(), flags); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + inspector.SelectTarget(EditorInspector::ITT_GameObject, object); + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + { + showInfo.out_openPopup = object; + } + + if (opened) { + ImGui::Indent(); + for (auto& child : object->GetChildren()) { + GobjTreeNode(showInfo, child); + } + ImGui::Unindent(); + } + + // END tree node + ImGui::PopID(); +}; + +#define GAMEOBJECT_CONSTRUCTOR(ClassName) [](GameWorld* world) -> GameObject* { return new ClassName(world); } +struct CreatableGameObject { + GameObject* (*factory)(GameWorld*); + const char* name; + GameObject::Kind kind; +} creatableGameObjects[] = { + { + .factory = GAMEOBJECT_CONSTRUCTOR(GameObject), + .name = "GameObject", + .kind = GameObject::KD_Generic, + }, + { + .factory = GAMEOBJECT_CONSTRUCTOR(SimpleGeometryObject), + .name = "Simple Geometry", + .kind = GameObject::KD_SimpleGeometry, + }, + { + .factory = GAMEOBJECT_CONSTRUCTOR(BuildingObject), + .name = "Building", + .kind = GameObject::KD_Building, + }, + { + .factory = GAMEOBJECT_CONSTRUCTOR(LevelWrapperObject), + .name = "Level Wrapper", + .kind = GameObject::KD_LevelWrapper, + }, +}; +#undef GAMEOBJECT_CONSTRUCTOR } // namespace ProjectBrussel_UNITY_ID EditorInstance::EditorInstance(App* app, GameWorld* world) @@ -228,44 +309,129 @@ EditorInstance::~EditorInstance() { } void EditorInstance::Show() { - if (!mWorld) return; + using namespace ProjectBrussel_UNITY_ID; + using namespace Tags; + + if (!mWorld) { + return; + } auto& io = ImGui::GetIO(); - if (io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_SPACE, false)) { - mEdContentBrowserVisible = !mEdContentBrowserVisible; + + ImGui::BeginMainMenuBar(); + if (ImGui::BeginMenu("View")) { + ImGui::MenuItem("ImGui Demo", nullptr, &mWindowVisible_ImGuiDemo); + ImGui::MenuItem("Command Palette", "Ctrl+Shift+P", &mWindowVisible_CommandPalette); + ImGui::MenuItem("Inspector", nullptr, &mWindowVisible_Inspector); + ImGui::MenuItem("Content Browser", "Ctrl+Space", &mWindowVisible_ContentBrowser); + ImGui::MenuItem("World Structure", nullptr, &mWindowVisible_WorldStructure); + ImGui::MenuItem("World Properties", nullptr, &mWindowVisible_WorldProperties); + ImGui::EndMenu(); } + ImGui::EndMainMenuBar(); - ImGui::Begin("World properties"); - ShowWorldProperties(); - ImGui::End(); + if (mWindowVisible_ImGuiDemo) { + ImGui::ShowDemoWindow(&mWindowVisible_ImGuiDemo); + } - ImGui::Begin("World structure"); - ShowGameObjectInTree(&mWorld->GetRoot()); - ImGui::End(); + if (io.KeyCtrl && io.KeyShift && ImGui::IsKeyPressed(GLFW_KEY_P, false)) { + mWindowVisible_CommandPalette = !mWindowVisible_CommandPalette; + } + if (mWindowVisible_CommandPalette) { + ImCmd::ShowCommandPaletteWindow("Command Palette", &mWindowVisible_CommandPalette); + } - ImGui::Begin("Inspector"); - switch (mEdInspector.selectedItt) { - case EditorInspector::ITT_GameObject: { - ShowInspector(static_cast<GameObject*>(mEdInspector.selectedItPtr)); - } break; + if (io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_SPACE, false)) { + mWindowVisible_ContentBrowser = !mWindowVisible_ContentBrowser; + } + if (mWindowVisible_ContentBrowser) { + mEdContentBrowser.Show(&mWindowVisible_ContentBrowser); + } - case EditorInspector::ITT_Ires: { - auto ires = static_cast<IresObject*>(mEdInspector.selectedItPtr); - ShowInspector(ires); - } break; + if (mWindowVisible_Inspector) { + ImGui::Begin("Inspector"); + switch (mEdInspector.selectedItt) { + case EditorInspector::ITT_GameObject: { + ShowInspector(static_cast<GameObject*>(mEdInspector.selectedItPtr)); + } break; - case EditorInspector::ITT_None: break; + case EditorInspector::ITT_Ires: { + auto ires = static_cast<IresObject*>(mEdInspector.selectedItPtr); + ShowInspector(ires); + } break; + + case EditorInspector::ITT_None: break; + } + ImGui::End(); } - ImGui::End(); - if (mEdContentBrowserVisible) { - mEdContentBrowser.Show(&mEdContentBrowserVisible); + if (mWindowVisible_WorldProperties) { + ImGui::Begin("World properties"); + ShowWorldProperties(); + ImGui::End(); + } + + if (mWindowVisible_WorldStructure) { + ImGui::Begin("World structure"); + { + GobjTreeNodeShowInfo showInfo{ + .in_editor = this, + }; + GobjTreeNode(showInfo, &mWorld->GetRoot()); + + if (showInfo.out_openPopup) { + mPopupCurrent_GameObject = showInfo.out_openPopup; + + ImGui::OpenPopup("GameObject Popup"); + ImGui::SetNextWindowPos(ImGui::GetMousePos()); + } + if (ImGui::BeginPopup("GameObject Popup")) { + // Target no longer selected during popup open + if (!mPopupCurrent_GameObject) { + ImGui::CloseCurrentPopup(); + } + + if (ImGui::BeginMenu("Add child")) { + for (size_t i = 0; i < std::size(creatableGameObjects); ++i) { + auto& info = creatableGameObjects[i]; + if (ImGui::MenuItem(info.name)) { + auto object = info.factory(mWorld); + mPopupCurrent_GameObject->AddChild(object); + } + } + ImGui::EndMenu(); + } + ImGui::Separator(); + if (ImGui::MenuItem("Remove")) { + // TODO + } + ImGui::EndPopup(); + } + } + ImGui::End(); } ShowSpriteViewer(); + + ImGui::ShowNotifications(); } void EditorInstance::ShowWorldProperties() { + if (mApp->IsGameRunning()) { + if (ImGui::Button("Pause")) { + mApp->SetGameRunning(false); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("The game is currently running. Click to pause."); + } + } else { + if (ImGui::Button("Play")) { + mApp->SetGameRunning(true); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("The game is currently paused. Click to run."); + } + } } // TOOD move resource-specific and gameobject-specific inspector code into attachments mechanism @@ -290,17 +456,42 @@ void EditorInstance::ShowInspector(GameObject* object) { } }; - auto type = object->GetTypeTag(); + auto type = object->GetKind(); switch (type) { - case Tags::GOT_Player: { + case GameObject::KD_Player: { ShowFields(); ImGui::Separator(); auto player = static_cast<Player*>(object); + auto ea = static_cast<EaPlayer*>(player->GetEditorAttachment()); auto& kb = player->keybinds; ImGui::Text("Player #%d", player->GetId()); + ImGui::TextUnformatted("Spritesheet: "); + ImGui::SameLine(); + IresObject::ShowReferenceSafe(*this, ea->confSprite.Get()); + if (ImGui::BeginDragDropTarget()) { + if (auto payload = ImGui::AcceptDragDropPayload(IresObject::ToString(IresObject::KD_Spritesheet).data())) { + auto spritesheet = *static_cast<IresSpritesheet* const*>(payload->Data); + ea->confSprite.Attach(spritesheet); + player->sprite.SetDefinition(spritesheet->GetInstance()); + } + ImGui::EndDragDropTarget(); + } + + ImGui::TextUnformatted("Material: "); + ImGui::SameLine(); + IresObject::ShowReferenceSafe(*this, ea->confMaterial.Get()); + if (ImGui::BeginDragDropTarget()) { + if (auto payload = ImGui::AcceptDragDropPayload(IresObject::ToString(IresObject::KD_Material).data())) { + auto material = *static_cast<IresMaterial* const*>(payload->Data); + ea->confMaterial.Attach(material); + player->SetMaterial(material->GetInstance()); + } + ImGui::EndDragDropTarget(); + } + if (ImGui::Button("Load config")) { bool success = player->LoadFromFile(); if (success) { @@ -340,49 +531,37 @@ void EditorInstance::ShowInspector(GameObject* object) { } } break; - case Tags::GOT_LevelWrapper: { + case GameObject::KD_SimpleGeometry: { ShowFields(); ImGui::Separator(); - auto lwo = static_cast<LevelWrapperObject*>(object); + auto sg = static_cast<SimpleGeometryObject*>(object); // TODO } break; - default: { + case GameObject::KD_Building: { ShowFields(); - } break; - } -} + ImGui::Separator(); -void EditorInstance::ShowGameObjectInTree(GameObject* object) { - auto attachment = object->GetEditorAttachment(); - if (!attachment) { - attachment = EaGameObject::Create(object).release(); - object->SetEditorAttachment(attachment); // NOTE: takes ownership - } + auto b = static_cast<BuildingObject*>(object); + // TODO + } break; - ImGuiTreeNodeFlags flags = 0; - flags |= ImGuiTreeNodeFlags_DefaultOpen; - flags |= ImGuiTreeNodeFlags_OpenOnDoubleClick; - flags |= ImGuiTreeNodeFlags_OpenOnArrow; - flags |= ImGuiTreeNodeFlags_SpanAvailWidth; - if (mEdInspector.selectedItPtr == object) { - flags |= ImGuiTreeNodeFlags_Selected; - } + case GameObject::KD_LevelWrapper: { + ShowFields(); + ImGui::Separator(); - if (ImGui::TreeNodeEx(attachment->name.c_str(), flags)) { - if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { - mEdInspector.SelectTarget(EditorInspector::ITT_GameObject, object); - } + auto lwo = static_cast<LevelWrapperObject*>(object); + // TODO + } break; - for (auto& child : object->GetChildren()) { - ShowGameObjectInTree(child); - } - ImGui::TreePop(); + default: { + ShowFields(); + } break; } } -void EditorInstance::OpenSpriteViewer(Sprite* sprite) { +void EditorInstance::OpenSpriteViewer(SpriteDefinition* sprite) { mSpriteView_Instance.Attach(sprite); mSpriteView_Frame = 0; mSpriteView_OpenNextFrame = true; diff --git a/source/EditorCore.hpp b/source/EditorCore.hpp index 13dc2d0..b473b2e 100644 --- a/source/EditorCore.hpp +++ b/source/EditorCore.hpp @@ -10,6 +10,7 @@ #include <memory> #include <string> +// TODO move inspector drawing to this class struct EditorInspector { enum TargetType { ITT_GameObject, @@ -58,12 +59,18 @@ class EditorInstance { private: App* mApp; GameWorld* mWorld; - RcPtr<Sprite> mSpriteView_Instance; + GameObject* mPopupCurrent_GameObject = nullptr; + RcPtr<SpriteDefinition> mSpriteView_Instance; EditorInspector mEdInspector; EditorContentBrowser mEdContentBrowser; int mSpriteView_Frame; bool mSpriteView_OpenNextFrame = false; - bool mEdContentBrowserVisible = false; + bool mWindowVisible_ImGuiDemo = false; + bool mWindowVisible_CommandPalette = false; + bool mWindowVisible_Inspector = true; + bool mWindowVisible_ContentBrowser = true; + bool mWindowVisible_WorldStructure = true; + bool mWindowVisible_WorldProperties = true; public: EditorInstance(App* app, GameWorld* world); @@ -74,7 +81,7 @@ public: EditorInspector& GetInspector() { return mEdInspector; } EditorContentBrowser& GetContentBrowser() { return mEdContentBrowser; } - void OpenSpriteViewer(Sprite* sprite); + void OpenSpriteViewer(SpriteDefinition* sprite); private: void ShowWorldProperties(); @@ -82,7 +89,5 @@ private: void ShowInspector(IresObject* ires); void ShowInspector(GameObject* object); - void ShowGameObjectInTree(GameObject* object); - void ShowSpriteViewer(); }; diff --git a/source/EditorNotification.cpp b/source/EditorNotification.cpp index 1fdd93a..e4a869e 100644 --- a/source/EditorNotification.cpp +++ b/source/EditorNotification.cpp @@ -3,7 +3,9 @@ #include "Macros.hpp" -// #include <IconsFontAwesome.h> +#define IMGUI_DEFINE_MATH_OPERATORS +#include <imgui_internal.h> + #include <chrono> #include <cstdarg> #include <cstdio> @@ -11,163 +13,163 @@ #include <vector> ImGuiToast::ImGuiToast(ImGuiToastType type, int dismissTime) { - IM_ASSERT(type < ImGuiToastType_COUNT); + IM_ASSERT(type < ImGuiToastType_COUNT); - mType = type; - mDismissTime = dismissTime; + mType = type; + mDismissTime = dismissTime; - using namespace std::chrono; - auto timeStamp = system_clock::now().time_since_epoch(); - mCreationTime = duration_cast<milliseconds>(timeStamp).count(); + using namespace std::chrono; + auto timeStamp = system_clock::now().time_since_epoch(); + mCreationTime = duration_cast<milliseconds>(timeStamp).count(); - memset(mTitle, 0, sizeof(mTitle)); - memset(mContent, 0, sizeof(mContent)); + memset(mTitle, 0, sizeof(mTitle)); + memset(mContent, 0, sizeof(mContent)); } ImGuiToast::ImGuiToast(ImGuiToastType type, const char* format, ...) - : ImGuiToast(type) { - if (format) { - va_list args; - va_start(args, format); - SetContent(format, args); - va_end(args); - } + : ImGuiToast(type) { + if (format) { + va_list args; + va_start(args, format); + SetContent(format, args); + va_end(args); + } } ImGuiToast::ImGuiToast(ImGuiToastType type, int dismissTime, const char* format, ...) - : ImGuiToast(type, dismissTime) { - if (format) { - va_list args; - va_start(args, format); - SetContent(format, args); - va_end(args); - } + : ImGuiToast(type, dismissTime) { + if (format) { + va_list args; + va_start(args, format); + SetContent(format, args); + va_end(args); + } } void ImGuiToast::SetTitle(const char* format, ...) { - if (format) { - va_list args; - va_start(args, format); - SetTitle(format, args); - va_end(args); - } + if (format) { + va_list args; + va_start(args, format); + SetTitle(format, args); + va_end(args); + } } void ImGuiToast::SetContent(const char* format, ...) { - if (format) { - va_list args; - va_start(args, format); - SetContent(format, args); - va_end(args); - } + if (format) { + va_list args; + va_start(args, format); + SetContent(format, args); + va_end(args); + } } void ImGuiToast::SetType(const ImGuiToastType& type) { - IM_ASSERT(type < ImGuiToastType_COUNT); - mType = type; + IM_ASSERT(type < ImGuiToastType_COUNT); + mType = type; } const char* ImGuiToast::GetTitle() { - return mTitle; + return mTitle; } const char* ImGuiToast::GetDefaultTitle() { - if (!strlen(mTitle)) { - switch (mType) { - case ImGuiToastType_None: return nullptr; - case ImGuiToastType_Success: return "Success"; - case ImGuiToastType_Warning: return "Warning"; - case ImGuiToastType_Error: return "Error"; - case ImGuiToastType_Info: return "Info"; - case ImGuiToastType_COUNT: UNREACHABLE; - } - } + if (!strlen(mTitle)) { + switch (mType) { + case ImGuiToastType_None: return nullptr; + case ImGuiToastType_Success: return "Success"; + case ImGuiToastType_Warning: return "Warning"; + case ImGuiToastType_Error: return "Error"; + case ImGuiToastType_Info: return "Info"; + case ImGuiToastType_COUNT: UNREACHABLE; + } + } - return mTitle; + return mTitle; } ImGuiToastType ImGuiToast::GetType() { - return mType; + return mType; } ImVec4 ImGuiToast::GetColor() { - switch (mType) { - case ImGuiToastType_None: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White - case ImGuiToastType_Success: return ImVec4(0, 1.0f, 0, 1.0f); // Green - case ImGuiToastType_Warning: return ImVec4(1.0f, 1.0f, 0, 1.0f); // Yellow - case ImGuiToastType_Error: return ImVec4(1.0f, 0, 0, 1.0f); // Red - case ImGuiToastType_Info: return ImVec4(0, 0.616, 1.0f, 1.0f); // Blue - case ImGuiToastType_COUNT: UNREACHABLE; - } - return ImVec4(); + switch (mType) { + case ImGuiToastType_None: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White + case ImGuiToastType_Success: return ImVec4(0, 1.0f, 0, 1.0f); // Green + case ImGuiToastType_Warning: return ImVec4(1.0f, 1.0f, 0, 1.0f); // Yellow + case ImGuiToastType_Error: return ImVec4(1.0f, 0, 0, 1.0f); // Red + case ImGuiToastType_Info: return ImVec4(0, 0.616, 1.0f, 1.0f); // Blue + case ImGuiToastType_COUNT: UNREACHABLE; + } + return ImVec4(); } const char* ImGuiToast::GetIcon() { - switch (mType) { - case ImGuiToastType_None: return nullptr; + switch (mType) { + case ImGuiToastType_None: return nullptr; #if 1 - // TODO add IconFontHeaders and replace with proper icons - case ImGuiToastType_Success: return nullptr; - case ImGuiToastType_Warning: return nullptr; - case ImGuiToastType_Error: return nullptr; - case ImGuiToastType_Info: return nullptr; + // TODO add IconFontHeaders and replace with proper icons + case ImGuiToastType_Success: return nullptr; + case ImGuiToastType_Warning: return nullptr; + case ImGuiToastType_Error: return nullptr; + case ImGuiToastType_Info: return nullptr; #else - case ImGuiToastType_Success: return ICON_FA_CHECK_CIRCLE; - case ImGuiToastType_Warning: return ICON_FA_EXCLAMATION_TRIANGLE; - case ImGuiToastType_Error: return ICON_FA_TIMES_CIRCLE; - case ImGuiToastType_Info: return ICON_FA_INFO_CIRCLE; + case ImGuiToastType_Success: return ICON_FA_CHECK_CIRCLE; + case ImGuiToastType_Warning: return ICON_FA_EXCLAMATION_TRIANGLE; + case ImGuiToastType_Error: return ICON_FA_TIMES_CIRCLE; + case ImGuiToastType_Info: return ICON_FA_INFO_CIRCLE; #endif - case ImGuiToastType_COUNT: UNREACHABLE; - } - return nullptr; + case ImGuiToastType_COUNT: UNREACHABLE; + } + return nullptr; } const char* ImGuiToast::GetContent() { - return this->mContent; + return this->mContent; } uint64_t ImGuiToast::GetElapsedTime() { - using namespace std::chrono; - auto timeStamp = system_clock::now().time_since_epoch(); - auto timeStampI = duration_cast<milliseconds>(timeStamp).count(); - return timeStampI - mCreationTime; + using namespace std::chrono; + auto timeStamp = system_clock::now().time_since_epoch(); + auto timeStampI = duration_cast<milliseconds>(timeStamp).count(); + return timeStampI - mCreationTime; } ImGuiToastPhase ImGuiToast::GetPhase() { - const auto elapsed = GetElapsedTime(); + const auto elapsed = GetElapsedTime(); - if (elapsed > kNotifyFadeInOutTime + mDismissTime + kNotifyFadeInOutTime) { - return ImGuiToastPhase_Expired; - } else if (elapsed > kNotifyFadeInOutTime + mDismissTime) { - return ImGuiToastPhase_FadeOut; - } else if (elapsed > kNotifyFadeInOutTime) { - return ImGuiToastPhase_Wait; - } else { - return ImGuiToastPhase_FadeIn; - } + if (elapsed > kNotifyFadeInOutTime + mDismissTime + kNotifyFadeInOutTime) { + return ImGuiToastPhase_Expired; + } else if (elapsed > kNotifyFadeInOutTime + mDismissTime) { + return ImGuiToastPhase_FadeOut; + } else if (elapsed > kNotifyFadeInOutTime) { + return ImGuiToastPhase_Wait; + } else { + return ImGuiToastPhase_FadeIn; + } } float ImGuiToast::GetFadePercent() { - const auto phase = GetPhase(); - const auto elapsed = GetElapsedTime(); + const auto phase = GetPhase(); + const auto elapsed = GetElapsedTime(); - if (phase == ImGuiToastPhase_FadeIn) - { - return ((float)elapsed / (float)kNotifyFadeInOutTime) * kNotifyOpacity; - } else if (phase == ImGuiToastPhase_FadeOut) - { - return (1.0f - (((float)elapsed - (float)kNotifyFadeInOutTime - (float)mDismissTime) / (float)kNotifyFadeInOutTime)) * kNotifyOpacity; - } + if (phase == ImGuiToastPhase_FadeIn) + { + return ((float)elapsed / (float)kNotifyFadeInOutTime) * kNotifyOpacity; + } else if (phase == ImGuiToastPhase_FadeOut) + { + return (1.0f - (((float)elapsed - (float)kNotifyFadeInOutTime - (float)mDismissTime) / (float)kNotifyFadeInOutTime)) * kNotifyOpacity; + } - return 1.0f * kNotifyOpacity; + return 1.0f * kNotifyOpacity; } void ImGuiToast::SetTitle(const char* format, va_list args) { - vsnprintf(mTitle, sizeof(mTitle), format, args); + vsnprintf(mTitle, sizeof(mTitle), format, args); } void ImGuiToast::SetContent(const char* format, va_list args) { - vsnprintf(mContent, sizeof(mContent), format, args); + vsnprintf(mContent, sizeof(mContent), format, args); } namespace ImGui { @@ -175,100 +177,101 @@ static std::vector<ImGuiToast> notifications; } static bool IsNullOrEmpty(const char* str) { - return !str || !strlen(str); + return !str || !strlen(str); } void ImGui::AddNotification(ImGuiToast toast) { - notifications.push_back(std::move(toast)); + notifications.push_back(std::move(toast)); } void ImGui::RemoveNotification(int index) { - notifications.erase(notifications.begin() + index); + notifications.erase(notifications.begin() + index); } void ImGui::ShowNotifications() { - auto vpSize = GetMainViewport()->Size; + auto vpSize = GetMainViewport()->Size; - float height = 0.0f; - for (auto i = 0; i < notifications.size(); i++) { - auto* currentToast = ¬ifications[i]; + float height = 0.0f; + for (auto i = 0; i < notifications.size(); i++) { + auto* currentToast = ¬ifications[i]; - // Remove toast if expired - if (currentToast->GetPhase() == ImGuiToastPhase_Expired) { - RemoveNotification(i); - continue; - } + // Remove toast if expired + if (currentToast->GetPhase() == ImGuiToastPhase_Expired) { + RemoveNotification(i); + continue; + } - // Get icon, title and other data - const auto icon = currentToast->GetIcon(); - const auto title = currentToast->GetTitle(); - const auto content = currentToast->GetContent(); - const auto defaultTitle = currentToast->GetDefaultTitle(); - const auto opacity = currentToast->GetFadePercent(); // Get opacity based of the current phase + // Get icon, title and other data + const auto icon = currentToast->GetIcon(); + const auto title = currentToast->GetTitle(); + const auto content = currentToast->GetContent(); + const auto defaultTitle = currentToast->GetDefaultTitle(); + const auto opacity = currentToast->GetFadePercent(); // Get opacity based of the current phase - // Window rendering - auto textColor = currentToast->GetColor(); - textColor.w = opacity; + // Window rendering + auto textColor = currentToast->GetColor(); + textColor.w = opacity; - // Generate new unique name for this toast - char windowName[50]; - snprintf(windowName, std::size(windowName), "##TOAST%d", i); + // Generate new unique name for this toast + char windowName[50]; + snprintf(windowName, std::size(windowName), "##TOAST%d", i); - SetNextWindowBgAlpha(opacity); - SetNextWindowPos(ImVec2(vpSize.x - kNotifyPaddingX, vpSize.y - kNotifyPaddingY - height), ImGuiCond_Always, ImVec2(1.0f, 1.0f)); - Begin(windowName, nullptr, kNotifyToastFlags); + SetNextWindowBgAlpha(opacity); + SetNextWindowPos(ImVec2(vpSize.x - kNotifyPaddingX, vpSize.y - kNotifyPaddingY - height), ImGuiCond_Always, ImVec2(1.0f, 1.0f)); + Begin(windowName, nullptr, kNotifyToastFlags); + BringWindowToDisplayFront(GetCurrentWindow()); - // Here we render the toast content - { - PushTextWrapPos(vpSize.x / 3.0f); // We want to support multi-line text, this will wrap the text after 1/3 of the screen width + // Here we render the toast content + { + PushTextWrapPos(vpSize.x / 3.0f); // We want to support multi-line text, this will wrap the text after 1/3 of the screen width - bool wasTitleRendered = false; + bool wasTitleRendered = false; - // If an icon is set - if (!::IsNullOrEmpty(icon)) { - // Render icon text - PushStyleColor(ImGuiCol_Text, textColor); - TextUnformatted(icon); - PopStyleColor(); - wasTitleRendered = true; - } + // If an icon is set + if (!::IsNullOrEmpty(icon)) { + // Render icon text + PushStyleColor(ImGuiCol_Text, textColor); + TextUnformatted(icon); + PopStyleColor(); + wasTitleRendered = true; + } - // If a title is set - if (!::IsNullOrEmpty(title)) { - // If a title and an icon is set, we want to render on same line - if (!::IsNullOrEmpty(icon)) - SameLine(); + // If a title is set + if (!::IsNullOrEmpty(title)) { + // If a title and an icon is set, we want to render on same line + if (!::IsNullOrEmpty(icon)) + SameLine(); - TextUnformatted(title); // Render title text - wasTitleRendered = true; - } else if (!::IsNullOrEmpty(defaultTitle)) { - if (!::IsNullOrEmpty(icon)) - SameLine(); + TextUnformatted(title); // Render title text + wasTitleRendered = true; + } else if (!::IsNullOrEmpty(defaultTitle)) { + if (!::IsNullOrEmpty(icon)) + SameLine(); - TextUnformatted(defaultTitle); // Render default title text (ImGuiToastType_Success -> "Success", etc...) - wasTitleRendered = true; - } + TextUnformatted(defaultTitle); // Render default title text (ImGuiToastType_Success -> "Success", etc...) + wasTitleRendered = true; + } - // In case ANYTHING was rendered in the top, we want to add a small padding so the text (or icon) looks centered vertically - if (wasTitleRendered && !::IsNullOrEmpty(content)) { - SetCursorPosY(GetCursorPosY() + 5.0f); // Must be a better way to do this!!!! - } + // In case ANYTHING was rendered in the top, we want to add a small padding so the text (or icon) looks centered vertically + if (wasTitleRendered && !::IsNullOrEmpty(content)) { + SetCursorPosY(GetCursorPosY() + 5.0f); // Must be a better way to do this!!!! + } - // If a content is set - if (!::IsNullOrEmpty(content)) { - if (wasTitleRendered) { - Separator(); - } + // If a content is set + if (!::IsNullOrEmpty(content)) { + if (wasTitleRendered) { + Separator(); + } - TextUnformatted(content); // Render content text - } + TextUnformatted(content); // Render content text + } - PopTextWrapPos(); - } + PopTextWrapPos(); + } - // Save height for next toasts - height += GetWindowHeight() + kNotifyPaddingMessageY; + // Save height for next toasts + height += GetWindowHeight() + kNotifyPaddingMessageY; - End(); - } + End(); + } } diff --git a/source/EditorNotification.hpp b/source/EditorNotification.hpp index 01350f0..7bb6dab 100644 --- a/source/EditorNotification.hpp +++ b/source/EditorNotification.hpp @@ -5,31 +5,31 @@ #include <cstdint> enum ImGuiToastType { - ImGuiToastType_None, - ImGuiToastType_Success, - ImGuiToastType_Warning, - ImGuiToastType_Error, - ImGuiToastType_Info, - ImGuiToastType_COUNT + ImGuiToastType_None, + ImGuiToastType_Success, + ImGuiToastType_Warning, + ImGuiToastType_Error, + ImGuiToastType_Info, + ImGuiToastType_COUNT }; enum ImGuiToastPhase { - ImGuiToastPhase_FadeIn, - ImGuiToastPhase_Wait, - ImGuiToastPhase_FadeOut, - ImGuiToastPhase_Expired, - ImGuiToastPhase_COUNT + ImGuiToastPhase_FadeIn, + ImGuiToastPhase_Wait, + ImGuiToastPhase_FadeOut, + ImGuiToastPhase_Expired, + ImGuiToastPhase_COUNT }; enum ImGuiToastPos { - ImGuiToastPos_TopLeft, - ImGuiToastPos_TopCenter, - ImGuiToastPos_TopRight, - ImGuiToastPos_BottomLeft, - ImGuiToastPos_BottomCenter, - ImGuiToastPos_BottomRight, - ImGuiToastPos_Center, - ImGuiToastPos_COUNT + ImGuiToastPos_TopLeft, + ImGuiToastPos_TopCenter, + ImGuiToastPos_TopRight, + ImGuiToastPos_BottomLeft, + ImGuiToastPos_BottomCenter, + ImGuiToastPos_BottomRight, + ImGuiToastPos_Center, + ImGuiToastPos_COUNT }; constexpr int kNotifyMaxMsgLength = 4096; // Max message content length @@ -43,35 +43,35 @@ constexpr ImGuiWindowFlags kNotifyToastFlags = ImGuiWindowFlags_AlwaysAutoResize class ImGuiToast { private: - ImGuiToastType mType = ImGuiToastType_None; - char mTitle[kNotifyMaxMsgLength] = {}; - char mContent[kNotifyMaxMsgLength] = {}; - int mDismissTime = kNotifyDefaultDismiss; - uint64_t mCreationTime = 0; + ImGuiToastType mType = ImGuiToastType_None; + char mTitle[kNotifyMaxMsgLength] = {}; + char mContent[kNotifyMaxMsgLength] = {}; + int mDismissTime = kNotifyDefaultDismiss; + uint64_t mCreationTime = 0; public: - ImGuiToast(ImGuiToastType type, int dismissTime = kNotifyDefaultDismiss); - ImGuiToast(ImGuiToastType type, const char* format, ...); - ImGuiToast(ImGuiToastType type, int dismissTime, const char* format, ...); + ImGuiToast(ImGuiToastType type, int dismissTime = kNotifyDefaultDismiss); + ImGuiToast(ImGuiToastType type, const char* format, ...); + ImGuiToast(ImGuiToastType type, int dismissTime, const char* format, ...); - void SetTitle(const char* format, ...); - void SetContent(const char* format, ...); - void SetType(const ImGuiToastType& type); + void SetTitle(const char* format, ...); + void SetContent(const char* format, ...); + void SetType(const ImGuiToastType& type); - const char* GetTitle(); - const char* GetDefaultTitle(); - ImGuiToastType GetType(); - ImVec4 GetColor(); - const char* GetIcon(); - const char* GetContent(); - ; - uint64_t GetElapsedTime(); - ImGuiToastPhase GetPhase(); - float GetFadePercent(); + const char* GetTitle(); + const char* GetDefaultTitle(); + ImGuiToastType GetType(); + ImVec4 GetColor(); + const char* GetIcon(); + const char* GetContent(); + ; + uint64_t GetElapsedTime(); + ImGuiToastPhase GetPhase(); + float GetFadePercent(); private: - void SetTitle(const char* format, va_list args); - void SetContent(const char* format, va_list args); + void SetTitle(const char* format, va_list args); + void SetContent(const char* format, va_list args); }; namespace ImGui { diff --git a/source/FuzzyMatch.cpp b/source/FuzzyMatch.cpp new file mode 100644 index 0000000..0ab604d --- /dev/null +++ b/source/FuzzyMatch.cpp @@ -0,0 +1,174 @@ +// Adapted from https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.h +#include "FuzzyMatch.hpp" + +#include <cctype> +#include <cstring> + +namespace FuzzyMatch { + +namespace P6503_UNITY_ID { + bool SearchRecursive(const char* pattern, const char* src, int& outScore, const char* strBegin, const uint8_t srcMatches[], uint8_t newMatches[], int maxMatches, int& nextMatch, int& recursionCount, int recursionLimit); +} // namespace P6503_UNITY_ID + +bool SearchSimple(char const* pattern, char const* haystack) { + while (*pattern != '\0' && *haystack != '\0') { + if (tolower(*pattern) == tolower(*haystack)) { + ++pattern; + } + ++haystack; + } + + return *pattern == '\0'; +} + +bool Search(char const* pattern, char const* haystack, int& outScore) { + uint8_t matches[256]; + int matchCount = 0; + return Search(pattern, haystack, outScore, matches, sizeof(matches), matchCount); +} + +bool Search(char const* pattern, char const* haystack, int& outScore, uint8_t matches[], int maxMatches, int& outMatches) { + int recursionCount = 0; + int recursionLimit = 10; + int newMatches = 0; + bool result = P6503_UNITY_ID::SearchRecursive(pattern, haystack, outScore, haystack, nullptr, matches, maxMatches, newMatches, recursionCount, recursionLimit); + outMatches = newMatches; + return result; +} + +namespace P6503_UNITY_ID { + bool SearchRecursive(const char* pattern, const char* src, int& outScore, const char* strBegin, const uint8_t srcMatches[], uint8_t newMatches[], int maxMatches, int& nextMatch, int& recursionCount, int recursionLimit) { + // Count recursions + ++recursionCount; + if (recursionCount >= recursionLimit) { + return false; + } + + // Detect end of strings + if (*pattern == '\0' || *src == '\0') { + return false; + } + + // Recursion params + bool recursiveMatch = false; + uint8_t bestRecursiveMatches[256]; + int bestRecursiveScore = 0; + + // Loop through pattern and str looking for a match + bool firstMatch = true; + while (*pattern != '\0' && *src != '\0') { + // Found match + if (tolower(*pattern) == tolower(*src)) { + // Supplied matches buffer was too short + if (nextMatch >= maxMatches) { + return false; + } + + // "Copy-on-Write" srcMatches into matches + if (firstMatch && srcMatches) { + memcpy(newMatches, srcMatches, nextMatch); + firstMatch = false; + } + + // Recursive call that "skips" this match + uint8_t recursiveMatches[256]; + int recursiveScore; + int recursiveNextMatch = nextMatch; + if (SearchRecursive(pattern, src + 1, recursiveScore, strBegin, newMatches, recursiveMatches, sizeof(recursiveMatches), recursiveNextMatch, recursionCount, recursionLimit)) { + // Pick the best recursive score + if (!recursiveMatch || recursiveScore > bestRecursiveScore) { + memcpy(bestRecursiveMatches, recursiveMatches, 256); + bestRecursiveScore = recursiveScore; + } + recursiveMatch = true; + } + + // Advance + newMatches[nextMatch++] = (uint8_t)(src - strBegin); + ++pattern; + } + ++src; + } + + // Determine if full pattern was matched + bool matched = *pattern == '\0'; + + // Calculate score + if (matched) { + const int sequentialBonus = 15; // bonus for adjacent matches + const int separatorBonus = 30; // bonus if match occurs after a separator + const int camelBonus = 30; // bonus if match is uppercase and prev is lower + const int firstLetterBonus = 15; // bonus if the first letter is matched + + const int leadingLetterPenalty = -5; // penalty applied for every letter in str before the first match + const int maxLeadingLetterPenalty = -15; // maximum penalty for leading letters + const int unmatchedLetterPenalty = -1; // penalty for every letter that doesn't matter + + // Iterate str to end + while (*src != '\0') { + ++src; + } + + // Initialize score + outScore = 100; + + // Apply leading letter penalty + int penalty = leadingLetterPenalty * newMatches[0]; + if (penalty < maxLeadingLetterPenalty) { + penalty = maxLeadingLetterPenalty; + } + outScore += penalty; + + // Apply unmatched penalty + int unmatched = (int)(src - strBegin) - nextMatch; + outScore += unmatchedLetterPenalty * unmatched; + + // Apply ordering bonuses + for (int i = 0; i < nextMatch; ++i) { + uint8_t currIdx = newMatches[i]; + + if (i > 0) { + uint8_t prevIdx = newMatches[i - 1]; + + // Sequential + if (currIdx == (prevIdx + 1)) + outScore += sequentialBonus; + } + + // Check for bonuses based on neighbor character value + if (currIdx > 0) { + // Camel case + char neighbor = strBegin[currIdx - 1]; + char curr = strBegin[currIdx]; + if (::islower(neighbor) && ::isupper(curr)) { + outScore += camelBonus; + } + + // Separator + bool neighborSeparator = neighbor == '_' || neighbor == ' '; + if (neighborSeparator) { + outScore += separatorBonus; + } + } else { + // First letter + outScore += firstLetterBonus; + } + } + } + + // Return best result + if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) { + // Recursive score is better than "this" + memcpy(newMatches, bestRecursiveMatches, maxMatches); + outScore = bestRecursiveScore; + return true; + } else if (matched) { + // "this" score is better than recursive + return true; + } else { + // no match + return false; + } + } +} // namespace P6503_UNITY_ID +} // namespace FuzzyMatch diff --git a/source/FuzzyMatch.hpp b/source/FuzzyMatch.hpp new file mode 100644 index 0000000..7a26b7e --- /dev/null +++ b/source/FuzzyMatch.hpp @@ -0,0 +1,10 @@ +// Adapted from https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.h +#pragma once + +#include <cstdint> + +namespace FuzzyMatch { +bool SearchSimple(char const* pattern, char const* haystack); +bool Search(char const* pattern, char const* haystack, int& outScore); +bool Search(char const* pattern, char const* haystack, int& outScore, uint8_t matches[], int maxMatches, int& outMatches); +} // namespace FuzzyMatch diff --git a/source/GameObject.cpp b/source/GameObject.cpp index 0a063e8..bb86acc 100644 --- a/source/GameObject.cpp +++ b/source/GameObject.cpp @@ -1,55 +1,36 @@ #include "GameObject.hpp" -#include "GameObjectTags.hpp" +#include "Player.hpp" +#include "SceneThings.hpp" #include "World.hpp" #include <string_view> #include <utility> -namespace ProjectBrussel_UNITY_ID { -const char* kNamesGOMM[] = { - "None" /* GOMM_None */, - "AllChildren" /* GOMM_AllChildren */, - "SelfAndAllChildren" /* GOMM_SelfAndAllChildren */, - -}; -const char* kNamesGOT[] = { - "GameObject" /* GOT_Generic */, - "Player" /* GOT_Player */, - "Building" /* GOT_Building */, - "LevelWrapper" /* GOT_LevelWrapper */, -}; +using namespace std::literals; +namespace ProjectBrussel_UNITY_ID { bool ValidateGameObjectChild(GameObject* parent, GameObject* child) { return parent->GetWorld() == child->GetWorld(); } } // namespace ProjectBrussel_UNITY_ID -const char* Tags::NameOf(GameObjectMemoryManagement value) { - return ProjectBrussel_UNITY_ID::kNamesGOMM[value]; -} - -const char* Tags::NameOf(GameObjectType value) { - return ProjectBrussel_UNITY_ID::kNamesGOT[value]; -} - void GameObject::FreeRecursive(GameObject* obj) { - auto gomm = obj->GetMemoryManagement(); - bool freeSelf = gomm != Tags::GOMM_SelfAndAllChildren; - bool freeChildren = gomm != Tags::GOMM_SelfAndAllChildren && gomm != Tags::GOMM_AllChildren; - - if (freeChildren) { + if (!obj->mStopFreePropagation) { for (auto child : obj->GetChildren()) { FreeRecursive(obj); } } - if (freeSelf) { - delete obj; - } + delete obj; } GameObject::GameObject(GameWorld* world) - : mWorld{ world } { + : GameObject(KD_Generic, world) { +} + +GameObject::GameObject(Kind kind, GameWorld* world) + : mWorld{ world } + , mKind{ kind } { } GameObject::~GameObject() { @@ -60,6 +41,31 @@ GameObject::~GameObject() { } } +std::string_view GameObject::ToString(Kind kind) { + switch (kind) { + case KD_Generic: return "GameObject"sv; + case KD_Player: return "Player"sv; + case KD_SimpleGeometry: return "SimpleGeometry"sv; + case KD_Building: return "Building"sv; + case KD_LevelWrapper: return "LevelWrapper"sv; + case KD_COUNT: break; + } + return std::string_view(); +} + +GameObject::Kind GameObject::FromString(std::string_view name) { + if (name == "GameObject"sv) return KD_Generic; + if (name == "Player"sv) return KD_Player; + if (name == "SimpleGeometry"sv) return KD_SimpleGeometry; + if (name == "Building"sv) return KD_Building; + if (name == "LevelWrapper"sv) return KD_LevelWrapper; + return KD_COUNT; +} + +GameObject::Kind GameObject::GetKind() const { + return mKind; +} + GameWorld* GameObject::GetWorld() const { return mWorld; } @@ -73,10 +79,12 @@ const PodVector<GameObject*>& GameObject::GetChildren() const { } void GameObject::AddChild(GameObject* child) { + using namespace ProjectBrussel_UNITY_ID; + if (child->mParent) { return; } - if (!ProjectBrussel_UNITY_ID::ValidateGameObjectChild(this, child)) { + if (!ValidateGameObjectChild(this, child)) { return; } @@ -144,20 +152,11 @@ void GameObject::SetRotation(const glm::quat& rotation) { mRot = rotation; } -Tags::GameObjectMemoryManagement GameObject::GetMemoryManagement() const { - return Tags::GOMM_None; -}; - -Tags::GameObjectType GameObject::GetTypeTag() const { - return Tags::GOT_Generic; -} - -const Material* GameObject::GetMeshMaterial() const { - return nullptr; +std::span<const RenderObject> GameObject::GetRenderObjects() const { + return {}; } -const GpuMesh* GameObject::GetMesh() const { - return nullptr; +void GameObject::OnInitialized() { } void GameObject::Awaken() { diff --git a/source/GameObject.hpp b/source/GameObject.hpp index e1e6dd1..be53ca0 100644 --- a/source/GameObject.hpp +++ b/source/GameObject.hpp @@ -1,30 +1,46 @@ #pragma once #include "EditorAttachment.hpp" -#include "GameObjectTags.hpp" #include "Material.hpp" -#include "Mesh.hpp" #include "PodVector.hpp" +#include "Renderer.hpp" +#include "VertexIndex.hpp" #include <glm/glm.hpp> #include <glm/gtc/quaternion.hpp> +#include <span> #include <vector> class GameWorld; class GameObject { -public: // NOTE: public for editors +public: + enum Kind { + KD_Generic, + KD_Player, + KD_SimpleGeometry, + KD_Building, + KD_LevelWrapper, + KD_COUNT, + }; + +private: std::unique_ptr<EditorAttachment> mEditorAttachment = nullptr; GameWorld* mWorld = nullptr; GameObject* mParent = nullptr; PodVector<GameObject*> mChildren; glm::quat mRot{}; glm::vec3 mPos{}; + Kind mKind; + +protected: + bool mStopFreePropagation : 1 = true; public: static void FreeRecursive(GameObject* object); // TODO allow moving between worlds - explicit GameObject(GameWorld* world); + GameObject(GameWorld* world); + GameObject(Kind kind, GameWorld* world); virtual ~GameObject(); GameObject(const GameObject&) = delete; @@ -32,6 +48,10 @@ public: GameObject(GameObject&&) = default; GameObject& operator=(GameObject&&) = default; + static std::string_view ToString(Kind kind); + static Kind FromString(std::string_view name); + Kind GetKind() const; + GameWorld* GetWorld() const; GameObject* GetParent() const; const PodVector<GameObject*>& GetChildren() const; @@ -49,15 +69,11 @@ public: const glm::quat& GetRotation() const; void SetRotation(const glm::quat& rotation); - // Tag - virtual Tags::GameObjectMemoryManagement GetMemoryManagement() const; - virtual Tags::GameObjectType GetTypeTag() const; - // Visuals - virtual const Material* GetMeshMaterial() const; - virtual const GpuMesh* GetMesh() const; + virtual std::span<const RenderObject> GetRenderObjects() const; // Lifetime hooks + virtual void OnInitialized(); virtual void Awaken(); virtual void Resleep(); virtual void Update(); diff --git a/source/GameObjectTags.hpp b/source/GameObjectTags.hpp deleted file mode 100644 index 01a0ca4..0000000 --- a/source/GameObjectTags.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -namespace Tags { -enum GameObjectMemoryManagement { - GOMM_None, - GOMM_AllChildren, - GOMM_SelfAndAllChildren, - GOMM_COUNT, -}; - -const char* NameOf(GameObjectMemoryManagement value); - -enum GameObjectType { - GOT_Generic, ///< All uncategorized game objects. - GOT_Player, - GOT_Building, - GOT_LevelWrapper, - GOT_COUNT, -}; - -const char* NameOf(GameObjectType value); -} // namespace Tags diff --git a/source/Ires.cpp b/source/Ires.cpp index f60fcb7..346e6e6 100644 --- a/source/Ires.cpp +++ b/source/Ires.cpp @@ -74,7 +74,15 @@ void IresObject::SetName(std::string name) { } } -void IresObject::ShowNullName(EditorInstance& editor, Kind kind) { +void IresObject::ShowNameSafe(IresObject* ires) { + if (ires) { + ires->ShowName(); + } else { + ShowNameNull(); + } +} + +void IresObject::ShowNameNull() { ImGui::Text("<null>"); } @@ -86,15 +94,15 @@ void IresObject::ShowName() const { } } -void IresObject::ShowFullName() const { - if (IsAnnoymous()) { - ImGui::Text("<annoymous %p> (%lx-%lx)", (void*)this, mUid.upper, mUid.lower); +void IresObject::ShowReferenceSafe(EditorInstance& editor, IresObject* ires) { + if (ires) { + ires->ShowReference(editor); } else { - ImGui::Text("%s (%lx-%lx)", mName.c_str(), mUid.upper, mUid.lower); + ShowReferenceNull(editor); } } -void IresObject::ShowNullReference(EditorInstance& editor, Kind kind) { +void IresObject::ShowReferenceNull(EditorInstance& editor) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]); ImGui::Text("<null>"); ImGui::PopStyleColor(); diff --git a/source/Ires.hpp b/source/Ires.hpp index 5875264..a4867da 100644 --- a/source/Ires.hpp +++ b/source/Ires.hpp @@ -52,12 +52,14 @@ public: void SetName(std::string name); const Uid& GetUid() const { return mUid; } - static void ShowNullName(EditorInstance& editor, Kind kind); + static void ShowNameSafe(IresObject* ires); + static void ShowNameNull(); void ShowName() const; - void ShowFullName() const; - static void ShowNullReference(EditorInstance& editor, Kind kind); - virtual void ShowReference(EditorInstance& editor); + static void ShowReferenceSafe(EditorInstance& editor, IresObject* ires); + static void ShowReferenceNull(EditorInstance& editor); + void ShowReference(EditorInstance& editor); + virtual void ShowEditor(EditorInstance& editor); EditorAttachment* GetEditorAttachment() const { return mEditorAttachment.get(); } diff --git a/source/Level.cpp b/source/Level.cpp index 71a7473..eec7b85 100644 --- a/source/Level.cpp +++ b/source/Level.cpp @@ -1,5 +1,10 @@ #include "Level.hpp" +LevelWrapperObject::LevelWrapperObject(GameWorld* world) + : GameObject(KD_LevelWrapper, world) { + mStopFreePropagation = true; +} + LevelWrapperObject::~LevelWrapperObject() { for (auto child : GetChildren()) { FreeRecursive(child); diff --git a/source/Level.hpp b/source/Level.hpp index 2359c1d..5c92333 100644 --- a/source/Level.hpp +++ b/source/Level.hpp @@ -4,11 +4,8 @@ class LevelWrapperObject : public GameObject { public: - using GameObject::GameObject; - ~LevelWrapperObject() override; - - virtual Tags::GameObjectMemoryManagement GetMemoryManagement() const override { return Tags::GOMM_AllChildren; } - virtual Tags::GameObjectType GetTypeTag() const override { return Tags::GOT_LevelWrapper; } + LevelWrapperObject(GameWorld* world); + ~LevelWrapperObject() override; }; /// Represents a seralized GameObject tree. diff --git a/source/Material.cpp b/source/Material.cpp index abb2f57..3d227f6 100644 --- a/source/Material.cpp +++ b/source/Material.cpp @@ -346,7 +346,7 @@ void IresMaterial::ShowEditor(EditorInstance& editor) { if (shader) { shader->GetIres()->ShowReference(editor); } else { - IresObject::ShowNullReference(editor, KD_Shader); + IresObject::ShowReferenceNull(editor); } if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload(ToString(KD_Shader).data())) { diff --git a/source/Material.hpp b/source/Material.hpp index 623a022..eb05a73 100644 --- a/source/Material.hpp +++ b/source/Material.hpp @@ -105,6 +105,9 @@ public: void UseUniforms() const; }; +// Initialized in main() +inline RcPtr<Material> gDefaultMaterial; + class IresMaterial : public IresObject { private: RcPtr<Material> mInstance; diff --git a/source/Mesh.cpp b/source/Mesh.cpp index 5b4f708..244e2e3 100644 --- a/source/Mesh.cpp +++ b/source/Mesh.cpp @@ -1,98 +1,54 @@ #include "Mesh.hpp" -#include <algorithm> +#include <cstring> -GpuVertexBuffer::GpuVertexBuffer() { - glGenBuffers(1, &handle); -} +// StandardCpuMesh::StandardCpuMesh() +// : mGpuMesh(new GpuMesh()) { +// mGpuMesh->vertFormat = gVformatStandard; +// mGpuMesh->vertBufBindings.SetBinding(0, new GpuVertexBuffer()); +// mGpuMesh->vertBufBindings.SetBinding(1, new GpuVertexBuffer()); +// mGpuMesh->indexBuf.Attach(new GpuIndexBuffer()); +// } -GpuVertexBuffer::~GpuVertexBuffer() { - glDeleteBuffers(1, &handle); -} +// StandardCpuMesh::~StandardCpuMesh() { +// delete mData; +// } -void GpuVertexBuffer::Upload(const std::byte* data, size_t sizeInBytes) { - glBindBuffer(GL_ARRAY_BUFFER, handle); - glBufferData(GL_ARRAY_BUFFER, sizeInBytes, data, GL_DYNAMIC_DRAW); -} +// void StandardCpuMesh::CreateCpuData() { +// if (!mData) { +// mData = new StandardCpuMeshData(); +// } +// } -GpuIndexBuffer::GpuIndexBuffer() { - glGenBuffers(1, &handle); -} +// GpuVertexBuffer* StandardCpuMesh::GetPosBuffer() const { +// return mGpuMesh->vertBufBindings.bindings[0].Get(); +// } -GpuIndexBuffer::~GpuIndexBuffer() { - glDeleteBuffers(1, &handle); -} +// GpuVertexBuffer* StandardCpuMesh::GetExtraBuffer() const { +// return mGpuMesh->vertBufBindings.bindings[1].Get(); +// } -void GpuIndexBuffer::Upload(const std::byte* data, size_t count) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * Tags::SizeOf(indexType), data, GL_DYNAMIC_DRAW); -} +// bool StandardCpuMesh::UpdatePositions(glm::vec3* pos, size_t count, size_t startVertIndex) { +// if (mData) { +// std::memcpy(&mData->vertPositions[startVertIndex], pos, count * sizeof(glm::vec3)); +// } +// auto posBuf = GetPosBuffer(); +// glBindBuffer(GL_ARRAY_BUFFER, posBuf->handle); +// glBufferSubData(GL_ARRAY_BUFFER, startVertIndex * mGpuMesh->vertFormat->vertexSize, count * sizeof(glm::vec3), pos); +// return true; +// } -void GpuIndexBuffer::Upload(const std::byte* data, Tags::IndexType type, size_t count) { - indexType = type; - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * Tags::SizeOf(type), data, GL_DYNAMIC_DRAW); -} +// bool StandardCpuMesh::UpdateColors(RgbaColor* color, size_t count, size_t starVertIndex) { +// if (!mData) return false; +// // TODO +// } -int BufferBindings::GetMaxBindingIndex() const { - return bindings.size() - 1; -} +// bool StandardCpuMesh::UpdateNormals(glm::vec2* normals, size_t count, size_t startVertIndex) { +// if (!mData) return false; +// // TODO +// } -GpuVertexBuffer* BufferBindings::GetBinding(int index) const { - if (index >= 0 && index < bindings.size()) { - return bindings[index].Get(); - } else { - return nullptr; - } -} - -void BufferBindings::SetBinding(int index, GpuVertexBuffer* buffer) { - int maxBindingIndex = GetMaxBindingIndex(); - if (index > maxBindingIndex) { - int countDelta = index - maxBindingIndex; - bindings.resize(bindings.size() + countDelta); - } - - bindings[index].Attach(buffer); - if (index == maxBindingIndex && buffer == nullptr) { - bindings.pop_back(); - } -} - -void BufferBindings::Clear() { - bindings.clear(); -} - -int VertexElementFormat::GetStride() const { - return Tags::SizeOf(type); -} - -void VertexFormat::AddElement(VertexElementFormat element) { - vertexSize += element.GetStride(); - - int lastIdx = (int)elements.size() - 1; - if (lastIdx >= 0) { - auto& last = elements[lastIdx]; - element.offset = last.offset + last.GetStride(); - } else { - element.offset = 0; - } - - elements.push_back(std::move(element)); -} - -void VertexFormat::RemoveElement(int index) { - auto& element = elements[index]; - vertexSize -= element.GetStride(); - elements.erase(elements.begin() + index); -} - -GpuMesh::GpuMesh() { -} - -GpuMesh::~GpuMesh() { -} - -bool GpuMesh::IsEmpty() const { - return vertFormat == nullptr || indexBuf == nullptr; -} +// bool StandardCpuMesh::UpdateIndices(uint32_t* indices, size_t count, size_t startVertIndex) { +// if (!mData) return false; +// // TODO +// } diff --git a/source/Mesh.hpp b/source/Mesh.hpp index bc755a3..f86fd55 100644 --- a/source/Mesh.hpp +++ b/source/Mesh.hpp @@ -1,83 +1,45 @@ #pragma once -#include "GraphicsTags.hpp" +#include "Color.hpp" +#include "VertexIndex.hpp" +#include "PodVector.hpp" #include "RcPtr.hpp" -#include "SmallVector.hpp" -#include <glad/glad.h> #include <cstddef> #include <cstdint> -#include <vector> +#include <glm/glm.hpp> +#include <memory> -struct GpuVertexBuffer : public RefCounted { - GLuint handle; - int sizeInBytes; - - GpuVertexBuffer(); - ~GpuVertexBuffer(); - - void Upload(const std::byte* data, size_t sizeInBytes); +struct StandardVertexExtra { + float u, v; + uint8_t r, g, b, a; }; -struct GpuIndexBuffer : public RefCounted { - GLuint handle; - Tags::IndexType indexType; - int sizeInBytes; - - GpuIndexBuffer(); - ~GpuIndexBuffer(); - - void Upload(const std::byte* data, size_t count); - void Upload(const std::byte* data, Tags::IndexType type, size_t count); -}; - -struct BufferBindings { - SmallVector<RcPtr<GpuVertexBuffer>, 4> bindings; - - int GetMaxBindingIndex() const; - - /// Safe. Returns nullptr if the index is not bound to any buffers. - GpuVertexBuffer* GetBinding(int index) const; - /// Adds or updates a buffer binding. Setting a binding to nullptr effectively removes the binding. - void SetBinding(int index, GpuVertexBuffer* buffer); - void Clear(); -}; - -struct VertexElementFormat { - /// NOTE: - /// "Automatic" means it will be set inside VertexFormat::AddElement() - /// "Parameter" means it must be set by the user - /* Automatic */ int offset; - /* Parameter */ int bindingIndex; - /* Parameter */ Tags::VertexElementType type; - /* Parameter */ Tags::VertexElementSemantic semantic; - - int GetStride() const; -}; - -struct VertexFormat : public RefCounted { - std::vector<VertexElementFormat> elements; - int vertexSize = 0; - - const std::vector<VertexElementFormat>& GetElements() { return elements; } - void AddElement(VertexElementFormat element); - void RemoveElement(int index); +class StandardCpuMeshData { +public: + PodVector<glm::vec3> vertPositions; + PodVector<StandardVertexExtra> vertExtra; + PodVector<uint32_t> index; + size_t vertexCount; + size_t triangleCount; }; -// Initialized in main() -inline RcPtr<VertexFormat> gVformatStandard{}; -inline RcPtr<VertexFormat> gVformatStandardPacked{}; +class StandardCpuMesh { +// private: +// StandardCpuMeshData* mData = nullptr; +// RcPtr<GpuMesh> mGpuMesh; -// TODO handle immutability -class GpuMesh : public RefCounted { -public: - RcPtr<VertexFormat> vertFormat; - BufferBindings vertBufBindings; - RcPtr<GpuIndexBuffer> indexBuf; +// public: +// StandardCpuMesh(); +// ~StandardCpuMesh(); -public: - GpuMesh(); - ~GpuMesh(); +// GpuVertexBuffer* GetPosBuffer() const; +// GpuVertexBuffer* GetExtraBuffer() const; +// GpuMesh* GetGpuMesh() const { return mGpuMesh.Get(); } - bool IsEmpty() const; +// void CreateCpuData(); +// bool UpdatePositions(glm::vec3* pos, size_t count, size_t startVertIndex); +// bool UpdateColors(RgbaColor* color, size_t count, size_t starVertIndex); +// bool UpdateNormals(glm::vec2* normals, size_t count, size_t startVertIndex); +// bool UpdateIndices(uint32_t* indices, size_t count, size_t startVertIndex); }; diff --git a/source/Player.cpp b/source/Player.cpp index f56d6ea..f97793e 100644 --- a/source/Player.cpp +++ b/source/Player.cpp @@ -19,8 +19,11 @@ std::span<bool> PlayerKeyBinds::GetKeyStatusArray() { } Player::Player(GameWorld* world, int id) - : GameObject(world) + : GameObject(KD_Player, world) , mId{ id } { + renderObject.SetMaterial(gDefaultMaterial.Get()); + renderObject.SetFormat(gVformatStandardPacked.Get(), Tags::IT_16Bit); + renderObject.RebuildIfNecessary(); } void Player::Awaken() { @@ -32,6 +35,8 @@ void Player::Resleep() { } void Player::Update() { + using namespace Tags; + if (keybinds.pressedLeft) { } if (keybinds.pressedRight) { @@ -40,6 +45,38 @@ void Player::Update() { // TODO jump controller // TODO attack controller + + // TODO set default sprite to get rid of this check + if (sprite.GetDefinition()) { + int prevFrame = sprite.GetFrame(); + sprite.PlayFrame(); + int currFrame = sprite.GetFrame(); + if (prevFrame != currFrame) { + uint16_t indices[6]; + Index_U16::Assign(indices, 0); + renderObject.GetIndexBuffer()->Upload((const std::byte*)indices, IT_16Bit, std::size(indices)); + + Vertex_PTC vertices[4]; + Vertex_PTC::Assign(vertices, Rect<float>{ GetPos(), sprite.GetDefinition()->GetBoundingBox() }); + Vertex_PTC::Assign(vertices, 0.0f); + Vertex_PTC::Assign(vertices, RgbaColor(255, 255, 255)); + Vertex_PTC::Assign(vertices, sprite.GetFrameSubregion()); + renderObject.GetVertexBufferBindings().bindings[0]->Upload((const std::byte*)vertices, sizeof(vertices)); + } + } +} + +Material* Player::GetMaterial() const { + return renderObject.GetMaterial(); +} + +void Player::SetMaterial(Material* material) { + renderObject.SetMaterial(material); + renderObject.RebuildIfNecessary(); +} + +std::span<const RenderObject> Player::GetRenderObjects() const { + return { &renderObject, 1 }; } void Player::HandleKeyInput(int key, int action) { diff --git a/source/Player.hpp b/source/Player.hpp index 4ae4f80..d003a25 100644 --- a/source/Player.hpp +++ b/source/Player.hpp @@ -1,7 +1,9 @@ #pragma once #include "GameObject.hpp" -#include "GameObjectTags.hpp" +#include "Material.hpp" +#include "RcPtr.hpp" +#include "Sprite.hpp" #define GLFW_INCLUDE_NONE #include <GLFW/glfw3.h> @@ -24,26 +26,25 @@ struct PlayerKeyBinds { std::span<bool> GetKeyStatusArray(); }; -class PlayerVisual { -public: -}; - class Player : public GameObject { public: std::vector<GLFWkeyboard*> boundKeyboards; PlayerKeyBinds keybinds; - PlayerVisual visuals; + Sprite sprite; + RenderObject renderObject; int mId; public: Player(GameWorld* world, int id); - virtual Tags::GameObjectType GetTypeTag() const override { return Tags::GOT_Player; } - virtual void Awaken() override; virtual void Resleep() override; virtual void Update() override; + Material* GetMaterial() const; + void SetMaterial(Material* material); + virtual std::span<const RenderObject> GetRenderObjects() const override; + int GetId() const { return mId; } void HandleKeyInput(int key, int action); diff --git a/source/Renderer.cpp b/source/Renderer.cpp index dec24ea..f96c3a0 100644 --- a/source/Renderer.cpp +++ b/source/Renderer.cpp @@ -1,19 +1,43 @@ #include "Renderer.hpp" -RenderObject::RenderObject(GpuMesh* mesh, Material* material) { +#include <cassert> + +RenderObject::RenderObject() + : mVao{ GL_NONE } { +} + +RenderObject::~RenderObject() { + DeleteGLObjects(); +} + +GLuint RenderObject::GetGLVao() const { + return mVao; +} + +void RenderObject::RebuildIfNecessary() { + if (mVao != GL_NONE) { + return; + } + + assert(mIndexBuf != nullptr); + assert(mVertexFormat != nullptr); + glGenVertexArrays(1, &mVao); glBindVertexArray(mVao); - auto& vf = mesh->vertFormat; - auto& vBindings = mesh->vertBufBindings.bindings; - auto& shaderInfo = material->GetShader()->GetInfo(); + auto& vBindings = mVertexBufBinding.bindings; + auto& shaderInfo = mMaterial->GetShader()->GetInfo(); // Setup vertex buffers - for (auto& elm : vf->elements) { + for (auto& elm : mVertexFormat->elements) { assert(elm.bindingIndex < vBindings.size()); auto& buffer = vBindings[elm.bindingIndex]; - int index = material->GetShader()->GetInfo().FindInputLocation(elm.semantic); + int index = shaderInfo.FindInputLocation(elm.semantic); + if (index == -1) { + continue; + } + glBindBuffer(GL_ARRAY_BUFFER, buffer->handle); glEnableVertexAttribArray(index); glVertexAttribPointer( @@ -21,23 +45,101 @@ RenderObject::RenderObject(GpuMesh* mesh, Material* material) { Tags::VectorLenOf(elm.type), Tags::FindGLType(elm.type), Tags::IsNormalized(elm.type), - vf->vertexSize, + mVertexFormat->vertexSize, (void*)(uintptr_t)elm.offset); } // Setup index buffer - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh->indexBuf->handle); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuf->handle); glBindVertexArray(GL_NONE); } -RenderObject::~RenderObject() { - glDeleteVertexArrays(1, &mVao); +void RenderObject::SetMaterial(Material* material) { + mMaterial.Attach(material); + DeleteGLObjects(); +} + +void RenderObject::UpdateIndexBuffer(GpuIndexBuffer* indexBuffer) { + mIndexBuf.Attach(indexBuffer); + DeleteGLObjects(); +} + +void RenderObject::UpdateVertexFormat(VertexFormat* vertexFormat) { + mVertexFormat.Attach(vertexFormat); + DeleteGLObjects(); +} + +void RenderObject::UpdateVertexBufferBindings(BufferBindings** bindingsOut) { + *bindingsOut = &mVertexBufBinding; + DeleteGLObjects(); +} + +void RenderObject::SetFormat(VertexFormat* vertexFormat, Tags::IndexType indexFormat) { + mIndexBuf.Attach(new GpuIndexBuffer()); + mIndexBuf->indexType = indexFormat; + mIndexBuf->count = 0; + + mVertexFormat.Attach(vertexFormat); + mVertexBufBinding.Clear(); + for (auto& element : vertexFormat->elements) { + if (mVertexBufBinding.GetBinding(element.bindingIndex) == nullptr) { + mVertexBufBinding.SetBinding(element.bindingIndex, new GpuVertexBuffer()); + } + } +} + +void RenderObject::DeleteGLObjects() { + if (mVao != GL_NONE) { + glDeleteVertexArrays(1, &mVao); + mVao = GL_NONE; + } +} + +void Renderer::BeginFrame(Camera& camera, float currentTime, float deltaTime) { + assert(mInsideFrame == false); + mInsideFrame = true; + mFrame_Cam = &camera; + mFrame_Transform = camera.CalcProjectionMatrix() * camera.CalcViewMatrix(); + mFrame_Time = currentTime; + mFrame_DeltaTime = deltaTime; +} + +void Renderer::EndFrame() { + assert(mInsideFrame == true); + mInsideFrame = false; } -void Renderer::Draw(RenderObject& object) { - auto indexType = object.GetMesh()->indexBuf->indexType; +void Renderer::Draw(const RenderObject* objects, size_t count) { + using namespace Tags; + + assert(mInsideFrame); + + // TODO shader grouping + // TODO material grouping + for (size_t i = 0; i < count; ++i) { + auto& object = objects[i]; + auto indexBuffer = object.GetIndexBuffer(); + auto mat = object.GetMaterial(); + auto shader = mat->GetShader(); - glBindVertexArray(object.GetGLVao()); - glDrawElements(GL_TRIANGLES, 0, Tags::FindGLType(indexType), 0); + glUseProgram(shader->GetProgram()); + + // Autofill uniforms + if (shader->autofillLoc_Transform != kInvalidLocation) { + glUniformMatrix4fv(shader->autofillLoc_Transform, 1, GL_FALSE, &mFrame_Transform[0][0]); + } + if (shader->autofillLoc_Time != kInvalidLocation) { + glUniform1f(shader->autofillLoc_Time, mFrame_Time); + } + if (shader->autofillLoc_DeltaTime != kInvalidLocation) { + glUniform1f(shader->autofillLoc_DeltaTime, mFrame_DeltaTime); + } + + // Material uniforms + mat->UseUniforms(); + + glBindVertexArray(object.GetGLVao()); + glDrawElements(GL_TRIANGLES, indexBuffer->count, indexBuffer->GetIndexTypeGL(), 0); + } } diff --git a/source/Renderer.hpp b/source/Renderer.hpp index 18a1e6e..b4ca735 100644 --- a/source/Renderer.hpp +++ b/source/Renderer.hpp @@ -1,46 +1,61 @@ #pragma once +#include "Camera.hpp" #include "Material.hpp" -#include "Mesh.hpp" #include "RcPtr.hpp" +#include "VertexIndex.hpp" #include <glad/glad.h> +#include <cstddef> #include <glm/glm.hpp> +// TODO add optional support for OpenGL separate attrib binding & only depend on vertex format + +// By default, RenderObject sets a default material but not data buffers. class RenderObject { public: glm::mat4 worldMatrix; private: RcPtr<Material> mMaterial; - RcPtr<GpuMesh> mMesh; + RcPtr<GpuIndexBuffer> mIndexBuf; + RcPtr<VertexFormat> mVertexFormat; + BufferBindings mVertexBufBinding; GLuint mVao; public: - RenderObject(GpuMesh* mesh, Material* material); + RenderObject(); ~RenderObject(); - GLuint GetGLVao() const { return mVao; } + GLuint GetGLVao() const; + void RebuildIfNecessary(); + Material* GetMaterial() const { return mMaterial.Get(); } - GpuMesh* GetMesh() const { return mMesh.Get(); } -}; + void SetMaterial(Material* material); -class Camera { -public: - glm::mat4 viewMatrix; - glm::mat4 projectionMatrix; + GpuIndexBuffer* GetIndexBuffer() const { return mIndexBuf.Get(); } + const VertexFormat* GetVertexFormat() const { return mVertexFormat.Get(); } + const BufferBindings& GetVertexBufferBindings() const { return mVertexBufBinding; } + void UpdateIndexBuffer(GpuIndexBuffer* indexBuffer); + void UpdateVertexFormat(VertexFormat* vertexFormat); + // Assumes the fetched BufferBinding object is modified + void UpdateVertexBufferBindings(BufferBindings** bindingsOut); + void SetFormat(VertexFormat* vertexFormat, Tags::IndexType indexFormat); -public: - void Move(glm::vec3 pos); - void LookAt(glm::vec3 pos); +private: + void DeleteGLObjects(); }; class Renderer { private: - Camera* mCam; + Camera* mFrame_Cam; + glm::mat4 mFrame_Transform; + float mFrame_Time; + float mFrame_DeltaTime; + bool mInsideFrame = false; public: - void BeginFrame(); + void BeginFrame(Camera& camera, float currentTime, float deltaTime); + void Draw(const RenderObject* objects, size_t count); void EndFrame(); - void Draw(RenderObject& object); }; diff --git a/source/SceneThings.cpp b/source/SceneThings.cpp index 894ea58..2a25fb6 100644 --- a/source/SceneThings.cpp +++ b/source/SceneThings.cpp @@ -1,19 +1,71 @@ #include "SceneThings.hpp" -void BuildingObject::SetMeshMaterial(Material* material) { - mMaterial.Attach(material); - // TODO update render +#include "CommonVertexIndex.hpp" +#include "Rect.hpp" + +#include <utility> + +SimpleGeometryObject::SimpleGeometryObject(GameWorld* world) + : GameObject(KD_SimpleGeometry, world) + , mRenderObject() + , mSize{ 20.0f, 20.0f } + , mColor(60, 60, 60) { + mRenderObject.SetMaterial(gDefaultMaterial.Get()); + mRenderObject.SetFormat(gVformatStandardPacked.Get(), Tags::IT_16Bit); + mRenderObject.RebuildIfNecessary(); +} + +void SimpleGeometryObject::SetSize(glm::vec2 size) { + mSize = size; +} + +void SimpleGeometryObject::SetColor(RgbaColor color) { + mColor = color; +} + +std::span<const RenderObject> SimpleGeometryObject::GetRenderObjects() const { + return { &mRenderObject, 1 }; } -const Material* BuildingObject::GetMeshMaterial() const { - return mMaterial.Get(); +void SimpleGeometryObject::UpdateRenderObject() { + using namespace Tags; + + uint16_t indices[6]; + Index_U16::Assign(indices, 0); + mRenderObject.GetIndexBuffer()->Upload((const std::byte*)indices, IT_16Bit, std::size(indices)); + + Vertex_PTC vertices[4]; + Vertex_PTC::Assign(vertices, Rect<float>{ GetPos(), mSize }); + Vertex_PTC::Assign(vertices, 0.0f); + Vertex_PTC::Assign(vertices, mColor); + mRenderObject.GetVertexBufferBindings().bindings[0]->Upload((const std::byte*)vertices, sizeof(vertices)); } -void BuildingObject::SetMesh(GpuMesh* mesh) { - mMesh.Attach(mesh); - // TODO update render +BuildingObject::BuildingObject(GameWorld* world) + : GameObject(KD_Building, world) { + mRenderObject.SetMaterial(gDefaultMaterial.Get()); + mRenderObject.SetFormat(gVformatStandardPacked.Get(), Tags::IT_32Bit); + mRenderObject.RebuildIfNecessary(); } -const GpuMesh* BuildingObject::GetMesh() const { - return mMesh.Get(); +// void BuildingObject::SetMeshMaterial(Material* material) { +// mMaterial.Attach(material); +// // TODO update render +// } + +// const Material* BuildingObject::GetMeshMaterial() const { +// return mMaterial.Get(); +// } + +// void BuildingObject::SetMesh(GpuMesh* mesh) { +// mMesh.Attach(mesh); +// // TODO update render +// } + +// const GpuMesh* BuildingObject::GetMesh() const { +// return mMesh.Get(); +// } + +std::span<const RenderObject> BuildingObject::GetRenderObjects() const { + return { &mRenderObject, 1 }; } diff --git a/source/SceneThings.hpp b/source/SceneThings.hpp index f8ad000..da551c0 100644 --- a/source/SceneThings.hpp +++ b/source/SceneThings.hpp @@ -1,21 +1,42 @@ #pragma once +#include "Color.hpp" #include "GameObject.hpp" +#include "Renderer.hpp" +#include <glm/glm.hpp> #include <vector> -class BuildingObject : public GameObject { +class SimpleGeometryObject : public GameObject { private: - RcPtr<GpuMesh> mMesh; - RcPtr<Material> mMaterial; + RenderObject mRenderObject; + glm::vec2 mSize; + RgbaColor mColor; public: - using GameObject::GameObject; + SimpleGeometryObject(GameWorld* world); + + glm::vec2 GetSize() const { return mSize; } + void SetSize(glm::vec2 size); + RgbaColor GetColor() const { return mColor; } + void SetColor(RgbaColor color); + virtual std::span<const RenderObject> GetRenderObjects() const override; + +private: + void UpdateRenderObject(); +}; - virtual Tags::GameObjectType GetTypeTag() const override { return Tags::GOT_Building; } +class BuildingObject : public GameObject { +private: + RenderObject mRenderObject; + +public: + BuildingObject(GameWorld* world); - void SetMeshMaterial(Material* material); - virtual const Material* GetMeshMaterial() const override; - void SetMesh(GpuMesh* mesh); - virtual const GpuMesh* GetMesh() const override; + // TODO + // void SetMeshMaterial(Material* material); + // virtual const Material* GetMeshMaterial() const override; + // void SetMesh(GpuMesh* mesh); + // virtual const GpuMesh* GetMesh() const override; + virtual std::span<const RenderObject> GetRenderObjects() const override; }; diff --git a/source/Shader.hpp b/source/Shader.hpp index fefa67c..15290e2 100644 --- a/source/Shader.hpp +++ b/source/Shader.hpp @@ -151,6 +151,9 @@ public: bool IsValid() const; }; +// Initialized in main() +inline RcPtr<Shader> gDefaultShader; + class IresShader : public IresObject { private: RcPtr<Shader> mInstance; diff --git a/source/SmallVector.hpp b/source/SmallVector.hpp index 9461800..e33a25d 100644 --- a/source/SmallVector.hpp +++ b/source/SmallVector.hpp @@ -36,6 +36,14 @@ # pragma warning(disable : 4267) // The compiler detected a conversion from size_t to a smaller type. #endif +#if __has_builtin(__builtin_expect) || defined(__GNUC__) +# define LLVM_LIKELY(EXPR) __builtin_expect((bool)(EXPR), true) +# define LLVM_UNLIKELY(EXPR) __builtin_expect((bool)(EXPR), false) +#else +# define LLVM_LIKELY(EXPR) (EXPR) +# define LLVM_UNLIKELY(EXPR) (EXPR) +#endif + template <typename IteratorT> class iterator_range; diff --git a/source/Sprite.cpp b/source/Sprite.cpp index 876b75a..6cff1d3 100644 --- a/source/Sprite.cpp +++ b/source/Sprite.cpp @@ -5,6 +5,7 @@ #include "EditorUtils.hpp" #include "Image.hpp" #include "RapidJsonHelper.hpp" +#include "Rect.hpp" #include <imgui.h> #include <misc/cpp/imgui_stdlib.h> @@ -13,7 +14,7 @@ using namespace std::literals; -bool Sprite::IsValid() const { +bool SpriteDefinition::IsValid() const { return mAtlas != nullptr; } @@ -21,7 +22,7 @@ bool IresSpriteFiles::IsValid() const { return !spriteFiles.empty(); } -Sprite* IresSpriteFiles::CreateInstance() const { +SpriteDefinition* IresSpriteFiles::CreateInstance() const { if (IsValid()) { return nullptr; } @@ -42,7 +43,7 @@ Sprite* IresSpriteFiles::CreateInstance() const { return nullptr; } - auto sprite = std::make_unique<Sprite>(); + auto sprite = std::make_unique<SpriteDefinition>(); sprite->mAtlas.Attach(atlas.release()); sprite->mBoundingBox = atlasOut.elements[0].subregionSize; sprite->mFrames.reserve(atlasOut.elements.size()); @@ -58,7 +59,7 @@ Sprite* IresSpriteFiles::CreateInstance() const { return sprite.release(); } -Sprite* IresSpriteFiles::GetInstance() { +SpriteDefinition* IresSpriteFiles::GetInstance() { if (mInstance == nullptr) { mInstance.Attach(CreateInstance()); } @@ -91,11 +92,11 @@ bool IresSpritesheet::IsValid() const { sheetHSplit != 0; } -void IresSpritesheet::ResplitSpritesheet(Sprite* sprite, const IresSpritesheet* conf) { +void IresSpritesheet::ResplitSpritesheet(SpriteDefinition* sprite, const IresSpritesheet* conf) { ResplitSpritesheet(sprite, conf->sheetWSplit, conf->sheetHSplit, conf->frameCountOverride); } -void IresSpritesheet::ResplitSpritesheet(Sprite* sprite, int wSplit, int hSplit, int frameCount) { +void IresSpritesheet::ResplitSpritesheet(SpriteDefinition* sprite, int wSplit, int hSplit, int frameCount) { auto atlas = sprite->GetAtlas(); auto size = atlas->GetInfo().size; int frameWidth = size.x / wSplit; @@ -131,26 +132,26 @@ void IresSpritesheet::ResplitSpritesheet(Sprite* sprite, int wSplit, int hSplit, } } -Sprite* IresSpritesheet::CreateInstance() const { +SpriteDefinition* IresSpritesheet::CreateInstance() const { if (!IsValid()) { return nullptr; } char path[256]; - snprintf(path, sizeof(path), "%s/Ires/%s", AppConfig::assetDir.c_str(), spritesheetFile.c_str()); + snprintf(path, sizeof(path), "%s/%s", AppConfig::assetDir.c_str(), spritesheetFile.c_str()); auto atlas = std::make_unique<Texture>(); if (atlas->InitFromFile(path) != Texture::EC_Success) { return nullptr; } - auto sprite = std::make_unique<Sprite>(); + auto sprite = std::make_unique<SpriteDefinition>(); sprite->mAtlas.Attach(atlas.release()); ResplitSpritesheet(sprite.get(), this); return sprite.release(); } -Sprite* IresSpritesheet::GetInstance() { +SpriteDefinition* IresSpritesheet::GetInstance() { if (mInstance == nullptr) { mInstance.Attach(CreateInstance()); } @@ -281,18 +282,46 @@ void IresSpritesheet::Read(IresLoadingContext& ctx, const rapidjson::Value& valu BRUSSEL_JSON_GET_DEFAULT(value, "FrameCount", int, frameCountOverride, 0); } -SpriteMesh::SpriteMesh(Sprite* sprite) - : mSprite(sprite) { +Sprite::Sprite() + : mDefinition(nullptr) { } -void SpriteMesh::SetFrame(int frame) { - // TODO +bool Sprite::IsValid() const { + return mDefinition != nullptr; } -void SpriteMesh::PlayFrame() { - // TODO +void Sprite::SetDefinition(SpriteDefinition* definition) { + mDefinition.Attach(definition); + mCurrentFrame = 0; +} + +int Sprite::GetFrame() const { + return mCurrentFrame; +} + +const Subregion& Sprite::GetFrameSubregion() const { + return mDefinition->GetFrames()[mCurrentFrame]; +} + +void Sprite::SetFrame(int frame) { + mCurrentFrame = frame; +} + +void Sprite::PlayFrame() { + ++mTimeElapsed; + if (mTimeElapsed >= mPlaybackSpeed) { + mTimeElapsed -= mPlaybackSpeed; + + int frameCount = mDefinition->GetFrames().size(); + int nextFrame = (mCurrentFrame + 1) % frameCount; + SetFrame(nextFrame); + } +} + +int Sprite::GetPlaybackSpeed() const { + return mPlaybackSpeed; } -void SpriteMesh::SetPlaybackSpeed(int speed) { +void Sprite::SetPlaybackSpeed(int speed) { // TODO } diff --git a/source/Sprite.hpp b/source/Sprite.hpp index b7d357f..de2077f 100644 --- a/source/Sprite.hpp +++ b/source/Sprite.hpp @@ -1,11 +1,12 @@ #pragma once -#include "CpuMesh.hpp" +#include "CommonVertexIndex.hpp" #include "Ires.hpp" -#include "Mesh.hpp" #include "PodVector.hpp" #include "RcPtr.hpp" +#include "Renderer.hpp" #include "Texture.hpp" +#include "VertexIndex.hpp" #include <rapidjson/fwd.h> #include <glm/glm.hpp> @@ -13,7 +14,7 @@ #include <string_view> #include <vector> -class Sprite : public RefCounted { +class SpriteDefinition : public RefCounted { friend class IresSpriteFiles; friend class IresSpritesheet; @@ -31,7 +32,7 @@ public: class IresSpriteFiles : public IresObject { public: - RcPtr<Sprite> mInstance; + RcPtr<SpriteDefinition> mInstance; std::vector<std::string> spriteFiles; public: @@ -41,8 +42,8 @@ public: // NOTE: does not check whether all specified files have the same dimensions bool IsValid() const; - Sprite* CreateInstance() const; - Sprite* GetInstance(); + SpriteDefinition* CreateInstance() const; + SpriteDefinition* GetInstance(); void InvalidateInstance(); void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override; @@ -51,7 +52,7 @@ public: class IresSpritesheet : public IresObject { public: - RcPtr<Sprite> mInstance; + RcPtr<SpriteDefinition> mInstance; std::string spritesheetFile; int sheetWSplit = 1; int sheetHSplit = 1; @@ -63,11 +64,11 @@ public: bool IsValid() const; - static void ResplitSpritesheet(Sprite* sprite, const IresSpritesheet* conf); - static void ResplitSpritesheet(Sprite* sprite, int wSplit, int hSplit, int frameCountOverride = -1); + static void ResplitSpritesheet(SpriteDefinition* sprite, const IresSpritesheet* conf); + static void ResplitSpritesheet(SpriteDefinition* sprite, int wSplit, int hSplit, int frameCountOverride = -1); - Sprite* CreateInstance() const; - Sprite* GetInstance(); + SpriteDefinition* CreateInstance() const; + SpriteDefinition* GetInstance(); void InvalidateInstance(); bool IsFrameCountOverriden() const; @@ -79,27 +80,34 @@ public: void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override; }; -class SpriteMesh { +// TODO +class SpriteCollection { private: - RcPtr<GpuMesh> mMesh; - RcPtr<Sprite> mSprite; - PodVector<StandardVertex> mVertices; - PodVector<uint16_t> mIndices; + std::vector<SpriteDefinition> mSprites; +}; + +class Sprite { +private: + RcPtr<SpriteDefinition> mDefinition; int mCurrentFrame = 0; + int mTimeElapsed = 0; // # of frames per second int mPlaybackSpeed = 5; public: - SpriteMesh(Sprite* sprite); + Sprite(); + + bool IsValid() const; - Sprite* GetSprite() const { return mSprite.Get(); } - GpuMesh* GetGpuMesh() const { return mMesh.Get(); } + SpriteDefinition* GetDefinition() const { return mDefinition.Get(); } + void SetDefinition(SpriteDefinition* definition); - int GetFrame() const { return mCurrentFrame; } + int GetFrame() const; + const Subregion& GetFrameSubregion() const; void SetFrame(int frame); // Update as if a render frame has passed void PlayFrame(); - int GetPlaybackSpeed() const { return mPlaybackSpeed; } + int GetPlaybackSpeed() const; void SetPlaybackSpeed(int speed); }; diff --git a/source/VertexIndex.cpp b/source/VertexIndex.cpp new file mode 100644 index 0000000..ac68289 --- /dev/null +++ b/source/VertexIndex.cpp @@ -0,0 +1,84 @@ +#include "VertexIndex.hpp" + +#include <algorithm> + +GpuVertexBuffer::GpuVertexBuffer() { + glGenBuffers(1, &handle); +} + +GpuVertexBuffer::~GpuVertexBuffer() { + glDeleteBuffers(1, &handle); +} + +void GpuVertexBuffer::Upload(const std::byte* data, size_t sizeInBytes) { + glBindBuffer(GL_ARRAY_BUFFER, handle); + glBufferData(GL_ARRAY_BUFFER, sizeInBytes, data, GL_DYNAMIC_DRAW); +} + +GpuIndexBuffer::GpuIndexBuffer() { + glGenBuffers(1, &handle); +} + +GpuIndexBuffer::~GpuIndexBuffer() { + glDeleteBuffers(1, &handle); +} + +void GpuIndexBuffer::Upload(const std::byte* data, Tags::IndexType type, size_t count) { + this->indexType = type; + this->count = count; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * Tags::SizeOf(type), data, GL_DYNAMIC_DRAW); +} + +int BufferBindings::GetMaxBindingIndex() const { + return bindings.size() - 1; +} + +GpuVertexBuffer* BufferBindings::GetBinding(int index) const { + if (index >= 0 && index < bindings.size()) { + return bindings[index].Get(); + } else { + return nullptr; + } +} + +void BufferBindings::SetBinding(int index, GpuVertexBuffer* buffer) { + int maxBindingIndex = GetMaxBindingIndex(); + if (index > maxBindingIndex) { + int countDelta = index - maxBindingIndex; + bindings.resize(bindings.size() + countDelta); + } + + bindings[index].Attach(buffer); + if (index == maxBindingIndex && buffer == nullptr) { + bindings.pop_back(); + } +} + +void BufferBindings::Clear() { + bindings.clear(); +} + +int VertexElementFormat::GetStride() const { + return Tags::SizeOf(type); +} + +void VertexFormat::AddElement(VertexElementFormat element) { + vertexSize += element.GetStride(); + + int lastIdx = (int)elements.size() - 1; + if (lastIdx >= 0) { + auto& last = elements[lastIdx]; + element.offset = last.offset + last.GetStride(); + } else { + element.offset = 0; + } + + elements.push_back(std::move(element)); +} + +void VertexFormat::RemoveElement(int index) { + auto& element = elements[index]; + vertexSize -= element.GetStride(); + elements.erase(elements.begin() + index); +} diff --git a/source/VertexIndex.hpp b/source/VertexIndex.hpp new file mode 100644 index 0000000..cc5aeca --- /dev/null +++ b/source/VertexIndex.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "GraphicsTags.hpp" +#include "RcPtr.hpp" +#include "SmallVector.hpp" + +#include <glad/glad.h> +#include <cstddef> +#include <cstdint> +#include <vector> + +struct GpuVertexBuffer : public RefCounted { + GLuint handle; + int sizeInBytes; + + GpuVertexBuffer(); + ~GpuVertexBuffer(); + + void Upload(const std::byte* data, size_t sizeInBytes); +}; + +struct GpuIndexBuffer : public RefCounted { + GLuint handle; + Tags::IndexType indexType; + int count; + + GpuIndexBuffer(); + ~GpuIndexBuffer(); + + Tags::IndexType GetIndexType() const { return indexType; } + GLenum GetIndexTypeGL() const { return Tags::FindGLType(indexType); } + + void Upload(const std::byte* data, Tags::IndexType type, size_t count); +}; + +struct BufferBindings { + SmallVector<RcPtr<GpuVertexBuffer>, 4> bindings; + + int GetMaxBindingIndex() const; + + /// Safe. Returns nullptr if the index is not bound to any buffers. + GpuVertexBuffer* GetBinding(int index) const; + /// Adds or updates a buffer binding. Setting a binding to nullptr effectively removes the binding. + void SetBinding(int index, GpuVertexBuffer* buffer); + void Clear(); +}; + +struct VertexElementFormat { + /// NOTE: + /// "Automatic" means it will be set inside VertexFormat::AddElement() + /// "Parameter" means it must be set by the user + /* Automatic */ int offset; + /* Parameter */ int bindingIndex; + /* Parameter */ Tags::VertexElementType type; + /* Parameter */ Tags::VertexElementSemantic semantic; + + int GetStride() const; +}; + +struct VertexFormat : public RefCounted { + SmallVector<VertexElementFormat, 4> elements; + int vertexSize = 0; + + const decltype(elements)& GetElements() const { return elements; } + void AddElement(VertexElementFormat element); + void RemoveElement(int index); +}; + +// Initialized in main() +inline RcPtr<VertexFormat> gVformatStandard{}; +inline RcPtr<VertexFormat> gVformatStandardPacked{}; diff --git a/source/World.cpp b/source/World.cpp index 49aa58f..d4a8344 100644 --- a/source/World.cpp +++ b/source/World.cpp @@ -29,15 +29,8 @@ struct DrawCall { }; } // namespace ProjectBrussel_UNITY_ID -struct GameWorld::RenderData { - void SubmitDrawCalls() { - // TODO - } -}; - GameWorld::GameWorld() - : mRender{ new RenderData() } - , mRoot{ new GameObject(this) } { + : mRoot{ new GameObject(this) } { } GameWorld::~GameWorld() { @@ -45,7 +38,6 @@ GameWorld::~GameWorld() { Resleep(); } - delete mRender; delete mRoot; } @@ -73,9 +65,6 @@ void GameWorld::Update() { }); } -void GameWorld::Draw() { -} - GameObject& GameWorld::GetRoot() { return *mRoot; } diff --git a/source/World.hpp b/source/World.hpp index b12e5ad..288142e 100644 --- a/source/World.hpp +++ b/source/World.hpp @@ -1,16 +1,12 @@ #pragma once - class GameObject; class GameWorld { private: - class RenderData; - RenderData* mRender; - GameObject* mRoot; bool mAwakened = false; -public: +public: GameWorld(); ~GameWorld(); @@ -24,8 +20,6 @@ public: void Resleep(); void Update(); - void Draw(); - const GameObject& GetRoot() const; GameObject& GetRoot(); }; diff --git a/source/YCombinator.hpp b/source/YCombinator.hpp new file mode 100644 index 0000000..b1d2350 --- /dev/null +++ b/source/YCombinator.hpp @@ -0,0 +1,14 @@ +#pragma once + +template <class Func> +struct YCombinator { + // NOTE: implicit constructor allows initializing this + Func func; + + template <class... Ts> + decltype(auto) operator()(Ts&&... args) const { + // NOTE: static_cast<Ts>(args)... is equivalent to std::forward<Ts>(args)... + // written this way so that we don't have to include <utility>, as well as reducing template instanciations to help compile time + return func(*this, static_cast<Ts>(args)...); + } +}; diff --git a/source/main.cpp b/source/main.cpp index b166754..99a11c0 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,9 +1,10 @@ #include "App.hpp" #include "AppConfig.hpp" -#include "EditorNotification.hpp" #include "Ires.hpp" -#include "Mesh.hpp" +#include "Material.hpp" +#include "Shader.hpp" +#include "VertexIndex.hpp" #define GLFW_INCLUDE_NONE #include <GLFW/glfw3.h> @@ -13,47 +14,86 @@ #include <backends/imgui_impl_opengl3.h> #include <glad/glad.h> #include <imgui.h> +#include <imgui_internal.h> #include <cstdlib> #include <cxxopts.hpp> #include <filesystem> #include <string> namespace fs = std::filesystem; +using namespace std::literals; -static void GlfwErrorCallback(int error, const char* description) { - fprintf(stderr, "Glfw Error %d: %s\n", error, description); +struct GlfwUserData { + App* app = nullptr; +}; + +void GlfwErrorCallback(int error, const char* description) { + fprintf(stderr, "[GLFW] Error %d: %s\n", error, description); +} + +void GlfwFramebufferResizeCallback(GLFWwindow* window, int width, int height) { + AppConfig::mainWidnowWidth = width; + AppConfig::mainWindowHeight = height; + AppConfig::mainWindowAspectRatio = (float)width / height; } -static void GlfwMouseCallback(GLFWwindow* window, int button, int action, int mods) { +void GlfwMouseCallback(GLFWwindow* window, int button, int action, int mods) { if (ImGui::GetIO().WantCaptureMouse) { return; } - App* app = static_cast<App*>(glfwGetWindowUserPointer(window)); + auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window)); + auto app = userData->app; app->HandleMouse(button, action); } -static void GlfwMouseMotionCallback(GLFWwindow* window, double xOff, double yOff) { +void GlfwMouseMotionCallback(GLFWwindow* window, double xOff, double yOff) { if (ImGui::GetIO().WantCaptureMouse) { return; } - App* app = static_cast<App*>(glfwGetWindowUserPointer(window)); + auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window)); + auto app = userData->app; app->HandleMouseMotion(xOff, yOff); } -static void GlfwKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { +void GlfwKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { if (ImGui::GetIO().WantCaptureKeyboard) { return; } GLFWkeyboard* keyboard = glfwGetLastActiveKeyboard(); if (keyboard) { - App* app = static_cast<App*>(glfwGetWindowUserPointer(window)); + auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window)); + auto app = userData->app; app->HandleKey(keyboard, key, action); } } +// https://stackoverflow.com/questions/54499256/how-to-find-the-saved-games-folder-programmatically-in-c-c +#if defined(_WIN32) +# if defined(__MINGW32__) +# include <ShlObj.h> +# else +# include <ShlObj_core.h> +# endif +# include <objbase.h> +# pragma comment(lib, "shell32.lib") +# pragma comment(lib, "ole32.lib") +#elif defined(__linux__) +fs::path GetEnvVar(const char* name, const char* backup) { + if (const char* path = std::getenv(name)) { + fs::path dataDir(path); + fs::create_directories(dataDir); + return dataDir; + } else { + fs::path dataDir(backup); + fs::create_directories(dataDir); + return dataDir; + } +} +#endif + int main(int argc, char* argv[]) { using namespace Tags; @@ -72,7 +112,7 @@ int main(int argc, char* argv[]) { auto args = options.parse(argc, argv); bool imguiUseOpenGL3; - { + if (args.count(kImGuiBackend) > 0) { auto imguiBackend = args[kImGuiBackend].as<std::string>(); if (imguiBackend == "opengl2") { imguiUseOpenGL3 = false; @@ -80,11 +120,13 @@ int main(int argc, char* argv[]) { imguiUseOpenGL3 = true; } else { // TODO support more backends? - imguiUseOpenGL3 = false; + imguiUseOpenGL3 = true; } + } else { + imguiUseOpenGL3 = true; } - { + if (args.count(kGameAssetDir) > 0) { auto assetDir = args[kGameAssetDir].as<std::string>(); fs::path assetDirPath(assetDir); @@ -95,6 +137,31 @@ int main(int argc, char* argv[]) { AppConfig::assetDir = std::move(assetDir); AppConfig::assetDirPath = std::move(assetDirPath); + } else { +#if defined(_WIN32) + fs::path dataDir; + + PWSTR path = nullptr; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path); + if (SUCCEEDED(hr)) { + dataDir = fs::path(path) / AppConfig::kAppName; + CoTaskMemFree(path); + + fs::create_directories(dataDir); + } else { + std::string msg; + msg += "Failed to find/create the default user data directory at %APPDATA%. Error code: "; + msg += hr; + throw std::runtime_error(msg); + } +#elif defined(__APPLE__) + // MacOS programming guide recommends apps to hardcode the path - user customization of "where data are stored" is done in Finder + auto dataDir = fs::path("~/Library/Application Support/") / AppConfig::kAppName; + fs::create_directories(dataDir); +#elif defined(__linux__) + auto dataDir = GetEnvVar("XDG_DATA_HOME", "~/.local/share") / AppConfig::kAppName; + fs::create_directories(dataDir); +#endif } if (args.count(kGameDataDir) > 0) { @@ -118,13 +185,7 @@ int main(int argc, char* argv[]) { glfwSetErrorCallback(&GlfwErrorCallback); // Decide GL+GLSL versions -#if defined(IMGUI_IMPL_OPENGL_ES2) - // GL ES 2.0 + GLSL 100 - const char* imguiGlslVersion = "#version 100"; - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); - glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); -#elif defined(__APPLE__) +#if defined(__APPLE__) // GL 3.2 + GLSL 150 const char* imguiGlslVersion = "#version 150"; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); @@ -132,29 +193,37 @@ int main(int argc, char* argv[]) { glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac #else - // GL 3.0 + GLSL 130 + // GL 3.3 + GLSL 130 const char* imguiGlslVersion = "#version 130"; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); #endif - App app; + GlfwUserData glfwUserData; GLFWwindow* window = glfwCreateWindow(1280, 720, "ImGui Command Palette Example", nullptr, nullptr); if (window == nullptr) { return -2; } - glfwSetWindowUserPointer(window, &app); + glfwSetWindowUserPointer(window, &glfwUserData); // Window callbacks are retained by ImGui GLFW backend + glfwSetFramebufferSizeCallback(window, &GlfwFramebufferResizeCallback); glfwSetKeyCallback(window, &GlfwKeyCallback); glfwSetMouseButtonCallback(window, &GlfwMouseCallback); glfwSetCursorPosCallback(window, &GlfwMouseMotionCallback); + { + int width, height; + glfwGetFramebufferSize(window, &width, &height); + GlfwFramebufferResizeCallback(window, width, height); + } + glfwMakeContextCurrent(window); glfwSwapInterval(1); + // TODO setup opengl debug context if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { return -3; } @@ -169,6 +238,9 @@ int main(int argc, char* argv[]) { ImGui_ImplOpenGL2_Init(); } + auto& io = ImGui::GetIO(); + auto& ctx = *ImGui::GetCurrentContext(); + IresManager::instance = new IresManager(); IresManager::instance->DiscoverFilesDesignatedLocation(); @@ -206,45 +278,169 @@ int main(int argc, char* argv[]) { .semantic = VES_Color1, }); - app.Init(); - while (!glfwWindowShouldClose(window)) { - glfwPollEvents(); + // Matches gVformatStandardPacked + gDefaultShader.Attach(new Shader()); + gDefaultShader->InitFromSources(Shader::ShaderSources{ + .vertex = R"""( +#version 330 core +layout(location = 0) in vec3 pos; +layout(location = 1) in vec2 texcoord; +layout(location = 2) in vec4 color; +out vec4 v2fColor; +uniform mat4 transformation; +void main() { + gl_Position = transformation * vec4(pos, 1.0); + v2fColor = color; +} +)"""sv, + .fragment = R"""( +#version 330 core +in vec4 v2fColor; +out vec4 fragColor; +void main() { + fragColor = v2fColor; +} +)"""sv, + }); + { // in vec3 pos; + ShaderMathVariable var; + var.scalarType = GL_FLOAT; + var.width = 1; + var.height = 3; + var.arrayLength = 1; + var.semantic = VES_Position; + var.location = 0; + gDefaultShader->GetInfo().inputs.push_back(std::move(var)); + gDefaultShader->GetInfo().things.try_emplace( + "pos"s, + ShaderThingId{ + .kind = ShaderThingId::KD_Input, + .index = (int)gDefaultShader->GetInfo().inputs.size() - 1, + }); + } + { // in vec2 texcoord; + ShaderMathVariable var; + var.scalarType = GL_FLOAT; + var.width = 1; + var.height = 2; + var.arrayLength = 1; + var.semantic = VES_TexCoords1; + var.location = 1; + gDefaultShader->GetInfo().inputs.push_back(std::move(var)); + gDefaultShader->GetInfo().things.try_emplace( + "texcoord"s, + ShaderThingId{ + .kind = ShaderThingId::KD_Input, + .index = (int)gDefaultShader->GetInfo().inputs.size() - 1, + }); + } + { // in vec4 color; + ShaderMathVariable var; + var.scalarType = GL_FLOAT; + var.width = 1; + var.height = 4; + var.arrayLength = 1; + var.semantic = VES_Color1; + var.location = 2; + gDefaultShader->GetInfo().inputs.push_back(std::move(var)); + gDefaultShader->GetInfo().things.try_emplace( + "color"s, + ShaderThingId{ + .kind = ShaderThingId::KD_Input, + .index = (int)gDefaultShader->GetInfo().inputs.size() - 1, + }); + } + { // out vec4 fragColor; + ShaderMathVariable var; + var.scalarType = GL_FLOAT; + var.width = 1; + var.height = 4; + var.arrayLength = 1; + gDefaultShader->GetInfo().outputs.push_back(std::move(var)); + gDefaultShader->GetInfo().things.try_emplace( + "fragColor"s, + ShaderThingId{ + .kind = ShaderThingId::KD_Output, + .index = (int)gDefaultShader->GetInfo().outputs.size() - 1, + }); + } + // NOTE: autofill uniforms not recorded here - int fbWidth, fbHeight; - glfwGetFramebufferSize(window, &fbWidth, &fbHeight); - glViewport(0, 0, fbWidth, fbHeight); - auto clearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); - glClearColor(clearColor.x * clearColor.w, clearColor.y * clearColor.w, clearColor.z * clearColor.w, clearColor.w); - glClear(GL_COLOR_BUFFER_BIT); + gDefaultMaterial.Attach(new Material()); + gDefaultMaterial->SetShader(gDefaultShader.Get()); - { // Regular draw - app.Update(); - app.Draw(); - } + { // Main loop + App app; + glfwUserData.app = &app; + + // 60 updates per second + constexpr double kMsPerUpdate = 1000.0 / 60; + constexpr double kSecondsPerUpdate = kMsPerUpdate / 1000; + double prevTime = glfwGetTime(); + double accumulatedTime = 0.0; + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + + double currTime = glfwGetTime(); + double deltaTime = prevTime - currTime; - { // ImGui stuff - if (imguiUseOpenGL3) { - ImGui_ImplOpenGL3_NewFrame(); - } else { - ImGui_ImplOpenGL2_NewFrame(); + // In seconds + accumulatedTime += currTime - prevTime; + + // Update + // Play "catch up" to ensure a deterministic number of Update()'s per second + while (accumulatedTime >= kSecondsPerUpdate) { + double beg = glfwGetTime(); + { + app.Update(); + } + double end = glfwGetTime(); + + // Update is taking longer than it should be, start skipping updates + auto diff = end - beg; + if (diff >= kSecondsPerUpdate) { + auto skippedUpdates = (int)(accumulatedTime / kSecondsPerUpdate); + accumulatedTime = 0.0; + fprintf(stderr, "Elapsed time %f, skipped %d updates.", diff, skippedUpdates); + } else { + accumulatedTime -= kSecondsPerUpdate; + } } - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - app.Show(); - ImGui::ShowNotifications(); + int fbWidth = AppConfig::mainWidnowWidth; + int fbHeight = AppConfig::mainWindowHeight; + glfwGetFramebufferSize(window, &fbWidth, &fbHeight); + glViewport(0, 0, fbWidth, fbHeight); + auto clearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + glClearColor(clearColor.x * clearColor.w, clearColor.y * clearColor.w, clearColor.z * clearColor.w, clearColor.w); + glClear(GL_COLOR_BUFFER_BIT); - ImGui::Render(); - if (imguiUseOpenGL3) { - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - } else { - ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData()); + { // Regular draw + app.Draw(currTime, deltaTime); } - } - glfwSwapBuffers(window); + { // ImGui draw + if (imguiUseOpenGL3) { + ImGui_ImplOpenGL3_NewFrame(); + } else { + ImGui_ImplOpenGL2_NewFrame(); + } + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + app.Show(); + + ImGui::Render(); + if (imguiUseOpenGL3) { + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } else { + ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData()); + } + } + + glfwSwapBuffers(window); + } } - app.Shutdown(); if (imguiUseOpenGL3) { ImGui_ImplOpenGL3_Shutdown(); |