diff options
Diffstat (limited to 'src/brussel.engine')
56 files changed, 9036 insertions, 0 deletions
diff --git a/src/brussel.engine/App.cpp b/src/brussel.engine/App.cpp new file mode 100644 index 0000000..8328589 --- /dev/null +++ b/src/brussel.engine/App.cpp @@ -0,0 +1,168 @@ +#include "App.hpp" + +#include "ScopeGuard.hpp" +#include "Utils.hpp" + +#include <rapidjson/document.h> +#include <rapidjson/filereadstream.h> +#include <string> +#include <utility> + +using namespace std::literals; + +App::App() + : mActiveCamera{ &mMainCamera } { + auto& worldRoot = mWorld.GetRoot(); + + constexpr int kPlayerCount = 2; + for (int i = 0; i < kPlayerCount; ++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 + + mMainCamera.name = "Main Camera"s; + mMainCamera.SetEyePos(glm::vec3(0, 0, 1)); + mMainCamera.SetTargetDirection(glm::vec3(0, 0, -1)); + mMainCamera.SetHasPerspective(false); + + do { + auto file = Utils::OpenCstdioFile("assets/GameRendererBindings.json", Utils::Read); + if (!file) break; + DEFER { fclose(file); }; + + char readerBuffer[65536]; + rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer)); + + rapidjson::Document root; + root.ParseStream(stream); + + mWorldRenderer.LoadBindings(root); + } while (false); +} + +App::~App() { +} + +Camera* App::GetActiveCamera() const { + return mActiveCamera; +} + +void App::BindActiveCamera(Camera* camera) { + mActiveCamera = camera; +} + +void App::UnbindActiveCamera() { + mActiveCamera = &mMainCamera; +} + +bool App::IsGameRunning() const { + return mGameRunning; +} + +void App::SetGameRunning(bool running) { + if (mGameRunning != running) { + mGameRunning = running; + if (running) { + mWorld.Awaken(); + } else { + mWorld.Resleep(); + } + } +} + +bool App::IsEditorVisible() const { + return mEditorVisible; +} + +void App::SetEditorVisible(bool visible) { + if (mEditorVisible != visible) { + if (visible) { +#if BRUSSEL_ENABLE_EDITOR + mEditorVisible = true; + if (mEditor == nullptr) { + mEditor = IEditor::CreateInstance(this); + } +#endif + } else { + mEditorVisible = false; + } + } +} + +void App::Show() { + if (mEditorVisible) { + mEditor->Show(); + } +} + +void App::Update() { + if (IsGameRunning()) { + mWorld.Update(); + } +} + +void App::Draw(float currentTime, float deltaTime) { + mWorldRenderer.BeginFrame(*mActiveCamera, 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(); + mWorldRenderer.Draw(renderObjects.data(), obj, renderObjects.size()); + } + + mWorldRenderer.EndFrame(); +} + +void App::HandleMouse(int button, int action) { +} + +void App::HandleMouseMotion(double xOff, double yOff) { +} + +void App::HandleKey(GLFWkeyboard* keyboard, int key, int action) { + if (!mKeyCaptureCallbacks.empty()) { + auto& callback = mKeyCaptureCallbacks.front(); + bool remove = callback(key, action); + if (remove) { + mKeyCaptureCallbacks.pop_front(); + } + } + + switch (key) { + case GLFW_KEY_F3: { + if (action == GLFW_PRESS) { + SetEditorVisible(!IsEditorVisible()); + } + return; + } + } + + for (auto& player : mPlayers) { + for (auto playerKeyboard : player->boundKeyboards) { + if (playerKeyboard == keyboard) { + player->HandleKeyInput(key, action); + } + } + } +} + +void App::PushKeyCaptureCallback(KeyCaptureCallback callback) { + mKeyCaptureCallbacks.push_back(std::move(callback)); +} diff --git a/src/brussel.engine/App.hpp b/src/brussel.engine/App.hpp new file mode 100644 index 0000000..c73c5a1 --- /dev/null +++ b/src/brussel.engine/App.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "Camera.hpp" +#include "EditorCore.hpp" +#include "Player.hpp" +#include "PodVector.hpp" +#include "Renderer.hpp" +#include "World.hpp" + +#define GLFW_INCLUDE_NONE +#include <GLFW/glfw3.h> + +#include <deque> +#include <functional> +#include <memory> +#include <vector> + +using KeyCaptureCallback = std::function<bool(int, int)>; + +class App { +private: + std::deque<KeyCaptureCallback> mKeyCaptureCallbacks; + PodVector<Player*> mPlayers; + std::unique_ptr<IEditor> mEditor; + GameWorld mWorld; + Renderer mWorldRenderer; + Camera mMainCamera; + Camera* mActiveCamera; + // NOTE: should only be true when mEditor != nullptr + bool mEditorVisible = false; + bool mGameRunning = false; + +public: + App(); + ~App(); + + IEditor* GetEditor() { return mEditor.get(); } + GameWorld* GetWorld() { return &mWorld; } + Renderer* GetWorldRenderer() { return &mWorldRenderer; } + + Camera* GetActiveCamera() const; + void BindActiveCamera(Camera* camera); + void UnbindActiveCamera(); + + 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(float currentTime, float deltaTime); + + void HandleMouse(int button, int action); + void HandleMouseMotion(double xOff, double yOff); + void HandleKey(GLFWkeyboard* keyboard, int key, int action); + + void PushKeyCaptureCallback(KeyCaptureCallback callback); +}; diff --git a/src/brussel.engine/AppConfig.hpp b/src/brussel.engine/AppConfig.hpp new file mode 100644 index 0000000..794bee5 --- /dev/null +++ b/src/brussel.engine/AppConfig.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include <imgui.h> +#include <filesystem> +#include <string> + +namespace AppConfig { +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 mainWindowWidth; +inline float mainWindowHeight; +inline float mainWindowAspectRatio; + +// TODO add a bold font +inline ImFont* fontRegular = nullptr; +inline ImFont* fontBold = nullptr; + +// 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; +inline std::string dataDir; +inline std::filesystem::path assetDirPath; +inline std::string assetDir; +} // namespace AppConfig diff --git a/src/brussel.engine/Camera.cpp b/src/brussel.engine/Camera.cpp new file mode 100644 index 0000000..39f0369 --- /dev/null +++ b/src/brussel.engine/Camera.cpp @@ -0,0 +1,46 @@ +#include "Camera.hpp" + +#include "AppConfig.hpp" + +#include <glm/gtc/matrix_transform.hpp> + +Camera::Camera() + : eye(0.0f, 0.0f, 0.0f) + , target(0.0, 0.0f, -2.0f) + , pixelsPerMeter{ 50.0f } // Basic default + , fov{ M_PI / 4 } // 45deg is the convention + , perspective{ false } // +{ +} + +void Camera::SetEyePos(glm::vec3 pos) { + auto lookVector = this->target - /*Old pos*/ this->eye; + this->eye = pos; + this->target = pos + lookVector; +} + +void Camera::SetTargetPos(glm::vec3 pos) { + this->target = pos; +} + +void Camera::SetTargetDirection(glm::vec3 lookVector) { + this->target = this->eye + lookVector; +} + +void Camera::SetHasPerspective(bool perspective) { + this->perspective = perspective; +} + +glm::mat4 Camera::CalcViewMatrix() const { + return glm::lookAt(eye, target, glm::vec3(0, 1, 0)); +} + +glm::mat4 Camera::CalcProjectionMatrix() const { + if (perspective) { + return glm::perspective(fov, AppConfig::mainWindowAspectRatio, 0.1f, 1000.0f); + } else { + float widthMeters = AppConfig::mainWindowWidth / pixelsPerMeter; + float heightMeters = AppConfig::mainWindowHeight / pixelsPerMeter; + return glm::ortho(-widthMeters / 2, +widthMeters / 2, -heightMeters / 2, +heightMeters / 2); + } +} diff --git a/src/brussel.engine/Camera.hpp b/src/brussel.engine/Camera.hpp new file mode 100644 index 0000000..7bf0a6c --- /dev/null +++ b/src/brussel.engine/Camera.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include <glm/glm.hpp> +#include <string> + +class Camera { +public: + std::string name; + glm::vec3 eye; + glm::vec3 target; + + // --- Orthographic settings --- + float pixelsPerMeter; + // --- Orthographic settings --- + + // ---- Perspective settings --- + /// In radians + float fov; + // ---- Perspective settings --- + + bool perspective; + +public: + Camera(); + + void SetEyePos(glm::vec3 pos); + void SetTargetPos(glm::vec3 pos); + void SetTargetDirection(glm::vec3 lookVector); + + bool HasPerspective() const { return perspective; } + void SetHasPerspective(bool perspective); + + glm::mat4 CalcViewMatrix() const; + glm::mat4 CalcProjectionMatrix() const; +}; diff --git a/src/brussel.engine/CommonVertexIndex.cpp b/src/brussel.engine/CommonVertexIndex.cpp new file mode 100644 index 0000000..e9a3ce6 --- /dev/null +++ b/src/brussel.engine/CommonVertexIndex.cpp @@ -0,0 +1,152 @@ +#include "CommonVertexIndex.hpp" + +template <typename 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 <typename 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 <typename 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 <typename 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 <typename TVertex> +static void AssignDepths(TVertex vertices[4], float z) { + for (int i = 0; i < 4; ++i) { + auto& vert = vertices[i]; + vert.z = z; + } +} + +template <typename 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 <typename 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/src/brussel.engine/CommonVertexIndex.hpp b/src/brussel.engine/CommonVertexIndex.hpp new file mode 100644 index 0000000..7e6aa66 --- /dev/null +++ b/src/brussel.engine/CommonVertexIndex.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "Color.hpp" +#include "RcPtr.hpp" +#include "Rect.hpp" +#include "Texture.hpp" +#include "VertexIndex.hpp" + +#include <cstdint> + +// Initialized in main() +inline RcPtr<VertexFormat> gVformatStandard{}; +inline RcPtr<VertexFormat> gVformatStandardSplit{}; +inline RcPtr<VertexFormat> gVformatLines{}; + +// 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/src/brussel.engine/EditorAccessories.cpp b/src/brussel.engine/EditorAccessories.cpp new file mode 100644 index 0000000..821d41e --- /dev/null +++ b/src/brussel.engine/EditorAccessories.cpp @@ -0,0 +1,26 @@ +#include "EditorAccessories.hpp" + +#include "Input.hpp" + +#define GLFW_INCLUDE_NONE +#include <GLFW/glfw3.h> + +#include <imgui.h> + +void EditorKeyboardViewer::Show(bool* open) { + ImGui::Begin("Keyboards", open); + + int count; + GLFWkeyboard** keyboards = glfwGetKeyboards(&count); + + for (int i = 0; i < count; ++i) { + GLFWkeyboard* keyboard = keyboards[i]; + auto attachment = static_cast<GlfwKeyboardAttachment*>(glfwGetKeyboardUserPointer(keyboard)); + + ImGui::BulletText("%s", glfwGetKeyboardName(keyboard)); + ImGui::Indent(); + ImGui::Unindent(); + } + + ImGui::End(); +} diff --git a/src/brussel.engine/EditorAccessories.hpp b/src/brussel.engine/EditorAccessories.hpp new file mode 100644 index 0000000..56a8238 --- /dev/null +++ b/src/brussel.engine/EditorAccessories.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "Player.hpp" + +class EditorKeyboardViewer { +public: + void Show(bool* open = nullptr); +}; diff --git a/src/brussel.engine/EditorAttachment.hpp b/src/brussel.engine/EditorAttachment.hpp new file mode 100644 index 0000000..61b824b --- /dev/null +++ b/src/brussel.engine/EditorAttachment.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include <string> + +class EditorAttachment { +public: + std::string name; + +public: + EditorAttachment(); + virtual ~EditorAttachment() = default; +}; diff --git a/src/brussel.engine/EditorAttachmentImpl.cpp b/src/brussel.engine/EditorAttachmentImpl.cpp new file mode 100644 index 0000000..b09c133 --- /dev/null +++ b/src/brussel.engine/EditorAttachmentImpl.cpp @@ -0,0 +1,23 @@ +#include "EditorAttachmentImpl.hpp" +#include "EditorAttachment.hpp" + +#include <Metadata.hpp> + +EditorAttachment::EditorAttachment() { +} + +std::unique_ptr<EditorAttachment> EaGameObject::Create(GameObject* object) { + EaGameObject* result; + + 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 EaGameObject(); break; + } + + result->name = Metadata::EnumToString(kind); + result->eulerAnglesRotation = glm::eulerAngles(object->GetRotation()); + return std::unique_ptr<EditorAttachment>(result); +} diff --git a/src/brussel.engine/EditorAttachmentImpl.hpp b/src/brussel.engine/EditorAttachmentImpl.hpp new file mode 100644 index 0000000..53bcd37 --- /dev/null +++ b/src/brussel.engine/EditorAttachmentImpl.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "EditorAttachment.hpp" +#include "GameObject.hpp" +#include "Material.hpp" +#include "Player.hpp" +#include "Sprite.hpp" + +#include <memory> + +class EaGameObject : public EditorAttachment { +public: + // NOTE: in degrees + glm::vec3 eulerAnglesRotation; + +public: + static std::unique_ptr<EditorAttachment> Create(GameObject* object); +}; + +class EaPlayer : public EaGameObject { +public: + RcPtr<IresSpritesheet> confSprite; + RcPtr<IresMaterial> confMaterial; +}; + +class EaLevelWrapper : public EaGameObject { +public: +}; + +class EaIresObject : public EditorAttachment { +public: + std::string nameEditingScratch; + bool isEditingName = false; +}; diff --git a/src/brussel.engine/EditorCommandPalette.cpp b/src/brussel.engine/EditorCommandPalette.cpp new file mode 100644 index 0000000..0e7b894 --- /dev/null +++ b/src/brussel.engine/EditorCommandPalette.cpp @@ -0,0 +1,406 @@ +#include "EditorCommandPalette.hpp" + +#include "AppConfig.hpp" +#include "EditorUtils.hpp" +#include "FuzzyMatch.hpp" +#include "Utils.hpp" + +#include <GLFW/glfw3.h> +#include <imgui.h> +#include <misc/cpp/imgui_stdlib.h> +#include <algorithm> +#include <limits> +#include <utility> + +#define IMGUI_DEFINE_MATH_OPERATORS +#include <imgui_internal.h> + +using namespace std::literals; + +bool EditorCommandExecuteContext::IsInitiated() const { + return mCommand != nullptr; +} + +const EditorCommand* EditorCommandExecuteContext::GetCurrentCommand() const { + return mCommand; +} + +void EditorCommandExecuteContext::Initiate(const EditorCommand& command) { + if (mCommand == nullptr) { + mCommand = &command; + } +} + +void EditorCommandExecuteContext::Prompt(std::vector<std::string> options) { + assert(mCommand != nullptr); + mCurrentOptions = std::move(options); + ++mDepth; +} + +void EditorCommandExecuteContext::Finish() { + assert(mCommand != nullptr); + mCommand = nullptr; + mCurrentOptions.clear(); + mDepth = 0; +} + +int EditorCommandExecuteContext::GetExecutionDepth() const { + return mDepth; +} + +struct EditorCommandPalette::SearchResult { + int itemIndex; + int score; + int matchCount; + uint8_t matches[32]; +}; + +struct EditorCommandPalette::Item { + bool hovered = false; + bool held = false; +}; + +EditorCommandPalette::EditorCommandPalette() = default; +EditorCommandPalette::~EditorCommandPalette() = default; + +namespace P6503_UNITY_ID { +std::string MakeCommandName(std::string_view category, std::string_view name) { + std::string result; + constexpr auto infix = ": "sv; + result.reserve(category.size() + infix.size() + name.size()); + result.append(category); + result.append(infix); + result.append(name); + return result; +} +} // namespace P6503_UNITY_ID + +void EditorCommandPalette::AddCommand(std::string_view category, std::string_view name, EditorCommand command) { + command.name = P6503_UNITY_ID::MakeCommandName(category, name); + + auto location = std::lower_bound( + mCommands.begin(), + mCommands.end(), + command, + [](const EditorCommand& a, const EditorCommand& b) -> bool { + return a.name < b.name; + }); + auto iter = mCommands.insert(location, std::move(command)); + + InvalidateSearchResults(); +} + +void EditorCommandPalette::RemoveCommand(std::string_view category, std::string_view name) { + auto commandName = P6503_UNITY_ID::MakeCommandName(category, name); + RemoveCommand(commandName); +} + +void EditorCommandPalette::RemoveCommand(const std::string& commandName) { + struct Comparator { + bool operator()(const EditorCommand& command, const std::string& str) const { + return command.name < str; + } + + bool operator()(const std::string& str, const EditorCommand& command) const { + return str < command.name; + } + }; + + auto range = std::equal_range(mCommands.begin(), mCommands.end(), commandName, Comparator{}); + mCommands.erase(range.first, range.second); + + InvalidateSearchResults(); +} + +void EditorCommandPalette::Show(bool* open) { + // Center window horizontally, align top vertically + ImGui::SetNextWindowPos(ImVec2(ImGui::GetMainViewport()->Size.x / 2, 0), ImGuiCond_Always, ImVec2(0.5f, 0.0f)); + ImGui::SetNextWindowSizeRelScreen(0.3f, 0.0f); + + ImGui::Begin("Command Palette", open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar); + float width = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x; + + if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) || mShouldCloseNextFrame) { + // Close popup when user unfocused the command palette window (clicking elsewhere) + // or some action requested closing this window + mShouldCloseNextFrame = false; + if (open) { + *open = false; + } + } + + if (ImGui::IsWindowAppearing() || mFocusSearchBox) { + mFocusSearchBox = false; + + // 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("##", &mSearchText)) { + // Search string updated, update search results + + mFocusedItemId = 0; + mSearchResults.clear(); + + size_t itemCount; + if (mExeCtx.GetExecutionDepth() == 0) { + itemCount = mCommands.size(); + } else { + itemCount = mExeCtx.mCurrentOptions.size(); + } + + for (size_t i = 0; i < itemCount; ++i) { + const char* text; + if (mExeCtx.GetExecutionDepth() == 0) { + text = mCommands[i].name.c_str(); + } else { + text = mExeCtx.mCurrentOptions[i].c_str(); + } + + SearchResult result{ + .itemIndex = (int)i, + }; + if (FuzzyMatch::Search(mSearchText.c_str(), text, result.score, result.matches, std::size(result.matches), result.matchCount)) { + mSearchResults.push_back(result); + } + } + + std::sort( + mSearchResults.begin(), + mSearchResults.end(), + [](const SearchResult& a, const SearchResult& b) -> bool { + // We want the biggest element first + return a.score > b.score; + }); + } + + ImGui::BeginChild("SearchResults", ImVec2(width, 300), false, ImGuiWindowFlags_AlwaysAutoResize); + auto window = ImGui::GetCurrentWindow(); + + auto& io = ImGui::GetIO(); + auto dlSharedData = ImGui::GetDrawListSharedData(); + + auto textColor = ImGui::GetColorU32(ImGuiCol_Text); + auto itemHoveredColor = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + auto itemActiveColor = ImGui::GetColorU32(ImGuiCol_HeaderActive); + auto itemSelectedColor = ImGui::GetColorU32(ImGuiCol_Header); + + int itemCount = GetItemCount(); + if (mItems.size() < itemCount) { + mItems.resize(itemCount); + } + + // Flag used to delay item selection until after the loop ends + bool selectFocusedItem = false; + for (size_t i = 0; i < itemCount; ++i) { + auto id = window->GetID(static_cast<int>(i)); + + ImVec2 size{ + ImGui::GetContentRegionAvail().x, + dlSharedData->Font->FontSize, + }; + ImRect rect{ + window->DC.CursorPos, + window->DC.CursorPos + ImGui::CalcItemSize(size, 0.0f, 0.0f), + }; + + bool& hovered = mItems[i].hovered; + bool& held = mItems[i].held; + if (held && hovered) { + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemActiveColor); + } else if (hovered) { + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemHoveredColor); + } else if (mFocusedItemId == i) { + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemSelectedColor); + } + + auto item = GetItem(i); + if (item.indexType == SearchResultIndex) { + // Iterating search results: draw text with highlights at matched chars + + auto& searchResult = mSearchResults[i]; + auto textPos = window->DC.CursorPos; + int rangeBegin; + int rangeEnd; + int lastRangeEnd = 0; + + auto DrawCurrentRange = [&]() -> void { + if (rangeBegin != lastRangeEnd) { + // Draw normal text between last highlighted range end and current highlighted range start + auto begin = item.text + lastRangeEnd; + auto end = item.text + rangeBegin; + window->DrawList->AddText(textPos, textColor, begin, end); + + auto segmentSize = dlSharedData->Font->CalcTextSizeA(dlSharedData->Font->FontSize, std::numeric_limits<float>::max(), 0.0f, begin, end); + textPos.x += segmentSize.x; + } + + auto begin = item.text + rangeBegin; + auto end = item.text + rangeEnd; + window->DrawList->AddText(AppConfig::fontBold, AppConfig::fontBold->FontSize, textPos, textColor, begin, end); + + auto segmentSize = AppConfig::fontBold->CalcTextSizeA(AppConfig::fontBold->FontSize, std::numeric_limits<float>::max(), 0.0f, begin, end); + textPos.x += segmentSize.x; + }; + + assert(searchResult.matchCount >= 1); + rangeBegin = searchResult.matches[0]; + rangeEnd = rangeBegin; + + int lastCharIdx = -1; + for (int j = 0; j < searchResult.matchCount; ++j) { + int charIdx = searchResult.matches[j]; + + if (charIdx == lastCharIdx + 1) { + // These 2 indices are equal, extend our current range by 1 + ++rangeEnd; + } else { + DrawCurrentRange(); + lastRangeEnd = rangeEnd; + rangeBegin = charIdx; + rangeEnd = charIdx + 1; + } + + lastCharIdx = charIdx; + } + + // Draw the remaining range (if any) + if (rangeBegin != rangeEnd) { + DrawCurrentRange(); + } + + // Draw the text after the last range (if any) + window->DrawList->AddText(textPos, textColor, item.text + rangeEnd); // Draw until \0 + } else { + // Iterating everything else: draw text as-is, there is no highlights + + window->DrawList->AddText(window->DC.CursorPos, textColor, item.text); + } + + ImGui::ItemSize(rect); + if (!ImGui::ItemAdd(rect, id)) { + continue; + } + if (ImGui::ButtonBehavior(rect, id, &hovered, &held)) { + mFocusedItemId = i; + selectFocusedItem = true; + } + } + + if (ImGui::IsKeyPressed(GLFW_KEY_UP)) { + mFocusedItemId = std::max(mFocusedItemId - 1, 0); + } else if (ImGui::IsKeyPressed(GLFW_KEY_DOWN)) { + mFocusedItemId = std::min(mFocusedItemId + 1, itemCount - 1); + } + if (ImGui::IsKeyPressed(GLFW_KEY_ENTER) || selectFocusedItem) { + SelectFocusedItem(); + } + + ImGui::EndChild(); + + ImGui::End(); +} + +size_t EditorCommandPalette::GetItemCount() const { + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + if (mSearchText.empty()) { + return mCommands.size(); + } else { + return mSearchResults.size(); + } + } else { + if (mSearchText.empty()) { + return mExeCtx.mCurrentOptions.size(); + } else { + return mSearchResults.size(); + } + } +} + +EditorCommandPalette::ItemInfo EditorCommandPalette::GetItem(size_t idx) const { + ItemInfo option; + + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + if (mSearchText.empty()) { + option.text = mCommands[idx].name.c_str(); + option.command = &mCommands[idx]; + option.itemId = idx; + option.indexType = DirectIndex; + } else { + auto id = mSearchResults[idx].itemIndex; + option.text = mCommands[id].name.c_str(); + option.command = &mCommands[id]; + option.itemId = id; + option.indexType = SearchResultIndex; + } + option.itemType = CommandItem; + } else { + assert(mExeCtx.GetCurrentCommand() != nullptr); + if (mSearchText.empty()) { + option.text = mExeCtx.mCurrentOptions[idx].c_str(); + option.command = mExeCtx.GetCurrentCommand(); + option.itemId = idx; + option.indexType = DirectIndex; + } else { + auto id = mSearchResults[idx].itemIndex; + option.text = mExeCtx.mCurrentOptions[id].c_str(); + option.command = mExeCtx.GetCurrentCommand(); + option.itemId = id; + option.indexType = SearchResultIndex; + } + option.itemType = CommandOptionItem; + } + + return option; +} + +void EditorCommandPalette::SelectFocusedItem() { + if (mFocusedItemId < 0 || mFocusedItemId >= GetItemCount()) { + return; + } + + auto selectedItem = GetItem(mFocusedItemId); + auto& command = *selectedItem.command; + + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + assert(!mExeCtx.IsInitiated()); + + mExeCtx.Initiate(*selectedItem.command); + if (command.callback) { + command.callback(mExeCtx); + + mFocusSearchBox = true; + // Don't invalidate search results if no further actions have been requested (returning to global list of commands) + if (mExeCtx.IsInitiated()) { + InvalidateSearchResults(); + } + } else { + mExeCtx.Finish(); + } + } else { + assert(mExeCtx.IsInitiated()); + assert(command.subsequentCallback); + command.subsequentCallback(mExeCtx, selectedItem.itemId); + + mFocusSearchBox = true; + InvalidateSearchResults(); + } + + // This action terminated execution, close command palette window + if (!mExeCtx.IsInitiated()) { + if (command.terminate) { + command.terminate(); + } + mShouldCloseNextFrame = true; + } +} + +void EditorCommandPalette::InvalidateSearchResults() { + mSearchText.clear(); + mSearchResults.clear(); + mFocusedItemId = 0; +} diff --git a/src/brussel.engine/EditorCommandPalette.hpp b/src/brussel.engine/EditorCommandPalette.hpp new file mode 100644 index 0000000..101344d --- /dev/null +++ b/src/brussel.engine/EditorCommandPalette.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include <imgui.h> +#include <cstddef> +#include <functional> +#include <string> +#include <string_view> + +class EditorCommandExecuteContext; +class EditorCommand { +public: + std::string name; + std::function<void(EditorCommandExecuteContext& ctx)> callback; + std::function<void(EditorCommandExecuteContext& ctx, size_t selectedOptionId)> subsequentCallback; + std::function<void()> terminate; +}; + +class EditorCommandExecuteContext { + friend class EditorCommandPalette; + +private: + const EditorCommand* mCommand = nullptr; + std::vector<std::string> mCurrentOptions; + int mDepth = 0; + +public: + bool IsInitiated() const; + const EditorCommand* GetCurrentCommand() const; + void Initiate(const EditorCommand& command); + + void Prompt(std::vector<std::string> options); + void Finish(); + + /// Return the number of prompts that the user is currently completing. For example, when the user opens command + /// palette fresh and selects a command, 0 is returned. If the command asks some prompt, and then the user selects + /// again, 1 is returned. + int GetExecutionDepth() const; +}; + +class EditorCommandPalette { +private: + struct SearchResult; + struct Item; + + std::vector<EditorCommand> mCommands; + std::vector<Item> mItems; + std::vector<SearchResult> mSearchResults; + std::string mSearchText; + EditorCommandExecuteContext mExeCtx; + int mFocusedItemId = 0; + bool mFocusSearchBox = false; + bool mShouldCloseNextFrame = false; + +public: + EditorCommandPalette(); + ~EditorCommandPalette(); + + EditorCommandPalette(const EditorCommandPalette&) = delete; + EditorCommandPalette& operator=(const EditorCommandPalette&) = delete; + EditorCommandPalette(EditorCommandPalette&&) = default; + EditorCommandPalette& operator=(EditorCommandPalette&&) = default; + + void AddCommand(std::string_view category, std::string_view name, EditorCommand command); + void RemoveCommand(std::string_view category, std::string_view name); + void RemoveCommand(const std::string& commandName); + + void Show(bool* open = nullptr); + + enum ItemType { + CommandItem, + CommandOptionItem, + }; + + enum IndexType { + DirectIndex, + SearchResultIndex, + }; + + struct ItemInfo { + const char* text; + const EditorCommand* command; + int itemId; + ItemType itemType; + IndexType indexType; + }; + + size_t GetItemCount() const; + ItemInfo GetItem(size_t idx) const; + + void SelectFocusedItem(); + +private: + void InvalidateSearchResults(); +}; diff --git a/src/brussel.engine/EditorCore.hpp b/src/brussel.engine/EditorCore.hpp new file mode 100644 index 0000000..726f43e --- /dev/null +++ b/src/brussel.engine/EditorCore.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include <memory> + +class App; +class SpriteDefinition; + +class IEditorInspector { +public: + enum TargetType { + ITT_GameObject, + ITT_Ires, + ITT_Level, + ITT_None, + }; + +public: + virtual ~IEditorInspector() = default; + virtual void SelectTarget(TargetType type, void* object) = 0; +}; + +class IEditorContentBrowser { +public: + virtual ~IEditorContentBrowser() = default; +}; + +class IEditor { +public: + static std::unique_ptr<IEditor> CreateInstance(App* app); + virtual ~IEditor() = default; + + virtual void OnGameStateChanged(bool running) = 0; + virtual void Show() = 0; + + virtual IEditorInspector& GetInspector() = 0; + virtual IEditorContentBrowser& GetContentBrowser() = 0; + + virtual void OpenSpriteViewer(SpriteDefinition* sprite) = 0; +}; diff --git a/src/brussel.engine/EditorCorePrivate.cpp b/src/brussel.engine/EditorCorePrivate.cpp new file mode 100644 index 0000000..3efa33c --- /dev/null +++ b/src/brussel.engine/EditorCorePrivate.cpp @@ -0,0 +1,1171 @@ +#include "EditorCorePrivate.hpp" + +#include "App.hpp" +#include "AppConfig.hpp" +#include "EditorAccessories.hpp" +#include "EditorAttachmentImpl.hpp" +#include "EditorCommandPalette.hpp" +#include "EditorUtils.hpp" +#include "GameObject.hpp" +#include "Mesh.hpp" +#include "Player.hpp" +#include "SceneThings.hpp" +#include "VertexIndex.hpp" + +#include <ImGuiNotification.hpp> +#include <Macros.hpp> +#include <Metadata.hpp> +#include <ScopeGuard.hpp> +#include <YCombinator.hpp> + +#define GLFW_INCLUDE_NONE +#include <GLFW/glfw3.h> + +#include <imgui.h> +#include <misc/cpp/imgui_stdlib.h> +#include <rapidjson/document.h> +#include <rapidjson/filereadstream.h> +#include <rapidjson/filewritestream.h> +#include <rapidjson/writer.h> +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <functional> +#include <glm/gtc/quaternion.hpp> +#include <glm/gtc/type_ptr.hpp> +#include <glm/gtx/quaternion.hpp> +#include <limits> +#include <memory> +#include <string> +#include <string_view> +#include <utility> + +using namespace std::literals; + +namespace ProjectBrussel_UNITY_ID { +// TODO handle internal state internally and move this to EditorUtils.hpp +enum RenamableSelectableAction { + RSA_None, + RSA_Selected, + RSA_RenameCommitted, + RSA_RenameCancelled, +}; +RenamableSelectableAction RenamableSelectable(const char* displayName, bool selected, bool& renaming, std::string& renamingScratchBuffer) // +{ + RenamableSelectableAction result = RSA_None; + + ImGuiSelectableFlags flags = 0; + // When renaming, disable all other entries that is not the one being renamed + if (renaming && !selected) { + flags |= ImGuiSelectableFlags_Disabled; + } + + if (renaming && selected) { + // State: being renamed + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 }); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0); + ImGui::SetKeyboardFocusHere(); + if (ImGui::InputText("##Rename", &renamingScratchBuffer, ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue)) { + // Confirm + renaming = false; + result = RSA_RenameCommitted; + } + ImGui::PopStyleVar(2); + + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + // Cancel + renaming = false; + result = RSA_RenameCancelled; + } + } else { + // State: normal + + if (ImGui::Selectable(displayName, selected, flags)) { + result = RSA_Selected; + } + } + + return result; +} +} // namespace ProjectBrussel_UNITY_ID + +void EditorInspector::SelectTarget(TargetType type, void* object) { + selectedItt = type; + selectedItPtr = object; + renaming = false; + renamingScratchBuffer.clear(); +} + +EditorContentBrowser::EditorContentBrowser(EditorInspector* inspector) + : mInspector{ inspector } { +} + +EditorContentBrowser::~EditorContentBrowser() { +} + +void EditorContentBrowser::Show(bool* open) { + using namespace ProjectBrussel_UNITY_ID; + + ImGuiWindowFlags windowFlags; + if (mDocked) { + // Center window horizontally, align bottom vertically + auto& viewportSize = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowPos(ImVec2(viewportSize.x / 2, viewportSize.y), ImGuiCond_Always, ImVec2(0.5f, 1.0f)); + ImGui::SetNextWindowSizeRelScreen(0.8f, mBrowserHeight); + windowFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse; + } else { + windowFlags = 0; + } + ImGui::Begin("Content Browser", open, windowFlags); + + ImGui::Splitter(true, kSplitterThickness, &mSplitterLeft, &mSplitterRight, kLeftPaneMinWidth, kRightPaneMinWidth); + + ImGui::BeginChild("LeftPane", ImVec2(mSplitterLeft - kPadding, 0.0f)); + { + if (ImGui::Selectable("Settings", mPane == P_Settings)) { + mPane = P_Settings; + } + if (ImGui::Selectable("Ires", mPane == P_Ires)) { + mPane = P_Ires; + } + if (ImGui::Selectable("Levels", mPane == P_Level)) { + mPane = P_Level; + } + } + ImGui::EndChild(); + + ImGui::SameLine(0.0f, kPadding + kSplitterThickness + kPadding); + ImGui::BeginChild("RightPane"); // Fill remaining space + auto origItt = mInspector->selectedItt; + auto origItPtr = mInspector->selectedItPtr; + switch (mPane) { + case P_Settings: { + ImGui::Checkbox("Docked", &mDocked); + ImGui::SliderFloat("Height", &mBrowserHeight, 0.1f, 1.0f); + } break; + + case P_Ires: { + bool isIttIres = origItt == EditorInspector::ITT_Ires; + + if (ImGui::Button("New")) { + ImGui::OpenPopup("New Ires"); + } + if (ImGui::BeginPopup("New Ires")) { + for (int i = 0; i < (int)IresObject::KD_COUNT; ++i) { + auto kind = static_cast<IresObject::Kind>(i); + if (ImGui::MenuItem(Metadata::EnumToString(kind).data())) { + auto ires = IresObject::Create(kind); + auto [DISCARD, success] = IresManager::instance->Add(ires.get()); + if (success) { + (void)ires.release(); + } + } + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button("Refresh list") || + ImGui::IsKeyPressed(ImGuiKey_F5)) + { + // TODO + } + + ImGui::SameLine(); + if (ImGui::Button("Save", !isIttIres)) { + auto ires = static_cast<IresObject*>(origItPtr); + IresManager::instance->Save(ires); + } + + ImGui::SameLine(); + if (ImGui::Button("Reload", !isIttIres)) { + auto ires = static_cast<IresObject*>(origItPtr); + IresManager::instance->Reload(ires); + } + + ImGui::SameLine(); + if (ImGui::Button("Rename", !isIttIres) || + (isIttIres && ImGui::IsKeyPressed(ImGuiKey_F2, false))) + { + auto ires = static_cast<IresObject*>(origItPtr); + mInspector->renaming = true; + mInspector->renamingScratchBuffer = ires->GetName(); + } + + ImGui::SameLine(); + if (ImGui::Button("Delete", !isIttIres) || + (isIttIres && ImGui::IsKeyPressed(ImGuiKey_Delete, false))) + { + ImGui::OpenPopup("Delete Ires"); + } + bool openedDummy = true; + if (ImGui::BeginPopupModal("Delete Ires", &openedDummy, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) { + if (ImGui::Button("Confirm")) { + auto ires = static_cast<IresObject*>(origItPtr); + IresManager::instance->Delete(ires); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button("...")) { + ImGui::OpenPopup("More Actions"); + } + if (ImGui::BeginPopup("More Actions")) { + if (ImGui::MenuItem("Rewrite all Ires to disk")) { + IresManager::instance->OverwriteAllToDisk(); + } + ImGui::EndPopup(); + } + + auto& objects = IresManager::instance->GetObjects(); + for (auto it = objects.begin(); it != objects.end(); ++it) { + auto ires = it->second.Get(); + auto& name = ires->GetName(); + + bool selected = origItPtr == ires; + + switch (RenamableSelectable(name.c_str(), selected, mInspector->renaming, mInspector->renamingScratchBuffer)) { + case RSA_Selected: { + mInspector->SelectTarget(EditorInspector::ITT_Ires, ires); + } break; + + case RSA_RenameCommitted: { + ires->SetName(std::move(mInspector->renamingScratchBuffer)); + } break; + + // Do nothing + case RSA_RenameCancelled: + case RSA_None: break; + } + if (!mInspector->renaming) { + if (ImGui::BeginDragDropSource()) { + auto kindName = Metadata::EnumToString(ires->GetKind()); + // Reason: intentionally using pointer as payload + ImGui::SetDragDropPayload(kindName.data(), &ires, sizeof(ires)); // NOLINT(bugprone-sizeof-expression) + ImGui::Text("%s '%s'", kindName.data(), name.c_str()); + ImGui::EndDragDropSource(); + } + } + } + } break; + + case P_Level: { + bool isIttLevel = origItt == EditorInspector::ITT_Level; + + if (ImGui::Button("New")) { + auto uid = Uid::Create(); + auto& ldObj = LevelManager::instance->AddLevel(uid); + mInspector->SelectTarget(EditorInspector::ITT_Level, &ldObj); + mInspector->renaming = true; + mInspector->renamingScratchBuffer = ldObj.name; + } + + if (ImGui::Button("Save", !isIttLevel)) { + auto ldObj = static_cast<LevelManager::LoadableObject*>(origItPtr); + LevelManager::instance->SaveLevel(ldObj->level->GetUid()); + } + + auto& objects = LevelManager::instance->mObjByUid; + for (auto it = objects.begin(); it != objects.end(); ++it) { + auto& uid = it->first; + auto& ldObj = it->second; + auto* level = ldObj.level.Get(); + bool selected = origItPtr == &ldObj; + const char* displayName = ldObj.name.c_str(); + if (strcmp(displayName, "") == 0) { + displayName = "<unnamed level>"; + } + + switch (RenamableSelectable(displayName, selected, mInspector->renaming, mInspector->renamingScratchBuffer)) { + case RSA_Selected: { + mInspector->SelectTarget(EditorInspector::ITT_Level, &ldObj); + } break; + + case RSA_RenameCommitted: { + ldObj.name = std::move(mInspector->renamingScratchBuffer); + } break; + + // Do nothing + case RSA_RenameCancelled: + case RSA_None: break; + } + if (!mInspector->renaming) { + if (ImGui::BeginDragDropSource()) { + // Reason: intentionally using pointer as payload + ImGui::SetDragDropPayload(BRUSSEL_TAG_Level, &ldObj, sizeof(ldObj)); // NOLINT(bugprone-sizeof-expression) + ImGui::Text(BRUSSEL_Uid_FORMAT_STR, BRUSSEL_Uid_FORMAT_EXPAND(uid)); + ImGui::TextUnformatted(ldObj.name.c_str()); + ImGui::EndDragDropSource(); + } + } + } + } break; + } + ImGui::EndChild(); + + ImGui::End(); +} + +namespace ProjectBrussel_UNITY_ID { +glm::quat CalcQuaternionFromDegreesEulerAngle(glm::vec3 eulerAngleDegrees) { + glm::vec3 eulerAngleRadians; + eulerAngleRadians.x = eulerAngleDegrees.x / 180 * M_PI; + eulerAngleRadians.y = eulerAngleDegrees.y / 180 * M_PI; + eulerAngleRadians.z = eulerAngleDegrees.z / 180 * M_PI; + return glm::quat(eulerAngleRadians); +} + +void PushKeyCodeRecorder(App* app, int* writeKey, bool* writeKeyStatus) { + app->PushKeyCaptureCallback([=](int key, int action) { + // Allow the user to cancel by pressing Esc + if (key == GLFW_KEY_ESCAPE) { + return true; + } + + if (action == GLFW_PRESS) { + *writeKey = key; + *writeKeyStatus = writeKeyStatus; + return true; + } + 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 + +void SaveRendererBindings(const Renderer& renderer) { + auto file = Utils::OpenCstdioFile("assets/GameRendererBindings.json", Utils::WriteTruncate); + if (!file) return; + DEFER { + fclose(file); + }; + + char writerBuffer[65536]; + rapidjson::FileWriteStream stream(file, writerBuffer, sizeof(writerBuffer)); + rapidjson::Writer writer(stream); + + rapidjson::Document root(rapidjson::kObjectType); + renderer.SaveBindings(root, root); + + root.Accept(writer); +} +} // namespace ProjectBrussel_UNITY_ID + +std::unique_ptr<IEditor> IEditor::CreateInstance(App* app) { + return std::make_unique<EditorInstance>(app); +} + +EditorInstance::EditorInstance(App* app) + : mApp{ app } + , mEdContentBrowser(&mEdInspector) + , mEdGuides(app, this) // +{ + mEditorCamera.name = "Editor Camera"s; + mEditorCamera.SetEyePos(glm::vec3(0, 0, 1)); + mEditorCamera.SetTargetDirection(glm::vec3(0, 0, -1)); + app->BindActiveCamera(&mEditorCamera); +} + +EditorInstance::~EditorInstance() { +} + +void EditorInstance::OnGameStateChanged(bool running) { + if (running) { + mApp->UnbindActiveCamera(); + } else { + mApp->BindActiveCamera(&mEditorCamera); + } +} + +void EditorInstance::Show() { + using namespace ProjectBrussel_UNITY_ID; + using namespace Tags; + + auto world = mApp->GetWorld(); + auto& io = ImGui::GetIO(); + + 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("Keyboard Viewer", nullptr, &mWindowVisible_KeyboardViewer); + ImGui::MenuItem("World Structure", nullptr, &mWindowVisible_WorldStructure); + ImGui::MenuItem("World Properties", nullptr, &mWindowVisible_WorldProperties); + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + + if (mWindowVisible_ImGuiDemo) { + ImGui::ShowDemoWindow(&mWindowVisible_ImGuiDemo); + } + + if (io.KeyCtrl && io.KeyShift && ImGui::IsKeyPressed(GLFW_KEY_P, false)) { + mWindowVisible_CommandPalette = !mWindowVisible_CommandPalette; + } + if (mWindowVisible_CommandPalette) { + mEdCommandPalette.Show(&mWindowVisible_CommandPalette); + } + + if (io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_SPACE, false)) { + mWindowVisible_ContentBrowser = !mWindowVisible_ContentBrowser; + } + if (mWindowVisible_ContentBrowser) { + mEdContentBrowser.Show(&mWindowVisible_ContentBrowser); + } + + if (mWindowVisible_KeyboardViewer) { + mEdKbViewer.Show(&mWindowVisible_KeyboardViewer); + } + + auto& camera = *mApp->GetActiveCamera(); + ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y); + ImGuizmo::SetDrawlist(ImGui::GetBackgroundDrawList()); + + if (IsCurrentCameraEditor() && mEcm == ECM_Side3D) { + float viewManipulateRight = io.DisplaySize.x; + float viewManipulateTop = 0; + + // TODO get rid of this massive hack: how to manage const better for intuitively read-only, but write doesn't-care data? + auto& lastFrameInfo = const_cast<RendererFrameInfo&>(mApp->GetWorldRenderer()->GetLastFrameInfo()); + auto& view = lastFrameInfo.matrixView; + auto& proj = lastFrameInfo.matrixProj; + +#if 0 + ImGuizmo::ViewManipulate( + glm::value_ptr(view), + 200.0f, // TODO + ImVec2(viewManipulateRight - 128, viewManipulateTop), + ImVec2(128, 128), + 0x10101010); + // Extract eye and target position from view matrix + // - View matrix transforms world space to view space + // - Inverse view matrix should transform view space into world space + // - In view space, camera's pos is (0,0,0) and the look/forward vector should be (0,0,-1) + auto invView = glm::inverse(view); + camera.eye = invView * glm::vec4(0, 0, 0, 1); + camera.target = camera.eye + glm::vec3(invView * glm::vec4(0, 0, -1, 1)); +#endif + + // TODO draw this as a part of the world so it doesn't block objects +#if 0 + glm::mat4 identity(1.00f); + ImGuizmo::DrawGrid( + glm::value_ptr(view), + glm::value_ptr(proj), + glm::value_ptr(identity), + 100.f); +#endif + + { // Camera controls + auto cameraPos = camera.eye; + auto cameraForward = glm::normalize(camera.target - camera.eye); + // Always move on the horzontal flat plane + cameraForward.y = 0.0f; + + if (mMoveCamKeyboard) { + if (ImGui::IsKeyDown(ImGuiKey_W)) { + cameraPos += mMoveCamSlideSpeed * cameraForward; + } + if (ImGui::IsKeyDown(ImGuiKey_S)) { + auto cameraBack = glm::normalize(-cameraForward); + cameraPos += mMoveCamSlideSpeed * cameraBack; + } + if (ImGui::IsKeyDown(ImGuiKey_A)) { + auto cameraRight = glm::normalize(glm::cross(cameraForward, glm::vec3(0, 1, 0))); + auto cameraLeft = -cameraRight; + cameraPos += mMoveCamSlideSpeed * cameraLeft; + } + if (ImGui::IsKeyDown(ImGuiKey_D)) { + auto cameraRight = glm::normalize(glm::cross(cameraForward, glm::vec3(0, 1, 0))); + cameraPos += mMoveCamSlideSpeed * cameraRight; + } + if (ImGui::IsKeyDown(ImGuiKey_Space)) { + cameraPos.y += mMoveCamSlideSpeed; + } + if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) { + cameraPos.y -= mMoveCamSlideSpeed; + } + } + + if (mMoveCamScrollWheel) { + cameraPos += cameraForward * io.MouseWheel * mMoveCamScrollSpeed; + } + + camera.SetEyePos(cameraPos); + } + } else { + { // Camera controls + auto cameraPos = camera.eye; + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !io.WantCaptureMouse && !mDragCam_Happening) { + mDragCam_CamInitial = camera.eye; + mDragCam_CursorInitial = ImGui::GetMousePos(); + mDragCam_Happening = true; + } + if (mDragCam_Happening) { + auto newPos = ImGui::GetMousePos(); + // NOTE: we are emulating as if the mouse is dragging the "canvas", through moving the camera in the opposite direction of the natural position delta + float deltaX = mDragCam_CursorInitial.x - newPos.x; + cameraPos.x = mDragCam_CamInitial.x + deltaX / camera.pixelsPerMeter; + float deltaY = -(mDragCam_CursorInitial.y - newPos.y); // Invert Y delta because ImGui uses top-left origin (mouse moving down translates to positive value, but in our coordinate system down is negative) + cameraPos.y = mDragCam_CamInitial.y + deltaY / camera.pixelsPerMeter; + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { + mDragCam_Happening = false; + } + + if (mMoveCamKeyboard) { + if (ImGui::IsKeyDown(ImGuiKey_W)) { + cameraPos.y += mMoveCamSlideSpeed; + } + if (ImGui::IsKeyDown(ImGuiKey_S)) { + cameraPos.y -= mMoveCamSlideSpeed; + } + if (ImGui::IsKeyDown(ImGuiKey_A)) { + cameraPos.x -= mMoveCamSlideSpeed; + } + if (ImGui::IsKeyDown(ImGuiKey_D)) { + cameraPos.x += mMoveCamSlideSpeed; + } + } + + if (mMoveCamScrollWheel) { + cameraPos.z = std::clamp(cameraPos.z + io.MouseWheel, 0.1f, 100.0f); + } + + camera.SetEyePos(cameraPos); + } + } + + if (mWindowVisible_Inspector) { + ImGui::Begin("Inspector"); + switch (mEdInspector.selectedItt) { + case EditorInspector::ITT_GameObject: { + auto object = static_cast<GameObject*>(mEdInspector.selectedItPtr); + ShowInspector(object); + } break; + + case EditorInspector::ITT_Ires: { + auto ires = static_cast<IresObject*>(mEdInspector.selectedItPtr); + ShowInspector(ires); + } break; + + case EditorInspector::ITT_Level: { + auto ldObj = static_cast<LevelManager::LoadableObject*>(mEdInspector.selectedItPtr); + ShowInspector(ldObj); + } break; + + case EditorInspector::ITT_None: break; + } + ImGui::End(); + } + + if (mWindowVisible_WorldProperties) { + ImGui::Begin("World properties"); + ShowWorldProperties(); + ImGui::End(); + } + + if (mWindowVisible_WorldStructure) { + ImGui::Begin("World structure"); + GobjTreeNodeShowInfo showInfo{ + .in_editor = this, + }; + GobjTreeNode(showInfo, &world->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(world); + mPopupCurrent_GameObject->AddChild(object); + } + } + ImGui::EndMenu(); + } + ImGui::Separator(); + if (ImGui::MenuItem("Remove")) { + ImGui::DialogConfirmation("Are you sure you want to delete this GameObject?", [object = mPopupCurrent_GameObject](bool yes) { + object->RemoveSelfFromParent(); + delete object; + }); + } + ImGui::EndPopup(); + } + ImGui::End(); + } + + ShowSpriteViewer(); + + ImGui::ShowDialogs(); + ImGui::ShowNotifications(); +} + +void EditorInstance::ShowWorldProperties() { + using namespace ProjectBrussel_UNITY_ID; + + 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."); + } + } + + auto& renderer = *mApp->GetWorldRenderer(); + auto& camera = *mApp->GetActiveCamera(); + + if (ImGui::CollapsingHeader("Renderer settings")) { + if (ImGui::Checkbox("Draw shaded", &mRenderer_DrawShaded)) { + renderer.SetRenderOption(Renderer::RO_Shading, mRenderer_DrawShaded); + } + if (ImGui::Checkbox("Draw wireframe", &mRenderer_DrawWireFrame)) { + renderer.SetRenderOption(Renderer::RO_Wireframe, mRenderer_DrawWireFrame); + } + + if (auto ires = Utils::SimpleIresReceptor<IresMaterial>(renderer.binding_WireframeMaterial->GetIres(), *this, IresObject::KD_Material)) { + renderer.binding_WireframeMaterial.Attach(ires->GetInstance()); + SaveRendererBindings(renderer); + } + } + + if (ImGui::CollapsingHeader("Camera settings")) { + // vvv Camera settings (per instance) + ImGui::TextUnformatted("Active camera:"); + ImGui::Indent(); + + ImGui::TextUnformatted(camera.name.c_str()); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Object at <%p>", &camera); + } + + ImGui::InputFloat3("Eye", glm::value_ptr(camera.eye)); + if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) { + ImGui::OpenPopup("##CTXMENU"); + } + ImGui::InputFloat3("Target", glm::value_ptr(camera.target)); + if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) { + ImGui::OpenPopup("##CTXMENU"); + } + if (ImGui::BeginPopup("##CTXMENU", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings)) { + if (ImGui::MenuItem("Reset to origin")) { + camera.SetEyePos(glm::vec3(1.0f, 1.0f, 1.0f)); + } + if (ImGui::MenuItem("Reset completely")) { + camera.eye = glm::vec3(0, 0, 1); + camera.target = glm::vec3(0, 0, 0); + } + ImGui::EndPopup(); + } + + if (IsCurrentCameraEditor()) { + const char* preview; + switch (mEcm) { + case ECM_2D: preview = "2D view"; break; + case ECM_Side3D: preview = "Side 3D view"; break; + } + if (ImGui::BeginCombo("Mode", preview)) { + if (ImGui::Selectable("2D view", mEcm == ECM_2D)) { + if (mEcm != ECM_2D) { + mEcm = ECM_2D; + + // TODO project eye to world plane + + camera.SetHasPerspective(false); + } + } + if (ImGui::Selectable("Side 3D view", mEcm == ECM_Side3D, ImGuiSelectableFlags_None)) { + if (mEcm != ECM_Side3D) { + mEcm = ECM_Side3D; + + auto origEye = camera.eye; + auto origTarget = camera.target; + + // New setup: focus on the point of world plane that we were originally "hovering above" + camera.target = origEye; + camera.target.z = 0.0f; + + // New setup: move the eye back at an angle + camera.eye = camera.target; + camera.eye.x += 4.0f * std::cos(60.0f); + camera.eye.y += 0.0f; + camera.eye.z += 4.0f * std::sin(60.0f); + + camera.SetHasPerspective(true); + } + } + ImGui::EndCombo(); + } + } + + ImGui::Checkbox("Perspective", &camera.perspective); + if (camera.perspective) { + float fovDegress = camera.fov / M_PI * 180.0f; + if (ImGui::SliderFloat("FOV", &fovDegress, 1.0f, 180.0f)) { + camera.fov = fovDegress / 180.0f * M_PI; + } + } else { + if (ImGui::InputFloat("Pixels per meter", &camera.pixelsPerMeter)) { + camera.pixelsPerMeter = std::max(camera.pixelsPerMeter, 0.0f); + } + } + + ImGui::Unindent(); + // ^^^ Camera settings (per instance) + // vvv Camera control settings + ImGui::TextUnformatted("Camera controls:"); + ImGui::Indent(); + + ImGui::Checkbox("Move camera with WASD", &mMoveCamKeyboard); + ImGui::SliderFloat("Slide speed", &mMoveCamSlideSpeed, 0.1f, 10.0f); + + ImGui::Checkbox("Move camera with scoll wheel", &mMoveCamScrollWheel); + ImGui::SliderFloat("Scroll speed", &mMoveCamScrollSpeed, 0.01, 10.0f); + + ImGui::Unindent(); + // ^^^ Camera control settings + } +} + +// TOOD move resource-specific and gameobject-specific inspector code into attachments mechanism + +void EditorInstance::ShowInspector(IresObject* ires) { + ires->ShowEditor(*this); +} + +void EditorInstance::ShowInspector(LevelManager::LoadableObject* ldObj) { + using namespace Tags; + using namespace ProjectBrussel_UNITY_ID; + + ImGui::InputText("Name", &ldObj->name); + ImGui::InputTextMultiline("Desciption", &ldObj->description); + + if (ImGui::CollapsingHeader("Instanciation Entries")) { + ldObj->level->ShowInstanciationEntries(*this); + } +} + +void EditorInstance::ShowInspector(GameObject* object) { + using namespace Tags; + using namespace ProjectBrussel_UNITY_ID; + + auto& io = ImGui::GetIO(); + + auto objectEa = static_cast<EaGameObject*>(object->GetEditorAttachment()); + + auto ShowInspector = [&](/*array[6]*/ float* bounds = nullptr) { + glm::mat4 identityMatrix(1.00f); + + if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_T)) + mGuizmo.currOperation = ImGuizmo::TRANSLATE; + if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_R)) + mGuizmo.currOperation = ImGuizmo::ROTATE; + if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_S)) + mGuizmo.currOperation = ImGuizmo::SCALE; + if (ImGui::RadioButton("Translate", mGuizmo.currOperation == ImGuizmo::TRANSLATE)) + mGuizmo.currOperation = ImGuizmo::TRANSLATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", mGuizmo.currOperation == ImGuizmo::ROTATE)) + mGuizmo.currOperation = ImGuizmo::ROTATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Scale", mGuizmo.currOperation == ImGuizmo::SCALE)) + mGuizmo.currOperation = ImGuizmo::SCALE; + ImGui::SameLine(); + if (ImGui::RadioButton("Universal", mGuizmo.currOperation == ImGuizmo::UNIVERSAL)) + mGuizmo.currOperation = ImGuizmo::UNIVERSAL; + + if (mGuizmo.currOperation != ImGuizmo::SCALE) { + if (ImGui::RadioButton("Local", mGuizmo.currMode == ImGuizmo::LOCAL)) + mGuizmo.currMode = ImGuizmo::LOCAL; + ImGui::SameLine(); + if (ImGui::RadioButton("World", mGuizmo.currMode == ImGuizmo::WORLD)) + mGuizmo.currMode = ImGuizmo::WORLD; + } + + ImGui::Checkbox("##UseSnap", &mGuizmo.useSnap); + ImGui::SameLine(); + switch (mGuizmo.currOperation) { + case ImGuizmo::TRANSLATE: + ImGui::InputFloat3("Pos Snap", &mGuizmo.snap[0]); + break; + case ImGuizmo::ROTATE: + ImGui::InputFloat("Angle Snap", &mGuizmo.snap[0]); + break; + case ImGuizmo::SCALE: + ImGui::InputFloat("Scale Snap", &mGuizmo.snap[0]); + break; + default: break; + } + + if (bounds) { + ImGui::Checkbox("Bound Sizing", &mGuizmo.boundSizing); + if (mGuizmo.boundSizing) { + ImGui::PushID(3); + ImGui::Checkbox("##BoundSizing", &mGuizmo.boundSizingSnap); + ImGui::SameLine(); + ImGui::InputFloat3("Bound Snap", mGuizmo.boundsSnap); + ImGui::PopID(); + } + } + + ImGui::Separator(); + + auto objectPos = object->GetPos(); + if (ImGui::InputFloat3("Translation", &objectPos.x)) { + object->SetPos(objectPos); + } + + auto objectRot = object->GetRotation(); + if (ImGui::InputFloat3("Rotation", &objectEa->eulerAnglesRotation.x)) { + objectRot = CalcQuaternionFromDegreesEulerAngle(objectEa->eulerAnglesRotation); + object->SetRotation(objectRot); + } + + auto objectScale = object->GetScale(); + if (ImGui::InputFloat3("Scale", &objectScale.x)) { + object->SetScale(objectScale); + } + }; + + auto ShowGuizmo = [&](/*array[6]*/ float* bounds = nullptr) { + glm::mat4 identityMatrix(1.00f); + + auto& lastFrameInfo = mApp->GetWorldRenderer()->GetLastFrameInfo(); + auto& cameraViewMat = lastFrameInfo.matrixView; + const float* cameraView = &cameraViewMat[0][0]; + auto& cameraProjMat = lastFrameInfo.matrixProj; + const float* cameraProj = &cameraProjMat[0][0]; + + auto objectPos = object->GetPos(); + auto objectScale = object->GetScale(); + + float matrix[16]; + ImGuizmo::RecomposeMatrixFromComponents(&objectPos.x, &objectEa->eulerAnglesRotation.x, &objectScale.x, matrix); + + bool edited = ImGuizmo::Manipulate( + cameraView, + cameraProj, + mGuizmo.currOperation, + mGuizmo.currMode, + matrix, + nullptr, + mGuizmo.useSnap ? mGuizmo.snap : nullptr, + mGuizmo.boundSizing ? bounds : nullptr, + (bounds && mGuizmo.boundSizingSnap) ? mGuizmo.boundsSnap : nullptr); + + if (edited) { + ImGuizmo::DecomposeMatrixToComponents(matrix, &objectPos.x, &objectEa->eulerAnglesRotation.x, &objectScale.x); + object->SetPos(objectPos); + object->SetRotation(CalcQuaternionFromDegreesEulerAngle(objectEa->eulerAnglesRotation)); + object->SetScale(objectScale); + } + }; + + auto type = object->GetKind(); + switch (type) { + case GameObject::KD_Player: { + ShowInspector(); + ShowGuizmo(); + ImGui::Separator(); + + auto player = static_cast<Player*>(object); + auto ea = static_cast<EaPlayer*>(objectEa); + 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(Metadata::EnumToString(IresObject::KD_Spritesheet).data())) { + auto spritesheet = *static_cast<IresSpritesheet* const*>(payload->Data); + ea->confSprite.Attach(spritesheet); + auto def = spritesheet->GetInstance(); + player->sprite.SetDefinition(def); + player->renderObject.autofill_TextureAtlas.Attach(def->GetAtlas()); + } + ImGui::EndDragDropTarget(); + } + + ImGui::TextUnformatted("Material: "); + ImGui::SameLine(); + IresObject::ShowReferenceSafe(*this, ea->confMaterial.Get()); + if (ImGui::BeginDragDropTarget()) { + if (auto payload = ImGui::AcceptDragDropPayload(Metadata::EnumToString(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) { + ImGui::AddNotification(ImGuiToast(ImGuiToastType_Success, "Successfully loaded player config")); + } + } + ImGui::SameLine(); + if (ImGui::Button("Save config")) { + bool success = player->SaveToFile(); + if (success) { + ImGui::AddNotification(ImGuiToast(ImGuiToastType_Success, "Successfully saved player config")); + } + } + + ImGui::Text("Move left (%s)", ImGui::GetKeyNameGlfw(kb.keyLeft)); + ImGui::SameLine(); + if (ImGui::Button("Change##Move left")) { + PushKeyCodeRecorder(mApp, &kb.keyLeft, &kb.pressedLeft); + } + + ImGui::Text("Move right (%s)", ImGui::GetKeyNameGlfw(kb.keyRight)); + ImGui::SameLine(); + if (ImGui::Button("Change##Move right")) { + PushKeyCodeRecorder(mApp, &kb.keyRight, &kb.pressedRight); + } + + ImGui::Text("Jump (%s)", ImGui::GetKeyNameGlfw(kb.keyJump)); + ImGui::SameLine(); + if (ImGui::Button("Change##Jump")) { + PushKeyCodeRecorder(mApp, &kb.keyJump, &kb.pressedJump); + } + + ImGui::Text("Attack (%s)", ImGui::GetKeyNameGlfw(kb.keyAttack)); + ImGui::SameLine(); + if (ImGui::Button("Change##Attack")) { + PushKeyCodeRecorder(mApp, &kb.keyAttack, &kb.pressedAttack); + } + } break; + + case GameObject::KD_SimpleGeometry: { + auto sg = static_cast<SimpleGeometryObject*>(object); + + float bounds[6] = { + /*x1*/ -sg->GetSize().x / 2.0f, + /*y1*/ -sg->GetSize().y / 2.0f, + /*z1*/ -sg->GetSize().z / 2.0f, + /*x2*/ +sg->GetSize().x / 2.0f, + /*y2*/ +sg->GetSize().y / 2.0f, + /*z2*/ +sg->GetSize().z / 2.0f, + }; + + ShowInspector(bounds); + ShowGuizmo(bounds); + ImGui::Separator(); + + sg->SetSize(glm::vec3(bounds[3] - bounds[0], bounds[4] - bounds[1], bounds[5] - bounds[2])); + + auto size = sg->GetSize(); + if (ImGui::InputFloat3("Size", &size.x)) { + sg->SetSize(size); + } + + auto xFaceColor = sg->GetXFaceColor(); + if (ImGui::ColorEdit4("X color", &xFaceColor)) { + sg->SetXFaceColor(xFaceColor); + } + auto yFaceColor = sg->GetYFaceColor(); + if (ImGui::ColorEdit4("Y color", &yFaceColor)) { + sg->SetYFaceColor(yFaceColor); + } + auto zFaceColor = sg->GetZFaceColor(); + if (ImGui::ColorEdit4("Z color", &zFaceColor)) { + sg->SetZFaceColor(zFaceColor); + } + + if (ImGui::Button("Sync from X color")) { + sg->SetYFaceColor(xFaceColor); + sg->SetZFaceColor(xFaceColor); + } + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + sg->SetXFaceColor(kXAxisColor); + sg->SetYFaceColor(kYAxisColor); + sg->SetZFaceColor(kZAxisColor); + } + } break; + + case GameObject::KD_Building: { + ShowInspector(); + ShowGuizmo(); + ImGui::Separator(); + + auto b = static_cast<BuildingObject*>(object); + // TODO + } break; + + case GameObject::KD_LevelWrapper: { + ShowInspector(); + ShowGuizmo(); + ImGui::Separator(); + + auto lwo = static_cast<LevelWrapperObject*>(object); + // TODO + } break; + + default: { + ShowInspector(); + ShowGuizmo(); + } break; + } +} + +void EditorInstance::OpenSpriteViewer(SpriteDefinition* sprite) { + mSpriteView_Instance.Attach(sprite); + mSpriteView_Frame = 0; + mSpriteView_OpenNextFrame = true; +} + +void EditorInstance::ShowSpriteViewer() { + if (mSpriteView_Instance == nullptr) return; + if (!mSpriteView_Instance->IsValid()) return; + + if (mSpriteView_OpenNextFrame) { + mSpriteView_OpenNextFrame = false; + ImGui::OpenPopup("Sprite Viewer"); + } + + bool windowOpen = true; + if (ImGui::BeginPopupModal("Sprite Viewer", &windowOpen)) { + auto atlas = mSpriteView_Instance->GetAtlas(); + auto atlasSize = atlas->GetInfo().size; + auto& frames = mSpriteView_Instance->GetFrames(); + + int frameCount = mSpriteView_Instance->GetFrames().size(); + if (ImGui::Button("<")) { + --mSpriteView_Frame; + } + ImGui::SameLine(); + ImGui::Text("%d/%d", mSpriteView_Frame + 1, frameCount); + ImGui::SameLine(); + if (ImGui::Button(">")) { + ++mSpriteView_Frame; + } + // Scrolling down (negative value) should advance, so invert the sign + mSpriteView_Frame += -std::round(ImGui::GetIO().MouseWheel); + // Clamp mSpriteView_Frame to range [0, frameCount) + if (mSpriteView_Frame < 0) { + mSpriteView_Frame = frameCount - 1; + } else if (mSpriteView_Frame >= frameCount) { + mSpriteView_Frame = 0; + } + + auto& currFrame = frames[mSpriteView_Frame]; + auto boundingBox = mSpriteView_Instance->GetBoundingBox(); + ImGui::Text("Frame size: (%d, %d)", boundingBox.x, boundingBox.y); + ImGui::Text("Frame location: (%f, %f) to (%f, %f)", currFrame.u0, currFrame.v0, currFrame.u1, currFrame.v1); + ImGui::Image( + (ImTextureID)(uintptr_t)atlas->GetHandle(), + Utils::FitImage(atlasSize), + ImVec2(currFrame.u0, currFrame.v0), + ImVec2(currFrame.u1, currFrame.v1)); + + ImGui::EndPopup(); + } +} diff --git a/src/brussel.engine/EditorCorePrivate.hpp b/src/brussel.engine/EditorCorePrivate.hpp new file mode 100644 index 0000000..4071e7a --- /dev/null +++ b/src/brussel.engine/EditorCorePrivate.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include "App.hpp" +#include "EditorAccessories.hpp" +#include "EditorAttachment.hpp" +#include "EditorCommandPalette.hpp" +#include "EditorCore.hpp" +#include "EditorUtils.hpp" +#include "EditorWorldGuides.hpp" +#include "GameObject.hpp" +#include "Ires.hpp" +#include "Level.hpp" +#include "RcPtr.hpp" +#include "Sprite.hpp" +#include "World.hpp" + +#include <memory> +#include <string> + +// TODO move inspector drawing to this class +class EditorInspector final : public IEditorInspector { +public: + std::string renamingScratchBuffer; + void* selectedItPtr = nullptr; + TargetType selectedItt = ITT_None; + bool renaming = false; + + void SelectTarget(TargetType type, void* object) override; +}; + +class EditorContentBrowser final : public IEditorContentBrowser { +private: + enum Pane { + P_Settings, + P_Ires, + P_Level, + }; + + static constexpr float kSplitterThickness = 3.0f; + static constexpr float kPadding = 4.0f; + + // <root> + static constexpr float kLeftPaneMinWidth = 200.0f; + static constexpr float kRightPaneMinWidth = 200.0f; + + EditorInspector* mInspector; + Pane mPane = P_Settings; + float mBrowserHeight = 0.5f; + float mSplitterLeft = kLeftPaneMinWidth; + float mSplitterRight = 0.0f; + bool mDocked = true; + +public: + EditorContentBrowser(EditorInspector* inspector); + ~EditorContentBrowser() override; + + void Show(bool* open = nullptr); +}; + +struct GuizmoState { + ImGuizmo::OPERATION currOperation = ImGuizmo::TRANSLATE; + ImGuizmo::MODE currMode = ImGuizmo::LOCAL; + glm::mat4 cubeMatrix; + float snap[3] = { 1.f, 1.f, 1.f }; + float boundsSnap[3] = { 0.1f, 0.1f, 0.1f }; + bool useSnap = false; + bool boundSizing = false; + bool boundSizingSnap = false; +}; + +// TODO editor undo stack +class EditorInstance : public IEditor { +public: + enum EditorCameraMode { + // "Game mode": the camera views from the side, where the forward vector is perpendicular to the world plane + ECM_2D, + // "Editor mode": equivalent to Unity's 3D mode, the camera is completely free and controlled as a 3d camera + ECM_Side3D, + }; + +private: + App* mApp; + GameObject* mPopupCurrent_GameObject = nullptr; + Camera mEditorCamera; + RcPtr<SpriteDefinition> mSpriteView_Instance; + EditorCommandPalette mEdCommandPalette; + EditorInspector mEdInspector; + EditorContentBrowser mEdContentBrowser; + EditorKeyboardViewer mEdKbViewer; + EditorWorldGuides mEdGuides; + GuizmoState mGuizmo; + glm::vec3 mDragCam_CamInitial; + ImVec2 mDragCam_CursorInitial; + int mSpriteView_Frame; + float mMoveCamScrollSpeed = 1.0f; + float mMoveCamSlideSpeed = 0.1f; + EditorCameraMode mEcm = ECM_2D; + bool mSpriteView_OpenNextFrame = 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; + bool mWindowVisible_KeyboardViewer = false; + bool mDragCam_Happening = false; + bool mMoveCamKeyboard = false; + bool mMoveCamScrollWheel = false; + bool mRenderer_DrawShaded = true; + bool mRenderer_DrawWireFrame = false; + +public: + EditorInstance(App* app); + ~EditorInstance() override; + + void OnGameStateChanged(bool running) override; + void Show() override; + + EditorInspector& GetInspector() override { return mEdInspector; } + EditorContentBrowser& GetContentBrowser() override { return mEdContentBrowser; } + + void OpenSpriteViewer(SpriteDefinition* sprite) override; + +private: + bool IsCurrentCameraEditor() const { + return !mApp->IsGameRunning(); + } + + void ShowWorldProperties(); + + void ShowInspector(IresObject* ires); + void ShowInspector(LevelManager::LoadableObject* ldObj); + void ShowInspector(GameObject* object); + + void ShowSpriteViewer(); +}; diff --git a/src/brussel.engine/EditorUtils.cpp b/src/brussel.engine/EditorUtils.cpp new file mode 100644 index 0000000..20caef7 --- /dev/null +++ b/src/brussel.engine/EditorUtils.cpp @@ -0,0 +1,447 @@ +#include "EditorUtils.hpp" + +#define IMGUI_DEFINE_MATH_OPERATORS +#include <imgui_internal.h> + +#include <backends/imgui_impl_glfw.h> +#include <cmath> +#include <glm/gtc/quaternion.hpp> +#include <glm/gtx/quaternion.hpp> +#include <numbers> + +const char* ImGui::GetKeyNameGlfw(int key) { + return GetKeyName(ImGui_ImplGlfw_KeyToImGuiKey(key)); +} + +void ImGui::SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond) { + auto vs = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowSize({ vs.x * xPercent, vs.y * yPercent }, cond); +} + +void ImGui::SetNextWindowCentered(ImGuiCond cond) { + auto vs = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowPos({ vs.x / 2, vs.y / 2 }, cond, { 0.5f, 0.5f }); +} + +void ImGui::PushDisabled() { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f * ImGui::GetStyle().Alpha); +} + +void ImGui::PopDisabled() { + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); +} + +bool ImGui::Button(const char* label, bool disabled) { + return Button(label, ImVec2{}, disabled); +} + +bool ImGui::Button(const char* label, const ImVec2& sizeArg, bool disabled) { + if (disabled) PushDisabled(); + bool res = ImGui::Button(label, sizeArg); + if (disabled) PopDisabled(); + + return res; +} + +#define EDIT_RGBA_COLOR(EditorFunction, kUsesAlpha) \ + float components[4]; \ + components[0] = color->GetNormalizedRed(); \ + components[1] = color->GetNormalizedGreen(); \ + components[2] = color->GetNormalizedBlue(); \ + if constexpr (kUsesAlpha) components[3] = color->GetNormalizedAlpha(); \ + if (EditorFunction(label, components, flags)) { \ + color->r = components[0] * 255; \ + color->g = components[1] * 255; \ + color->b = components[2] * 255; \ + if constexpr (kUsesAlpha) color->a = components[3] * 255; \ + return true; \ + } else { \ + return false; \ + } + +bool ImGui::ColorEdit3(const char* label, RgbaColor* color, ImGuiColorEditFlags flags) { + EDIT_RGBA_COLOR(ColorEdit3, false); +} + +bool ImGui::ColorEdit4(const char* label, RgbaColor* color, ImGuiColorEditFlags flags) { + EDIT_RGBA_COLOR(ColorEdit4, true); +} + +bool ImGui::ColorPicker3(const char* label, RgbaColor* color, ImGuiColorEditFlags flags) { + EDIT_RGBA_COLOR(ColorPicker3, false); +} + +bool ImGui::ColorPicker4(const char* label, RgbaColor* color, ImGuiColorEditFlags flags) { + EDIT_RGBA_COLOR(ColorPicker4, true); +} + +#undef EDIT_RGBA_COLOR + +struct InputTextCallbackUserData { + std::string* str; + ImGuiInputTextCallback chainCallback; + void* chainCallbackUserData; +}; + +static int InputTextCallback(ImGuiInputTextCallbackData* data) { + auto user_data = (InputTextCallbackUserData*)data->UserData; + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { + // Resize string callback + // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. + auto str = user_data->str; + IM_ASSERT(data->Buf == str->c_str()); + str->resize(data->BufTextLen); + data->Buf = (char*)str->c_str(); + } else if (user_data->chainCallback) { + // Forward to user callback, if any + data->UserData = user_data->chainCallbackUserData; + return user_data->chainCallback(data); + } + return 0; +} + +bool ImGui::Splitter(bool splitVertically, float thickness, float* size1, float* size2, float minSize1, float minSize2, float splitterLongAxisSize) { + // Adapted from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/blueprints-example.cpp + // ::Splitter + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID("##Splitter"); + ImRect bb; + bb.Min = window->DC.CursorPos + (splitVertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1)); + bb.Max = bb.Min + CalcItemSize(splitVertically ? ImVec2(thickness, splitterLongAxisSize) : ImVec2(splitterLongAxisSize, thickness), 0.0f, 0.0f); + + // Adapted from ImGui::SplitterBehavior, changes: + // - Simplified unneeded logic (hover_extend and hover_visibility_delay) + // - Changed clamped delta to clamping result size1 and deriving size2 from size1, allowing automatically adapting to the latest window content region width + + auto itemFlagsBackup = g.CurrentItemFlags; + g.CurrentItemFlags |= ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus; + bool itemAdd = ItemAdd(bb, id); + g.CurrentItemFlags = itemFlagsBackup; + if (!itemAdd) { + return false; + } + + bool hovered, held; + auto bbInteract = bb; + ButtonBehavior(bbInteract, id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap); + if (hovered) { + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; + } // for IsItemHovered(), because bbInteract is larger than bb + if (g.ActiveId != id) { + SetItemAllowOverlap(); + } + + if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= 0.0f)) { + SetMouseCursor((splitVertically ? ImGuiAxis_X : ImGuiAxis_Y) == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); + } + + float contentSize = splitVertically ? window->ContentRegionRect.GetWidth() : window->ContentRegionRect.GetHeight(); + if (held) { + ImVec2 mouseDelta2D = g.IO.MousePos - g.ActiveIdClickOffset - bbInteract.Min; + float mouseDelta = ((splitVertically ? ImGuiAxis_X : ImGuiAxis_Y) == ImGuiAxis_Y) ? mouseDelta2D.y : mouseDelta2D.x; + + // Apply resize + if (mouseDelta != 0.0f) { + *size1 = ImClamp(*size1 + mouseDelta, minSize1, contentSize - minSize2 - thickness); + *size2 = contentSize - *size1 - thickness; + MarkItemEdited(id); + } + } + + ImU32 col; + if (held) { + col = GetColorU32(ImGuiCol_SeparatorActive); + } else if (hovered && g.HoveredIdTimer >= 0.0f) { + col = GetColorU32(ImGuiCol_SeparatorHovered); + } else { + col = GetColorU32(ImGuiCol_Separator); + } + window->DrawList->AddRectFilled(bb.Min, bb.Max, col, 0.0f); + + return held; +} + +void ImGui::AddUnderLine(ImColor col) { + auto min = ImGui::GetItemRectMin(); + auto max = ImGui::GetItemRectMax(); + min.y = max.y; + ImGui::GetWindowDrawList()->AddLine(min, max, col, 1.0f); +} + +void ImGui::DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor) { + // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/utilities/drawing.cpp + // ax::NodeEditor::DrawIcon + + auto rect = ImRect(a, b); + auto rect_x = rect.Min.x; + auto rect_y = rect.Min.y; + auto rect_w = rect.Max.x - rect.Min.x; + auto rect_h = rect.Max.y - rect.Min.y; + auto rect_center_x = (rect.Min.x + rect.Max.x) * 0.5f; + auto rect_center_y = (rect.Min.y + rect.Max.y) * 0.5f; + auto rect_center = ImVec2(rect_center_x, rect_center_y); + const auto outline_scale = rect_w / 24.0f; + const auto extra_segments = static_cast<int>(2 * outline_scale); // for full circle + + if (type == IconType::Flow) { + const auto origin_scale = rect_w / 24.0f; + + const auto offset_x = 1.0f * origin_scale; + const auto offset_y = 0.0f * origin_scale; + const auto margin = 2.0f * origin_scale; + const auto rounding = 0.1f * origin_scale; + const auto tip_round = 0.7f; // percentage of triangle edge (for tip) + // const auto edge_round = 0.7f; // percentage of triangle edge (for corner) + const auto canvas = ImRect( + rect.Min.x + margin + offset_x, + rect.Min.y + margin + offset_y, + rect.Max.x - margin + offset_x, + rect.Max.y - margin + offset_y); + const auto canvas_x = canvas.Min.x; + const auto canvas_y = canvas.Min.y; + const auto canvas_w = canvas.Max.x - canvas.Min.x; + const auto canvas_h = canvas.Max.y - canvas.Min.y; + + const auto left = canvas_x + canvas_w * 0.5f * 0.3f; + const auto right = canvas_x + canvas_w - canvas_w * 0.5f * 0.3f; + const auto top = canvas_y + canvas_h * 0.5f * 0.2f; + const auto bottom = canvas_y + canvas_h - canvas_h * 0.5f * 0.2f; + const auto center_y = (top + bottom) * 0.5f; + // const auto angle = AX_PI * 0.5f * 0.5f * 0.5f; + + const auto tip_top = ImVec2(canvas_x + canvas_w * 0.5f, top); + const auto tip_right = ImVec2(right, center_y); + const auto tip_bottom = ImVec2(canvas_x + canvas_w * 0.5f, bottom); + + drawList->PathLineTo(ImVec2(left, top) + ImVec2(0, rounding)); + drawList->PathBezierCubicCurveTo( + ImVec2(left, top), + ImVec2(left, top), + ImVec2(left, top) + ImVec2(rounding, 0)); + drawList->PathLineTo(tip_top); + drawList->PathLineTo(tip_top + (tip_right - tip_top) * tip_round); + drawList->PathBezierCubicCurveTo( + tip_right, + tip_right, + tip_bottom + (tip_right - tip_bottom) * tip_round); + drawList->PathLineTo(tip_bottom); + drawList->PathLineTo(ImVec2(left, bottom) + ImVec2(rounding, 0)); + drawList->PathBezierCubicCurveTo( + ImVec2(left, bottom), + ImVec2(left, bottom), + ImVec2(left, bottom) - ImVec2(0, rounding)); + + if (!filled) { + if (innerColor & 0xFF000000) { + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + } + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } else { + drawList->PathFillConvex(color); + } + } else { + auto triangleStart = rect_center_x + 0.32f * rect_w; + + auto rect_offset = -static_cast<int>(rect_w * 0.25f * 0.25f); + + rect.Min.x += rect_offset; + rect.Max.x += rect_offset; + rect_x += rect_offset; + rect_center_x += rect_offset * 0.5f; + rect_center.x += rect_offset * 0.5f; + + if (type == IconType::Circle) { + const auto c = rect_center; + + if (!filled) { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + + if (innerColor & 0xFF000000) + drawList->AddCircleFilled(c, r, innerColor, 12 + extra_segments); + drawList->AddCircle(c, r, color, 12 + extra_segments, 2.0f * outline_scale); + } else { + drawList->AddCircleFilled(c, 0.5f * rect_w / 2.0f, color, 12 + extra_segments); + } + } + + if (type == IconType::Square) { + if (filled) { + const auto r = 0.5f * rect_w / 2.0f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + drawList->AddRectFilled(p0, p1, color, 0, 15 + extra_segments); + } else { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + if (innerColor & 0xFF000000) + drawList->AddRectFilled(p0, p1, innerColor, 0, 15 + extra_segments); + + drawList->AddRect(p0, p1, color, 0, 15 + extra_segments, 2.0f * outline_scale); + } + } + + if (type == IconType::Grid) { + const auto r = 0.5f * rect_w / 2.0f; + const auto w = ceilf(r / 3.0f); + + const auto baseTl = ImVec2(floorf(rect_center_x - w * 2.5f), floorf(rect_center_y - w * 2.5f)); + const auto baseBr = ImVec2(floorf(baseTl.x + w), floorf(baseTl.y + w)); + + auto tl = baseTl; + auto br = baseBr; + for (int i = 0; i < 3; ++i) { + tl.x = baseTl.x; + br.x = baseBr.x; + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + if (i != 1 || filled) + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + drawList->AddRectFilled(tl, br, color); + + tl.y += w * 2; + br.y += w * 2; + } + + triangleStart = br.x + w + 1.0f / 24.0f * rect_w; + } + + if (type == IconType::RoundSquare) { + if (filled) { + const auto r = 0.5f * rect_w / 2.0f; + const auto cr = r * 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + drawList->AddRectFilled(p0, p1, color, cr, 15); + } else { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + const auto cr = r * 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + if (innerColor & 0xFF000000) + drawList->AddRectFilled(p0, p1, innerColor, cr, 15); + + drawList->AddRect(p0, p1, color, cr, 15, 2.0f * outline_scale); + } + } else if (type == IconType::Diamond) { + if (filled) { + const auto r = 0.607f * rect_w / 2.0f; + const auto c = rect_center; + + drawList->PathLineTo(c + ImVec2(0, -r)); + drawList->PathLineTo(c + ImVec2(r, 0)); + drawList->PathLineTo(c + ImVec2(0, r)); + drawList->PathLineTo(c + ImVec2(-r, 0)); + drawList->PathFillConvex(color); + } else { + const auto r = 0.607f * rect_w / 2.0f - 0.5f; + const auto c = rect_center; + + drawList->PathLineTo(c + ImVec2(0, -r)); + drawList->PathLineTo(c + ImVec2(r, 0)); + drawList->PathLineTo(c + ImVec2(0, r)); + drawList->PathLineTo(c + ImVec2(-r, 0)); + + if (innerColor & 0xFF000000) + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } + } else { + const auto triangleTip = triangleStart + rect_w * (0.45f - 0.32f); + + drawList->AddTriangleFilled( + ImVec2(ceilf(triangleTip), rect_y + rect_h * 0.5f), + ImVec2(triangleStart, rect_center_y + 0.15f * rect_h), + ImVec2(triangleStart, rect_center_y - 0.15f * rect_h), + color); + } + } +} + +void ImGui::Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color, const ImVec4& innerColor) { + // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/utilities/widgets.cpp + // ax::NodeEditor::Icon + + if (ImGui::IsRectVisible(size)) { + auto cursorPos = ImGui::GetCursorScreenPos(); + auto drawList = ImGui::GetWindowDrawList(); + DrawIcon(drawList, cursorPos, cursorPos + size, type, filled, ImColor(color), ImColor(innerColor)); + } + + ImGui::Dummy(size); +} + +void ImGui::DrawArrow(ImDrawList* drawList, ImVec2 from, ImVec2 to, ImU32 color, float lineThickness) { + // Adapted from https://stackoverflow.com/a/6333775 + + using namespace std::numbers; + constexpr float kHeadLength = 10; + + auto angle = std::atan2(to.y - from.y, to.x - from.x); + drawList->AddLine(from, to, color, lineThickness); + drawList->AddLine(to, ImVec2(to.x - kHeadLength * std::cos(angle - pi / 6), to.y - kHeadLength * std::sin(angle - pi / 6)), color, lineThickness); + drawList->AddLine(to, ImVec2(to.x - kHeadLength * std::cos(angle + pi / 6), to.y - kHeadLength * std::sin(angle + pi / 6)), color, lineThickness); +} + +struct DialogObject { + std::string message; + std::function<void(int)> callback; +}; + +static DialogObject gConfirmationDialog{}; + +void ImGui::DialogConfirmation(std::string message, std::function<void(bool)> callback) { + gConfirmationDialog.message = std::move(message); + // TODO is converting void(bool) to void(int) fine? + gConfirmationDialog.callback = std::move(callback); +} + +void ImGui::ShowDialogs() { + if (gConfirmationDialog.callback) { + if (ImGui::BeginPopupModal("Confirmation")) { + ImGui::Text("%s", gConfirmationDialog.message.c_str()); + if (ImGui::Button("Cancel")) { + gConfirmationDialog.callback(false); + gConfirmationDialog.callback = {}; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Confirm")) { + gConfirmationDialog.callback(true); + gConfirmationDialog.callback = {}; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } +} + +float Utils::CalcImageHeight(glm::vec2 original, int targetWidth) { + // Xorig / Yorig = Xnew / Ynew + // Ynew = Xnew * Yorig / Xorig + return targetWidth * original.y / original.x; +} + +float Utils::CalcImageWidth(glm::vec2 original, float targetHeight) { + // Xorig / Yorig = Xnew / Ynew + // Xnew = Xorig / Yorig * Ynew + return original.x / original.y * targetHeight; +} + +ImVec2 Utils::FitImage(glm::vec2 original) { + float newWidth = ImGui::GetContentRegionAvail().x; + return ImVec2(newWidth, CalcImageHeight(original, newWidth)); +} diff --git a/src/brussel.engine/EditorUtils.hpp b/src/brussel.engine/EditorUtils.hpp new file mode 100644 index 0000000..96e92d3 --- /dev/null +++ b/src/brussel.engine/EditorUtils.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include "EditorCore.hpp" +#include "ImGuiGuizmo.hpp" +#include "Ires.hpp" + +#include <Color.hpp> +#include <Metadata.hpp> + +#include <imgui.h> +#include <string> + +// To check whether a payload is of this type, use starts_with() +#define BRUSSEL_TAG_PREFIX_GameObject "GameObject" +#define BRUSSEL_TAG_PREFIX_Ires "Ires" + +#define BRUSSEL_TAG_Level "Level" + +namespace ImGui { + +const char* GetKeyNameGlfw(int key); + +void SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond = ImGuiCond_None); +void SetNextWindowCentered(ImGuiCond cond = ImGuiCond_None); + +void PushDisabled(); +void PopDisabled(); + +bool Button(const char* label, bool disabled); +bool Button(const char* label, const ImVec2& sizeArg, bool disabled); + +bool ColorEdit3(const char* label, RgbaColor* color, ImGuiColorEditFlags flags = 0); +bool ColorEdit4(const char* label, RgbaColor* color, ImGuiColorEditFlags flags = 0); +bool ColorPicker3(const char* label, RgbaColor* color, ImGuiColorEditFlags flags = 0); +bool ColorPicker4(const char* label, RgbaColor* color, ImGuiColorEditFlags flags = 0); + +bool Splitter(bool splitVertically, float thickness, float* size1, float* size2, float minSize1, float minSize2, float splitterLongAxisSize = -1.0f); + +void AddUnderLine(ImColor col); + +enum class IconType { + Flow, + Circle, + Square, + Grid, + RoundSquare, + Diamond, +}; + +void DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor); +void Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color = ImVec4(1, 1, 1, 1), const ImVec4& innerColor = ImVec4(0, 0, 0, 0)); + +void DrawArrow(ImDrawList* drawList, ImVec2 from, ImVec2 to, ImU32 color, float lineThickness = 1.0f); + +// NOTE: string is copied into an internal storage +void DialogConfirmation(std::string message, std::function<void(bool)> callback); +void ShowDialogs(); + +} // namespace ImGui + +namespace Utils { + +float CalcImageHeight(glm::vec2 original, int targetWidth); +float CalcImageWidth(glm::vec2 original, float targetHeight); +ImVec2 FitImage(glm::vec2 original); + +// TODO get kind from T +template <typename T> +T* SimpleIresReceptor(T* existing, IEditor& editor, IresObject::Kind kind) { + if (existing) { + existing->ShowReference(editor); + } else { + IresObject::ShowReferenceNull(editor); + } + if (ImGui::BeginDragDropTarget()) { + if (auto payload = ImGui::AcceptDragDropPayload(Metadata::EnumToString(kind).data())) { + return *static_cast<T* const*>(payload->Data); + } + ImGui::EndDragDropTarget(); + } + return nullptr; +} + +} // namespace Utils diff --git a/src/brussel.engine/EditorWorldGuides.cpp b/src/brussel.engine/EditorWorldGuides.cpp new file mode 100644 index 0000000..f0d66b8 --- /dev/null +++ b/src/brussel.engine/EditorWorldGuides.cpp @@ -0,0 +1,26 @@ +#include "EditorWorldGuides.hpp" + +#include "App.hpp" +#include "CommonVertexIndex.hpp" +#include "GraphicsTags.hpp" +#include "VertexIndex.hpp" + +EditorWorldGuides::EditorWorldGuides(App* app, IEditor* editor) + : mApp{ app } + , mEditor{ editor } // +{ + using namespace Tags; + + mRoGrid.SetFormat(gVformatLines.Get(), IT_16Bit); + mRoGrid.SetMaterial(gDefaultMaterial.Get()); + mRoGrid.RebuildIfNecessary(); + + mRoDebugSkybox.SetFormat(gVformatStandard.Get(), IT_16Bit); + mRoDebugSkybox.SetMaterial(gDefaultMaterial.Get()); + mRoDebugSkybox.RebuildIfNecessary(); +} + +void EditorWorldGuides::Update() { + auto& camera = *mApp->GetActiveCamera(); + // TODO +} diff --git a/src/brussel.engine/EditorWorldGuides.hpp b/src/brussel.engine/EditorWorldGuides.hpp new file mode 100644 index 0000000..0dfdea2 --- /dev/null +++ b/src/brussel.engine/EditorWorldGuides.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "Renderer.hpp" + +class EditorWorldGuides { +private: + App* mApp; + IEditor* mEditor; + RenderObject mRoGrid; + RenderObject mRoDebugSkybox; + +public: + EditorWorldGuides(App* app, IEditor* editor); + + void Update(); +}; diff --git a/src/brussel.engine/FuzzyMatch.cpp b/src/brussel.engine/FuzzyMatch.cpp new file mode 100644 index 0000000..0ab604d --- /dev/null +++ b/src/brussel.engine/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/src/brussel.engine/FuzzyMatch.hpp b/src/brussel.engine/FuzzyMatch.hpp new file mode 100644 index 0000000..7a26b7e --- /dev/null +++ b/src/brussel.engine/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/src/brussel.engine/GameObject.cpp b/src/brussel.engine/GameObject.cpp new file mode 100644 index 0000000..3b15111 --- /dev/null +++ b/src/brussel.engine/GameObject.cpp @@ -0,0 +1,230 @@ +#include "GameObject.hpp" + +#include "Level.hpp" +#include "Player.hpp" +#include "SceneThings.hpp" +#include "World.hpp" + +#include <Metadata.hpp> +#include <RapidJsonHelper.hpp> + +#include <rapidjson/document.h> +#include <cassert> +#include <string_view> +#include <utility> + +using namespace std::literals; + +namespace ProjectBrussel_UNITY_ID { +GameObject* CreateGameObject(GameObject::Kind kind, GameWorld* world) { + using enum Tags::GameObjectKind; + switch (kind) { + case KD_Generic: return new GameObject(world); + case KD_SimpleGeometry: return new SimpleGeometryObject(world); + case KD_Building: return new BuildingObject(world); + case KD_LevelWrapper: return new LevelWrapperObject(world); + default: break; + } + return nullptr; +} + +bool ValidateGameObjectChild(GameObject* parent, GameObject* child) { + return parent->GetWorld() == child->GetWorld(); +} +} // namespace ProjectBrussel_UNITY_ID + +void GameObject::FreeRecursive(GameObject* obj) { + if (!obj->mStopFreePropagation) { + for (auto child : obj->GetChildren()) { + FreeRecursive(obj); + } + } + delete obj; +} + +GameObject::GameObject(GameWorld* world) + : GameObject(KD_Generic, world) { +} + +GameObject::GameObject(Kind kind, GameWorld* world) + : mEditorAttachment{ nullptr } + , mWorld{ world } + , mParent{ nullptr } + , mRot(1.0f, 0.0f, 0.0f, 0.0f) + , mPos(0.0f, 0.0f, 0.0f) + , mScale(1.0f, 1.0f, 1.0f) + , mKind{ kind } { +} + +GameObject::~GameObject() { + RemoveAllChildren(); + if (mParent) { + mParent->RemoveChild(this); + // NOTE: from this point on, mParent will be nullptr + } +} + +GameObject::Kind GameObject::GetKind() const { + return mKind; +} + +GameWorld* GameObject::GetWorld() const { + return mWorld; +} + +GameObject* GameObject::GetParent() const { + return mParent; +} + +const PodVector<GameObject*>& GameObject::GetChildren() const { + return mChildren; +} + +void GameObject::AddChild(GameObject* child) { + using namespace ProjectBrussel_UNITY_ID; + + if (child->mParent) { + return; + } + if (!ValidateGameObjectChild(this, child)) { + return; + } + + mChildren.push_back(child); + child->SetParent(this); +} + +GameObject* GameObject::RemoveChild(int index) { + if (index < 0 || index >= mChildren.size()) { + return nullptr; + } + + auto it = mChildren.begin() + index; + auto child = *it; + + // cancelUpdate(ret); + + std::swap(*it, mChildren.back()); + mChildren.pop_back(); + child->SetParent(nullptr); + return child; +} + +GameObject* GameObject::RemoveChild(GameObject* child) { + if (child) { + for (auto it = mChildren.begin(); it != mChildren.end(); ++it) { + if (*it == child) { + // cancelUpdate(child); + + std::swap(*it, mChildren.back()); + mChildren.pop_back(); + child->SetParent(nullptr); + return child; + } + } + } + return nullptr; +} + +void GameObject::RemoveSelfFromParent() { + if (mParent) { + mParent->RemoveChild(this); + } +} + +PodVector<GameObject*> GameObject::RemoveAllChildren() { + for (auto& child : mChildren) { + child->SetParent(nullptr); + } + + auto result = std::move(mChildren); + // Moving from STL object leaves it in a valid but _unspecified_ state, call std::vector::clear() to guarantee it's empty + // NOTE: even though we have the source code of PodVector<T>, we still do this to follow convention + mChildren.clear(); + return result; +} + +const glm::vec3& GameObject::GetPos() const { + return mPos; +} + +void GameObject::SetPos(const glm::vec3& pos) { + mPos = pos; +} + +const glm::quat& GameObject::GetRotation() const { + return mRot; +} + +void GameObject::SetRotation(const glm::quat& rotation) { + mRot = rotation; +} + +const glm::vec3& GameObject::GetScale() const { + return mScale; +} + +void GameObject::SetScale(const glm::vec3& scale) { + mScale = scale; +} + +std::span<const RenderObject> GameObject::GetRenderObjects() const { + return {}; +} + +void GameObject::OnInitialized() { +} + +void GameObject::Awaken() { +} + +void GameObject::Resleep() { +} + +void GameObject::Update() { +} + +rapidjson::Value GameObject::Serialize(GameObject* obj, rapidjson::Document& root) { + rapidjson::Value result(rapidjson::kObjectType); + + result.AddMember("Type", rapidjson::StringRef(Metadata::EnumToString(obj->GetKind())), root.GetAllocator()); + + rapidjson::Value rvValue(rapidjson::kObjectType); + obj->WriteSaveFormat(rvValue, root); + result.AddMember("Value", rvValue, root.GetAllocator()); + + return result; +} + +std::unique_ptr<GameObject> GameObject::Deserialize(const rapidjson::Value& value, GameWorld* world) { + using namespace ProjectBrussel_UNITY_ID; + + auto rvType = rapidjson::GetProperty(value, rapidjson::kStringType, "Type"sv); + if (!rvType) return nullptr; + + auto rvValue = rapidjson::GetProperty(value, rapidjson::kObjectType, "Value"sv); + if (!rvValue) return nullptr; + + auto kind = Metadata::EnumFromString<Kind>(rapidjson::AsStringView(*rvType)); + assert(kind.has_value()); + auto obj = std::unique_ptr<GameObject>(CreateGameObject(kind.value(), world)); + if (!obj) return nullptr; + obj->ReadSaveFormat(*rvValue); + + return obj; +} + +void GameObject::ReadSaveFormat(const rapidjson::Value& value) { +} + +void GameObject::WriteSaveFormat(rapidjson::Value& value, rapidjson::Document& root) { +} + +void GameObject::SetParent(GameObject* parent) { + if (mParent != parent) { + mParent = parent; + // needUpdate(); + } +} + +#include <generated/GameObject.gs.inl> diff --git a/src/brussel.engine/GameObject.hpp b/src/brussel.engine/GameObject.hpp new file mode 100644 index 0000000..40c52e7 --- /dev/null +++ b/src/brussel.engine/GameObject.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include "EditorAttachment.hpp" +#include "Material.hpp" +#include "Renderer.hpp" +#include "VertexIndex.hpp" + +#include <MacrosCodegen.hpp> +#include <PodVector.hpp> + +#include <rapidjson/fwd.h> +#include <glm/glm.hpp> +#include <glm/gtc/quaternion.hpp> +#include <span> +#include <vector> + +namespace Tags { +enum class GameObjectKind { + // clang-format off + BRUSSEL_ENUM(ToString, FromString, RemovePrefix KD_, AddPrefix GameObject, ExcludeHeuristics) + // clang-format on + + KD_Generic, + KD_Player, + KD_SimpleGeometry, + KD_Building, + KD_LevelWrapper, + KD_COUNT, +}; +} // namespace Tags + +class GameWorld; +class GameObject { + BRUSSEL_CLASS(InheritanceHiearchy) + +public: + using Kind = Tags::GameObjectKind; + using enum Tags::GameObjectKind; + +private: + std::unique_ptr<EditorAttachment> mEditorAttachment; + GameWorld* mWorld; + GameObject* mParent; + PodVector<GameObject*> mChildren; + glm::quat mRot; + glm::vec3 mPos; + glm::vec3 mScale; + Kind mKind; + +protected: + bool mStopFreePropagation : 1 = false; + +public: + static void FreeRecursive(GameObject* object); + + // TODO allow moving between worlds + GameObject(GameWorld* world); + GameObject(Kind kind, GameWorld* world); + virtual ~GameObject(); + + GameObject(const GameObject&) = delete; + GameObject& operator=(const GameObject&) = delete; + GameObject(GameObject&&) = default; + GameObject& operator=(GameObject&&) = default; + + Kind GetKind() const; + + GameWorld* GetWorld() const; + GameObject* GetParent() const; + const PodVector<GameObject*>& GetChildren() const; + void AddChild(GameObject* child); + GameObject* RemoveChild(int index); + GameObject* RemoveChild(GameObject* child); + void RemoveSelfFromParent(); + PodVector<GameObject*> RemoveAllChildren(); + + EditorAttachment* GetEditorAttachment() const { return mEditorAttachment.get(); } + void SetEditorAttachment(EditorAttachment* attachment) { mEditorAttachment.reset(attachment); } + + const glm::vec3& GetPos() const; + void SetPos(const glm::vec3& pos); + + const glm::quat& GetRotation() const; + void SetRotation(const glm::quat& rotation); + + const glm::vec3& GetScale() const; + void SetScale(const glm::vec3& scale); + + // Visuals + virtual std::span<const RenderObject> GetRenderObjects() const; + + // Lifetime hooks + virtual void OnInitialized(); + virtual void Awaken(); + virtual void Resleep(); + virtual void Update(); + + static rapidjson::Value Serialize(GameObject* obj, rapidjson::Document& root); + static std::unique_ptr<GameObject> Deserialize(const rapidjson::Value& value, GameWorld* world); + virtual void ReadSaveFormat(const rapidjson::Value& value); + virtual void WriteSaveFormat(rapidjson::Value& value, rapidjson::Document& root); + +protected: + void SetParent(GameObject* parent); +}; + +#include <generated/GameObject.gh.inl> diff --git a/src/brussel.engine/GraphicsTags.cpp b/src/brussel.engine/GraphicsTags.cpp new file mode 100644 index 0000000..83d52f8 --- /dev/null +++ b/src/brussel.engine/GraphicsTags.cpp @@ -0,0 +1,273 @@ +#include "GraphicsTags.hpp" + +#include <robin_hood.h> +#include <cstddef> +#include <cstdint> + +using namespace std::literals; + +int Tags::SizeOf(VertexElementType type) { + switch (type) { + case VET_Float1: + return sizeof(float); + case VET_Float2: + return sizeof(float) * 2; + case VET_Float3: + return sizeof(float) * 3; + case VET_Float4: + return sizeof(float) * 4; + case VET_Double1: + return sizeof(double); + case VET_Double2: + return sizeof(double) * 2; + case VET_Double3: + return sizeof(double) * 3; + case VET_Double4: + return sizeof(double) * 4; + case VET_Short2: + case VET_Short2Norm: + case VET_Ushort2: + case VET_Ushort2Norm: + return sizeof(short) * 2; + case VET_Short4: + case VET_Short4Norm: + case VET_Ushort4: + case VET_Ushort4Norm: + return sizeof(short) * 4; + case VET_Int1: + case VET_Uint1: + return sizeof(int); + case VET_Int2: + case VET_Uint2: + return sizeof(int) * 2; + case VET_Int3: + case VET_Uint3: + return sizeof(int) * 3; + case VET_Int4: + case VET_Uint4: + return sizeof(int) * 4; + case VET_Byte4: + case VET_Byte4Norm: + case VET_Ubyte4: + case VET_Ubyte4Norm: + return sizeof(char) * 4; + } + return 0; +} + +int Tags::VectorLenOf(VertexElementType type) { + switch (type) { + case VET_Float1: + case VET_Double1: + case VET_Int1: + case VET_Uint1: + return 1; + case VET_Float2: + case VET_Double2: + case VET_Short2: + case VET_Short2Norm: + case VET_Ushort2: + case VET_Ushort2Norm: + case VET_Int2: + case VET_Uint2: + return 2; + case VET_Float3: + case VET_Double3: + case VET_Int3: + case VET_Uint3: + return 3; + case VET_Float4: + case VET_Double4: + case VET_Short4: + case VET_Short4Norm: + case VET_Ushort4: + case VET_Ushort4Norm: + case VET_Int4: + case VET_Uint4: + case VET_Byte4: + case VET_Byte4Norm: + case VET_Ubyte4: + case VET_Ubyte4Norm: + return 4; + } + return 0; +} + +GLenum Tags::FindGLType(VertexElementType type) { + switch (type) { + case VET_Float1: + case VET_Float2: + case VET_Float3: + case VET_Float4: + return GL_FLOAT; + case VET_Double1: + case VET_Double2: + case VET_Double3: + case VET_Double4: + return GL_DOUBLE; + case VET_Short2: + case VET_Short2Norm: + case VET_Short4: + case VET_Short4Norm: + return GL_SHORT; + case VET_Ushort2: + case VET_Ushort2Norm: + case VET_Ushort4: + case VET_Ushort4Norm: + return GL_UNSIGNED_SHORT; + case VET_Int1: + case VET_Int2: + case VET_Int3: + case VET_Int4: + return GL_INT; + case VET_Uint1: + case VET_Uint2: + case VET_Uint3: + case VET_Uint4: + return GL_UNSIGNED_INT; + case VET_Byte4: + case VET_Byte4Norm: + return GL_BYTE; + case VET_Ubyte4: + case VET_Ubyte4Norm: + return GL_UNSIGNED_BYTE; + } + return 0; +} + +bool Tags::IsNormalized(VertexElementType type) { + return type >= VET_NORM_BEGIN && type <= VET_NORM_END; +} + +int Tags::SizeOf(IndexType type) { + switch (type) { + case IT_16Bit: return sizeof(uint16_t); + case IT_32Bit: return sizeof(uint32_t); + } + return 0; +} + +GLenum Tags::FindGLType(IndexType type) { + switch (type) { + case IT_16Bit: return GL_UNSIGNED_SHORT; + case IT_32Bit: return GL_UNSIGNED_BYTE; + } + return 0; +} + +namespace ProjectBrussel_UNITY_ID { +struct GLTypeInfo { + robin_hood::unordered_flat_map<GLenum, std::string_view> enum2Name; + robin_hood::unordered_flat_map<std::string_view, GLenum> name2Enum; + + GLTypeInfo() { + InsertEntry("float"sv, GL_FLOAT); + InsertEntry("double"sv, GL_DOUBLE); + InsertEntry("int"sv, GL_INT); + InsertEntry("uint"sv, GL_UNSIGNED_INT); + InsertEntry("bool"sv, GL_BOOL); + + InsertEntry("vec2"sv, GL_FLOAT_VEC2); + InsertEntry("vec3"sv, GL_FLOAT_VEC3); + InsertEntry("vec4"sv, GL_FLOAT_VEC4); + InsertEntry("dvec2"sv, GL_DOUBLE_VEC2); + InsertEntry("dvec3"sv, GL_DOUBLE_VEC3); + InsertEntry("dvec4"sv, GL_DOUBLE_VEC4); + InsertEntry("ivec2"sv, GL_INT_VEC2); + InsertEntry("ivec3"sv, GL_INT_VEC3); + InsertEntry("ivec4"sv, GL_INT_VEC4); + InsertEntry("uvec2"sv, GL_UNSIGNED_INT_VEC2); + InsertEntry("uvec3"sv, GL_UNSIGNED_INT_VEC3); + InsertEntry("uvec4"sv, GL_UNSIGNED_INT_VEC4); + InsertEntry("bvec2"sv, GL_BOOL_VEC2); + InsertEntry("bvec3"sv, GL_BOOL_VEC3); + InsertEntry("bvec4"sv, GL_BOOL_VEC4); + + InsertEntry("mat2"sv, GL_FLOAT_MAT2); + InsertEntry("mat3"sv, GL_FLOAT_MAT3); + InsertEntry("mat4"sv, GL_FLOAT_MAT4); + InsertEntry("mat2x3"sv, GL_FLOAT_MAT2x3); + InsertEntry("mat2x4"sv, GL_FLOAT_MAT2x4); + InsertEntry("mat3x2"sv, GL_FLOAT_MAT3x2); + InsertEntry("mat3x4"sv, GL_FLOAT_MAT3x4); + InsertEntry("mat4x2"sv, GL_FLOAT_MAT4x2); + InsertEntry("mat4x3"sv, GL_FLOAT_MAT4x3); + + InsertEntry("dmat2"sv, GL_DOUBLE_MAT2); + InsertEntry("dmat3"sv, GL_DOUBLE_MAT3); + InsertEntry("dmat4"sv, GL_DOUBLE_MAT4); + InsertEntry("dmat2x3"sv, GL_DOUBLE_MAT2x3); + InsertEntry("dmat2x4"sv, GL_DOUBLE_MAT2x4); + InsertEntry("dmat3x2"sv, GL_DOUBLE_MAT3x2); + InsertEntry("dmat3x4"sv, GL_DOUBLE_MAT3x4); + InsertEntry("dmat4x2"sv, GL_DOUBLE_MAT4x2); + InsertEntry("dmat4x3"sv, GL_DOUBLE_MAT4x3); + + InsertEntry("sampler1D"sv, GL_SAMPLER_1D); + InsertEntry("sampler2D"sv, GL_SAMPLER_2D); + InsertEntry("sampler3D"sv, GL_SAMPLER_3D); + InsertEntry("samplerCube"sv, GL_SAMPLER_CUBE); + InsertEntry("sampler1DShadow"sv, GL_SAMPLER_1D_SHADOW); + InsertEntry("sampler2DShadow"sv, GL_SAMPLER_2D_SHADOW); + InsertEntry("sampler1DArray"sv, GL_SAMPLER_1D_ARRAY); + InsertEntry("sampler2DArray"sv, GL_SAMPLER_2D_ARRAY); + InsertEntry("sampler1DArrayShadow"sv, GL_SAMPLER_1D_ARRAY_SHADOW); + InsertEntry("sampler2DArrayShadow"sv, GL_SAMPLER_2D_ARRAY_SHADOW); + InsertEntry("sampler2DMultisample"sv, GL_SAMPLER_2D_MULTISAMPLE); + InsertEntry("sampler2DMultisampleArray"sv, GL_SAMPLER_2D_MULTISAMPLE_ARRAY); + InsertEntry("samplerCubeShadow"sv, GL_SAMPLER_CUBE_SHADOW); + InsertEntry("samplerBuffer"sv, GL_SAMPLER_BUFFER); + InsertEntry("sampler2DRect"sv, GL_SAMPLER_2D_RECT); + InsertEntry("sampler2DRectShadow"sv, GL_SAMPLER_2D_RECT_SHADOW); + + InsertEntry("isampler1D"sv, GL_INT_SAMPLER_1D); + InsertEntry("isampler2D"sv, GL_INT_SAMPLER_2D); + InsertEntry("isampler3D"sv, GL_INT_SAMPLER_3D); + InsertEntry("isamplerCube"sv, GL_INT_SAMPLER_CUBE); + InsertEntry("isampler1DArray"sv, GL_INT_SAMPLER_1D_ARRAY); + InsertEntry("isampler2DArray"sv, GL_INT_SAMPLER_2D_ARRAY); + InsertEntry("isampler2DMultisample"sv, GL_INT_SAMPLER_2D_MULTISAMPLE); + InsertEntry("isampler2DMultisampleArray"sv, GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY); + InsertEntry("isamplerBuffer"sv, GL_INT_SAMPLER_BUFFER); + InsertEntry("isampler2DRect"sv, GL_INT_SAMPLER_2D_RECT); + + InsertEntry("usampler1D"sv, GL_UNSIGNED_INT_SAMPLER_1D); + InsertEntry("usampler2D"sv, GL_UNSIGNED_INT_SAMPLER_2D); + InsertEntry("usampler3D"sv, GL_UNSIGNED_INT_SAMPLER_3D); + InsertEntry("usamplerCube"sv, GL_UNSIGNED_INT_SAMPLER_CUBE); + InsertEntry("usampler1DArray"sv, GL_UNSIGNED_INT_SAMPLER_1D_ARRAY); + InsertEntry("usampler2DArray"sv, GL_UNSIGNED_INT_SAMPLER_2D_ARRAY); + InsertEntry("usampler2DMultisample"sv, GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE); + InsertEntry("usampler2DMultisampleArray"sv, GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY); + InsertEntry("usamplerBuffer"sv, GL_UNSIGNED_INT_SAMPLER_BUFFER); + InsertEntry("usampler2DRect"sv, GL_UNSIGNED_INT_SAMPLER_2D_RECT); + } + + void InsertEntry(std::string_view name, GLenum value) { + enum2Name.try_emplace(value, name); + name2Enum.try_emplace(name, value); + } +} const kGLTypeInfo; +} // namespace ProjectBrussel_UNITY_ID + +std::string_view Tags::GLTypeToString(GLenum value) { + using namespace ProjectBrussel_UNITY_ID; + auto iter = kGLTypeInfo.enum2Name.find(value); + if (iter != kGLTypeInfo.enum2Name.end()) { + return iter->second; + } else { + return std::string_view(); + } +} + +GLenum Tags::GLTypeFromString(std::string_view name) { + using namespace ProjectBrussel_UNITY_ID; + auto iter = kGLTypeInfo.name2Enum.find(name); + if (iter != kGLTypeInfo.name2Enum.end()) { + return iter->second; + } else { + return 0; + } +} + +#include <generated/GraphicsTags.gs.inl> diff --git a/src/brussel.engine/GraphicsTags.hpp b/src/brussel.engine/GraphicsTags.hpp new file mode 100644 index 0000000..f9628b2 --- /dev/null +++ b/src/brussel.engine/GraphicsTags.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include <MacrosCodegen.hpp> + +#include <glad/glad.h> +#include <limits> +#include <string> +#include <string_view> + +namespace Tags { +/// Vertex element semantics, used to identify the meaning of vertex buffer contents +enum VertexElementSemantic { + // clang-format off + BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics) + // clang-format on + + /// Position, typically VET_Float3 + VES_Position, + /// Blending weights + VES_BlendWeights, + /// Blending indices + VES_BlendIndices, + /// Normal, typically VET_Float3 + VES_Normal, + /// Colour, typically VET_Ubyte4 + VES_Color1, + VES_Color2, + VES_Color3, + /// Texture coordinates, typically VET_Float2 + VES_TexCoords1, + VES_TexCoords2, + VES_TexCoords3, + /// Binormal (Y axis if normal is Z) + VES_Binormal, + /// Tangent (X axis if normal is Z) + VES_Tangent, + /// Default semantic + VES_Generic, + VES_COUNT, +}; + +enum VertexElementType { + // clang-format off + BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics) + // clang-format on + + VET_Float1, + VET_Float2, + VET_Float3, + VET_Float4, + + VET_Short2, + VET_Short4, + VET_Ubyte4, + + // the following are not universally supported on all hardware: + VET_Double1, + VET_Double2, + VET_Double3, + VET_Double4, + VET_Ushort2, + VET_Ushort4, + VET_Int1, + VET_Int2, + VET_Int3, + VET_Int4, + VET_Uint1, + VET_Uint2, + VET_Uint3, + VET_Uint4, + VET_Byte4, /// signed bytes + + VET_Byte4Norm, /// signed bytes (normalized to -1..1) + VET_Ubyte4Norm, /// unsigned bytes (normalized to 0..1) + VET_Short2Norm, /// signed shorts (normalized to -1..1) + VET_Short4Norm, + VET_Ushort2Norm, /// unsigned shorts (normalized to 0..1) + VET_Ushort4Norm, +}; +constexpr auto VET_NORM_BEGIN = VET_Byte4Norm; +constexpr auto VET_NORM_END = VET_Ushort4Norm; + +int SizeOf(VertexElementType type); +int VectorLenOf(VertexElementType type); +GLenum FindGLType(VertexElementType type); +bool IsNormalized(VertexElementType type); + +enum IndexType { + IT_16Bit, + IT_32Bit, +}; + +int SizeOf(IndexType type); +GLenum FindGLType(IndexType type); + +enum TexFilter { + // clang-format off + BRUSSEL_ENUM(ToString, FromString, ExcludeHeuristics) + // clang-format on + + TF_Linear, + TF_Nearest, +}; + +std::string_view GLTypeToString(GLenum); +GLenum GLTypeFromString(std::string_view name); + +constexpr auto kInvalidLocation = std::numeric_limits<GLuint>::max(); +} // namespace Tags + +#include <generated/GraphicsTags.gh.inl> diff --git a/src/brussel.engine/Image.cpp b/src/brussel.engine/Image.cpp new file mode 100644 index 0000000..3673acc --- /dev/null +++ b/src/brussel.engine/Image.cpp @@ -0,0 +1,101 @@ +#include "Image.hpp" + +#include <stb_image.h> +#include <stb_rect_pack.h> +#include <cstring> + +Image::Image() + : mSize{} + , mChannels{ 0 } { +} + +bool Image::InitFromImageFile(const char* filePath, int desiredChannels) { + // Dimensions of the image in + int width, height; + // Number of channels that the image has, we'll get `desiredChannels` channels in our output (if it's non-0, which is the default argument) + int channels; + + // NOTE: don't free, the data is passed to std::unique_ptr + auto result = (uint8_t*)stbi_load(filePath, &width, &height, &channels, desiredChannels); + if (!result) { + return false; + } + + mData.reset(result); + mSize = { width, height }; + mChannels = desiredChannels == 0 ? channels : desiredChannels; + return true; +} + +bool Image::InitFromImageData(std::span<uint8_t> data, int desiredChannels) { + int width, height; + int channels; + + // NOTE: don't free, the data is passed to std::unique_ptr + auto result = (uint8_t*)stbi_load_from_memory(data.data(), data.size(), &width, &height, &channels, desiredChannels); + if (!result) { + return false; + } + + mData.reset(result); + mSize = { width, height }; + mChannels = desiredChannels == 0 ? channels : desiredChannels; + return true; +} + +bool Image::InitFromPixels(std::span<uint8_t> pixels, glm::ivec2 dimensions, int channels) { + mData = std::make_unique<uint8_t[]>(pixels.size()); + std::memcpy(mData.get(), pixels.data(), pixels.size()); + mSize = dimensions; + mChannels = channels; + return true; +} + +bool Image::InitFromPixels(std::unique_ptr<uint8_t[]> pixels, glm::ivec2 dimensions, int channels) { + mData = std::move(pixels); + mSize = dimensions; + mChannels = channels; + return true; +} + +RgbaColor Image::GetPixel(int x, int y) const { + size_t offset = (y * mSize.x + x) * mChannels; + RgbaColor color; + color.r = mData.get()[offset + 0]; + color.g = mData.get()[offset + 1]; + color.b = mData.get()[offset + 2]; + color.a = mData.get()[offset + 3]; + return color; +} + +void Image::SetPixel(int x, int y, RgbaColor color) { + size_t offset = (y * mSize.x + x) * mChannels; + mData.get()[offset + 0] = color.r; + mData.get()[offset + 1] = color.g; + mData.get()[offset + 2] = color.b; + mData.get()[offset + 3] = color.a; +} + +uint8_t* Image::GetDataPtr() const { + return mData.get(); +} + +size_t Image::GetDataLength() const { + return mSize.x * mSize.y * mChannels * sizeof(uint8_t); +} + +std::span<uint8_t> Image::GetData() const { + return { mData.get(), GetDataLength() }; +} + +glm::ivec2 Image::GetSize() const { + return mSize; +} + +int Image::GetChannels() const { + return mChannels; +} + +bool Image::IsEmpty() const { + return mSize.x == 0 || mSize.y == 0; +} diff --git a/src/brussel.engine/Image.hpp b/src/brussel.engine/Image.hpp new file mode 100644 index 0000000..c577c24 --- /dev/null +++ b/src/brussel.engine/Image.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "Color.hpp" +#include "RcPtr.hpp" + +#include <cstdint> +#include <glm/glm.hpp> +#include <memory> +#include <span> + +/// Image is a 2d array of pixels, stored as a continuous array in memory, with the first pixel +/// being the top-left pixel. If a vertically flipped image data is needed, load using stb_image +/// yourself, or flip the data here. +class Image : public RefCounted { +private: + std::unique_ptr<uint8_t[]> mData; + glm::ivec2 mSize; + int mChannels; + +public: + Image(); + + bool InitFromImageFile(const char* filePath, int desiredChannels = 0); + bool InitFromImageData(std::span<uint8_t> data, int desiredChannels = 0); + bool InitFromPixels(std::span<uint8_t> pixels, glm::ivec2 dimensions, int channels); + bool InitFromPixels(std::unique_ptr<uint8_t[]> pixels, glm::ivec2 dimensions, int channels); + + /// Get the pixel at the given location. + RgbaColor GetPixel(int x, int y) const; + void SetPixel(int x, int y, RgbaColor color); + + uint8_t* GetDataPtr() const; + size_t GetDataLength() const; + std::span<uint8_t> GetData() const; + glm::ivec2 GetSize() const; + int GetChannels() const; + bool IsEmpty() const; +}; diff --git a/src/brussel.engine/Input.cpp b/src/brussel.engine/Input.cpp new file mode 100644 index 0000000..9f304ff --- /dev/null +++ b/src/brussel.engine/Input.cpp @@ -0,0 +1,23 @@ +#include "Input.hpp" + +#include <cassert> + +GLFWkeyboard* InputState::FindKeyboard(std::string_view name) { + assert(false); + // TODO +} + +GlfwKeyboardAttachment* InputState::ConnectKeyboard(GLFWkeyboard* keyboard) { + auto attachment = new GlfwKeyboardAttachment(); + glfwSetKeyboardUserPointer(keyboard, attachment); + + return attachment; +} + +void InputState::DisconnectKeyboard(GLFWkeyboard* keyboard) { + InputState::instance->DisconnectKeyboard(keyboard); + auto attachment = static_cast<GlfwKeyboardAttachment*>(glfwGetKeyboardUserPointer(keyboard)); + // Defensive: this GLFWkeyboard* will be deleted after this callback ends anyways + glfwSetKeyboardUserPointer(keyboard, nullptr); + delete attachment; +} diff --git a/src/brussel.engine/Input.hpp b/src/brussel.engine/Input.hpp new file mode 100644 index 0000000..feb50f0 --- /dev/null +++ b/src/brussel.engine/Input.hpp @@ -0,0 +1,23 @@ +#pragma once + +#define GLFW_INCLUDE_NONE +#include <GLFW/glfw3.h> + +#include <string_view> +#include <utility> +#include <vector> + +/// Extra data attached to a GLFWkeyboard object +struct GlfwKeyboardAttachment { +}; + +class InputState { +public: + static inline InputState* instance = nullptr; + +public: + GLFWkeyboard* FindKeyboard(std::string_view name); + + GlfwKeyboardAttachment* ConnectKeyboard(GLFWkeyboard* keyboard); + void DisconnectKeyboard(GLFWkeyboard* keyboard); +}; diff --git a/src/brussel.engine/Ires.cpp b/src/brussel.engine/Ires.cpp new file mode 100644 index 0000000..0529395 --- /dev/null +++ b/src/brussel.engine/Ires.cpp @@ -0,0 +1,436 @@ +#include "Ires.hpp" + +#include "AppConfig.hpp" +#include "EditorCore.hpp" +#include "EditorUtils.hpp" +#include "Material.hpp" +#include "Shader.hpp" +#include "Sprite.hpp" +#include "Texture.hpp" + +#include <Macros.hpp> +#include <Metadata.hpp> +#include <RapidJsonHelper.hpp> +#include <ScopeGuard.hpp> +#include <Utils.hpp> + +#include <imgui.h> +#include <misc/cpp/imgui_stdlib.h> +#include <rapidjson/document.h> +#include <rapidjson/filereadstream.h> +#include <rapidjson/filewritestream.h> +#include <rapidjson/prettywriter.h> +#include <rapidjson/writer.h> +#include <algorithm> +#include <cassert> + +namespace fs = std::filesystem; +using namespace std::literals; + +IresObject::IresObject(Kind kind) + : mKind{ kind } { +} + +std::unique_ptr<IresObject> IresObject::Create(Kind kind) { + switch (kind) { + case KD_Texture: return std::make_unique<IresTexture>(); + case KD_Shader: return std::make_unique<IresShader>(); + case KD_Material: return std::make_unique<IresMaterial>(); + case KD_SpriteFiles: return std::make_unique<IresSpriteFiles>(); + case KD_Spritesheet: return std::make_unique<IresSpritesheet>(); + case KD_COUNT: break; + } + return nullptr; +} + +bool IresObject::IsAnnoymous() const { + return mName.empty(); +} + +void IresObject::SetName(std::string name) { + if (mMan) { + mMan->Rename(this, std::move(name)); + } else { + mName = std::move(name); + } +} + +void IresObject::ShowNameSafe(IresObject* ires) { + if (ires) { + ires->ShowName(); + } else { + ShowNameNull(); + } +} + +void IresObject::ShowNameNull() { + ImGui::Text("<null>"); +} + +void IresObject::ShowName() const { + if (IsAnnoymous()) { + ImGui::Text("<annoymous %p>", (void*)this); + } else { + ImGui::Text("%s", mName.c_str()); + } +} + +void IresObject::ShowReferenceSafe(IEditor& editor, IresObject* ires) { + if (ires) { + ires->ShowReference(editor); + } else { + ShowReferenceNull(editor); + } +} + +void IresObject::ShowReferenceNull(IEditor& editor) { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]); + ImGui::Text("<null>"); + ImGui::PopStyleColor(); + ImGui::AddUnderLine(ImGui::GetStyle().Colors[ImGuiCol_Button]); +} + +void IresObject::ShowReference(IEditor& editor) { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]); + if (IsAnnoymous()) { + ImGui::Text("<annoymous %p>", (void*)this); + } else { + ImGui::Text("%s", mName.c_str()); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + editor.GetInspector().SelectTarget(IEditorInspector::ITT_Ires, this); + } + ImGui::AddUnderLine(ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]); + } else { + ImGui::AddUnderLine(ImGui::GetStyle().Colors[ImGuiCol_Button]); + } +} + +void IresObject::ShowEditor(IEditor& editor) { + ImGui::Text("%.*s", PRINTF_STRING_VIEW(Metadata::EnumToString(mKind))); + + bool isAnnoymous = mName.empty(); + if (isAnnoymous) { + ImGui::Text("Name: <annoymous: %p>", (void*)this); + } else { + ImGui::Text("Name: %s", mName.c_str()); + } + + if (mUid.IsNull()) { + ImGui::TextUnformatted("Uid: <none>"); + } else { + ImGui::Text("Uid: %lx-%lx", mUid.upper, mUid.lower); + } +} + +void IresObject::WriteFull(IresWritingContext& ctx, IresObject* ires, rapidjson::Value& value, rapidjson::Document& root) { + rapidjson::Value rvIres(rapidjson::kObjectType); + ires->Write(ctx, rvIres, root); + + value.AddMember("Type", rapidjson::StringRef(Metadata::EnumToString(ires->GetKind())), root.GetAllocator()); + value.AddMember("Uid", ires->mUid.Write(root), root.GetAllocator()); + value.AddMember("Value", rvIres, root.GetAllocator()); +} + +std::unique_ptr<IresObject> IresObject::ReadFull(IresLoadingContext& ctx, const rapidjson::Value& value) { + auto ires = ReadBasic(value); + if (!ires) { + return nullptr; + } + + if (!ReadPartial(ctx, ires.get(), value)) { + return nullptr; + } + + return ires; +} + +std::unique_ptr<IresObject> IresObject::ReadBasic(const rapidjson::Value& value) { + std::unique_ptr<IresObject> ires; + Uid uid; + for (auto it = value.MemberBegin(); it != value.MemberEnd(); ++it) { + if (it->name == "Type"_rj_sv) { + if (it->value.GetType() != rapidjson::kStringType) + return nullptr; + auto kind = Metadata::EnumFromString<Kind>(rapidjson::AsStringView(it->value)); + if (!kind.has_value()) + return nullptr; + if ((ires = Create(*kind)) == nullptr) + return nullptr; + } else if (it->name == "Uid"_rj_sv) { + // FIXME this needs to do type checks + uid.Read(it->value); + } + } + if (ires == nullptr || uid.IsNull()) + return nullptr; + + ires->mUid = uid; + return ires; +} + +bool IresObject::ReadPartial(IresLoadingContext& ctx, IresObject* ires, const rapidjson::Value& value) { + auto rvValue = rapidjson::GetProperty(value, "Value"sv); + if (!rvValue) return false; + ires->Read(ctx, *rvValue); + return true; +} + +void IresObject::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const { +} + +void IresObject::Read(IresLoadingContext& ctx, const rapidjson::Value& value) { +} + +void IresManager::DiscoverFilesDesignatedLocation() { + auto path = AppConfig::assetDirPath / "Ires"; + DiscoverFiles(path); +} + +void IresManager::DiscoverFiles(const fs::path& dir) { + struct LoadCandidate { + fs::path path; + rapidjson::Document data; + RcPtr<IresObject> ires; + }; + + class IresLoadTimeContext final : public IresLoadingContext { + public: + // NOTE: pointer stability required + robin_hood::unordered_node_map<Uid, LoadCandidate> candidates; + std::vector<std::vector<LoadCandidate*>> candidatesByKind; + + IresLoadTimeContext() { + candidatesByKind.resize((int)IresObject::KD_COUNT); + } + + std::vector<LoadCandidate*>& GetByKind(IresObject::Kind kind) { + int i = static_cast<int>(kind); + return candidatesByKind[i]; + } + + virtual IresObject* FindIres(const Uid& uid) const override { + auto iter = candidates.find(uid); + if (iter != candidates.end()) { + auto& cand = iter->second; + return cand.ires.Get(); + } else { + return nullptr; + } + } + } ctx; + + for (auto& item : fs::directory_iterator(dir)) { + if (!item.is_regular_file()) { + continue; + } + if (item.path().extension() != ".json") { + continue; + } + + auto file = Utils::OpenCstdioFile(item.path(), Utils::Read); + if (!file) { + fprintf(stderr, "Ires file [" PLATFORM_PATH_STR "] Failed to open file.", item.path().c_str()); + continue; + } + DEFER { + fclose(file); + }; + + char readerBuffer[65536]; + rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer)); + + rapidjson::Document root; + root.ParseStream(stream); + + auto ires = IresObject::ReadBasic(root); + if (!ires) { + fprintf(stderr, "Ires file: [" PLATFORM_PATH_STR "] Failed to parse header.\n", item.path().c_str()); + continue; + } + + // Load name from filename + ires->mName = item.path().filename().replace_extension().string(); + auto& iresName = ires->mName; + + // Load uid should be handled by IresObject::ReadBasic + assert(!ires->mUid.IsNull()); + auto iresUid = ires->GetUid(); + + auto iresKind = ires->GetKind(); + + auto&& [iter, DISCARD] = ctx.candidates.try_emplace( + iresUid, + LoadCandidate{ + .path = item.path(), + .data = std::move(root), + .ires = RcPtr(ires.release()), + }); + auto& cand = iter->second; + + ctx.GetByKind(iresKind).push_back(&cand); + } + + // Load Ires in order by type, there are dependencies between them + // TODO full arbitary dependency between Ires + for (int i = 0; i < (int)IresObject::KD_COUNT; ++i) { + auto kind = static_cast<IresObject::Kind>(i); + auto& list = ctx.GetByKind(kind); + for (auto cand : list) { + auto& ires = cand->ires; + + if (!IresObject::ReadPartial(ctx, ires.Get(), cand->data)) { + fprintf(stderr, "Ires file: [" PLATFORM_PATH_STR "] Failed to parse object data.\n", cand->path.c_str()); + continue; + } + + ires->mMan = this; + mObjByUid.try_emplace(ires->GetUid(), ires); + } + } +} + +std::pair<IresObject*, bool> IresManager::Add(IresObject* ires) { + auto& name = ires->mName; + if (name.empty()) { + name = Utils::MakeRandomNumberedName(Metadata::EnumToString(ires->GetKind()).data()); + } + + auto& uid = ires->mUid; + if (uid.IsNull()) { + uid = Uid::Create(); + } + + auto [iter, inserted] = mObjByUid.try_emplace(uid, ires); + if (inserted) { + ires->mMan = this; + // TODO handle full path + return { ires, true }; + } else { + return { iter->second.Get(), false }; + } +} + +IresObject* IresManager::Load(const fs::path& filePath) { + auto file = Utils::OpenCstdioFile(filePath, Utils::Read); + if (!file) return nullptr; + DEFER { + fclose(file); + }; + + char readerBuffer[65536]; + rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer)); + + rapidjson::Document root; + root.ParseStream(stream); + + auto ires = IresObject::ReadFull(*this, root); + if (!ires) { + return nullptr; + } + + // Load uid should be handled by IresObject::ReadFull + assert(!ires->mUid.IsNull()); + // Load name from filename + ires->mName = filePath.filename().replace_extension().string(); + Add(ires.get()); + + return ires.release(); +} + +static fs::path GetDesignatedPath(IresObject* ires) { + return AppConfig::assetDirPath / "Ires" / fs::path(ires->GetName()).replace_extension(".json"); +} + +void IresManager::Delete(IresObject* ires) { + // TODO +} + +bool IresManager::Rename(IresObject* ires, std::string newName) { + auto oldPath = GetDesignatedPath(ires); + ires->mName = std::move(newName); + auto newPath = GetDesignatedPath(ires); + if (fs::exists(oldPath)) { + fs::rename(oldPath, newPath); + } + + // TODO validate no name duplication +#if 0 + if (mObjByPath.contains(newName)) { + return false; + } + + // Keep the material from being deleted, in case the old entry in map is the only one existing + RcPtr rc(ires); + + // Remove old entry (must do before replacing Material::mName, because the std::string_view in the map is a reference to it) + mObjByPath.erase(ires->GetName()); + + // Add new entry + ires->mName = std::move(newName); + // TODO handle full path + mObjByPath.try_emplace(ires->GetName(), ires); +#endif + return true; +} + +void IresManager::Reload(IresObject* ires) { + auto file = Utils::OpenCstdioFile(GetDesignatedPath(ires), Utils::Read); + if (!file) return; + DEFER { + fclose(file); + }; + + char readerBuffer[65536]; + rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer)); + + rapidjson::Document root; + root.ParseStream(stream); + + IresObject::ReadPartial(*this, ires, root); +} + +void IresManager::Save(IresObject* ires) { + Save(ires, GetDesignatedPath(ires)); +} + +void IresManager::Save(IresObject* ires, const fs::path& filePath) { + rapidjson::Document root(rapidjson::kObjectType); + + IresObject::WriteFull(*this, ires, root, root); + + auto file = Utils::OpenCstdioFile(filePath, Utils::WriteTruncate); + if (!file) return; + DEFER { + fclose(file); + }; + + char writerBuffer[65536]; + rapidjson::FileWriteStream stream(file, writerBuffer, sizeof(writerBuffer)); + rapidjson::PrettyWriter<rapidjson::FileWriteStream> writer(stream); + // We no longer need this after disabling BRUSSEL_Uid_WRITE_USE_ARRAY +// writer.SetFormatOptions(rapidjson::PrettyFormatOptions::kFormatSingleLineArray); +#if BRUSSEL_Uid_WRITE_USE_ARRAY +# warning "Writing Uid in array format but single line formatting isn't enabled, this might cause excessively long ires files." +#endif + root.Accept(writer); +} + +void IresManager::OverwriteAllToDisk() { + for (const auto& [DISCARD, ires] : mObjByUid) { + Save(ires.Get()); + } +} + +IresObject* IresManager::FindIres(const Uid& uid) const { + auto iter = mObjByUid.find(uid); + if (iter != mObjByUid.end()) { + return iter->second.Get(); + } else { + return nullptr; + } +} + +#include <generated/Ires.gs.inl> diff --git a/src/brussel.engine/Ires.hpp b/src/brussel.engine/Ires.hpp new file mode 100644 index 0000000..e2e79bd --- /dev/null +++ b/src/brussel.engine/Ires.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include "EditorAttachment.hpp" +#include "EditorCore.hpp" + +#include <MacrosCodegen.hpp> +#include <RcPtr.hpp> +#include <Uid.hpp> +#include <Utils.hpp> + +#include <rapidjson/fwd.h> +#include <robin_hood.h> +#include <filesystem> +#include <memory> +#include <string_view> + +// Forward declarations +class IresManager; +class IresWritingContext; +class IresLoadingContext; + +namespace Tags { +enum class IresObjectKind { + // clang-format off + BRUSSEL_ENUM(ToString, FromString, RemovePrefix KD_, AddPrefix Ires, ExcludeHeuristics) + // clang-format on + + KD_Texture, + KD_Shader, + KD_Material, + KD_SpriteFiles, + KD_Spritesheet, + KD_COUNT, +}; +} // namespace Tags + +class IresObject : public RefCounted { + friend class IresManager; + +public: + using Kind = Tags::IresObjectKind; + using enum Tags::IresObjectKind; + +private: + std::string mName; // Serialized as filename + Uid mUid; // Serialized in full mode + std::unique_ptr<EditorAttachment> mEditorAttachment; // Transient + IresManager* mMan = nullptr; // Transient + Kind mKind; // Serialized in full mode + +public: + IresObject(Kind kind); + virtual ~IresObject() = default; + + static std::unique_ptr<IresObject> Create(Kind kind); + Kind GetKind() const { return mKind; } + + IresManager* GetAssociatedManager() const { return mMan; } + bool IsAnnoymous() const; + const std::string& GetName() const { return mName; } + void SetName(std::string name); + const Uid& GetUid() const { return mUid; } + + static void ShowNameSafe(IresObject* ires); + static void ShowNameNull(); + void ShowName() const; + + static void ShowReferenceSafe(IEditor& editor, IresObject* ires); + static void ShowReferenceNull(IEditor& editor); + void ShowReference(IEditor& editor); + + virtual void ShowEditor(IEditor& editor); + + EditorAttachment* GetEditorAttachment() const { return mEditorAttachment.get(); } + void SetEditorAttachment(EditorAttachment* attachment) { mEditorAttachment.reset(attachment); } + + static void WriteFull(IresWritingContext& ctx, IresObject* ires, rapidjson::Value& value, rapidjson::Document& root); + + /// Sequentially call ReadBasic() and then ReadPartial() + static std::unique_ptr<IresObject> ReadFull(IresLoadingContext& ctx, const rapidjson::Value& value); + /// Reads the type and UID of the object from data, and return a newly constructed Ires object. + static std::unique_ptr<IresObject> ReadBasic(const rapidjson::Value& value); + /// Reads all object-speific data from the root-level data object into the given Ires object. + static bool ReadPartial(IresLoadingContext& ctx, IresObject* ires, const rapidjson::Value& value); + + virtual void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const; + virtual void Read(IresLoadingContext& ctx, const rapidjson::Value& value); + +protected: + rapidjson::Value WriteIresRef(IresObject* object); +}; + +class IresWritingContext { +public: + virtual ~IresWritingContext() = default; +}; + +class IresLoadingContext { +public: + virtual ~IresLoadingContext() = default; + virtual IresObject* FindIres(const Uid& uid) const = 0; +}; + +class IresManager final : public IresWritingContext, public IresLoadingContext { +public: + static inline IresManager* instance = nullptr; + +private: + robin_hood::unordered_map<Uid, RcPtr<IresObject>> mObjByUid; + +public: + void DiscoverFilesDesignatedLocation(); + void DiscoverFiles(const std::filesystem::path& dir); + + std::pair<IresObject*, bool> Add(IresObject* mat); + IresObject* Load(const std::filesystem::path& filePath); + void Delete(IresObject* ires); + bool Rename(IresObject* ires, std::string newName); + + void Reload(IresObject* ires); + void Save(IresObject* ires); + void Save(IresObject* ires, const std::filesystem::path& filePath); + + void OverwriteAllToDisk(); + + const auto& GetObjects() const { return mObjByUid; } + virtual IresObject* FindIres(const Uid& uid) const override; +}; + +#include <generated/Ires.gh.inl> diff --git a/src/brussel.engine/Level.cpp b/src/brussel.engine/Level.cpp new file mode 100644 index 0000000..076e5d5 --- /dev/null +++ b/src/brussel.engine/Level.cpp @@ -0,0 +1,228 @@ +#include "Level.hpp" + +#include "AppConfig.hpp" + +#include <PodVector.hpp> +#include <RapidJsonHelper.hpp> +#include <ScopeGuard.hpp> +#include <Utils.hpp> + +#include <imgui.h> +#include <rapidjson/document.h> +#include <rapidjson/filereadstream.h> +#include <rapidjson/filewritestream.h> +#include <rapidjson/prettywriter.h> +#include <cstdio> +#include <filesystem> + +using namespace std::literals; +namespace fs = std::filesystem; + +constexpr auto kParentToRootObject = std::numeric_limits<size_t>::max(); +constexpr auto kInvalidEntryId = std::numeric_limits<size_t>::max(); + +struct Level::InstanciationEntry { + // If set to std::numeric_limits<size_t>::max(), this object is parented to the "root" provided when instanciating + size_t parentId; + rapidjson::Document data; +}; + +Level::Level() = default; + +Level::~Level() = default; + +void Level::Instanciate(GameObject* relRoot) const { + auto objectsLut = std::make_unique<GameObject*[]>(mEntries.size()); + for (auto& entry : mEntries) { + GameObject* parent; + if (entry.parentId == kParentToRootObject) { + parent = relRoot; + } else { + parent = objectsLut[entry.parentId]; + } + + // TODO deser object + } +} + +void Level::ShowInstanciationEntries(IEditor& editor) { + for (auto& entry : mEntries) { + // TODO + } +} + +void LevelManager::DiscoverFilesDesignatedLocation() { + auto path = AppConfig::assetDirPath / "Levels"; + DiscoverFiles(path); +} + +void LevelManager::DiscoverFiles(const std::filesystem::path& dir) { + for (auto& item : fs::directory_iterator(dir)) { + auto& path = item.path(); + if (!item.is_regular_file()) { + continue; + } + if (path.extension() != ".json") { + continue; + } + + // Parse uid from filename, map key + Uid uid; + uid.ReadString(path.filename().string()); + + // Map value + LoadableObject obj; + obj.filePath = path; + + mObjByUid.try_emplace(uid, std::move(obj)); + } +} + +Level* LevelManager::FindLevel(const Uid& uid) const { + auto iter = mObjByUid.find(uid); + if (iter != mObjByUid.end()) { + return iter->second.level.Get(); + } else { + return nullptr; + } +} + +#define BRUSSEL_DEF_LEVEL_NAME "New Level" +#define BRUSSEL_DEF_LEVEL_DESC "No description." + +Level* LevelManager::LoadLevel(const Uid& uid) { + auto iter = mObjByUid.find(uid); + if (iter != mObjByUid.end()) { + auto& ldObj = iter->second; + if (ldObj.level != nullptr) { + auto file = Utils::OpenCstdioFile(ldObj.filePath, Utils::Read, false); + if (!file) { + fprintf(stderr, "Cannot open file level file %s that was discovered on game startup.", ldObj.filePath.string().c_str()); + return nullptr; + } + DEFER { fclose(file); }; + + char readerBuffer[65536]; + rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer)); + + rapidjson::Document root; + root.ParseStream(stream); + + Level* level; + ldObj.level.Attach(level = new Level()); + + level->mMan = this; + level->mUid = uid; + +#if defined(BRUSSEL_DEV_ENV) + BRUSSEL_JSON_GET_DEFAULT(root, "Name", std::string, ldObj.name, BRUSSEL_DEF_LEVEL_NAME); + BRUSSEL_JSON_GET_DEFAULT(root, "Description", std::string, ldObj.description, BRUSSEL_DEF_LEVEL_DESC) +#endif + + auto rvEntries = rapidjson::GetProperty(root, rapidjson::kArrayType, "DataEntries"sv); + if (!rvEntries) return nullptr; + for (auto iter = rvEntries->Begin(); iter != rvEntries->End(); ++iter) { + Level::InstanciationEntry entry; + + BRUSSEL_JSON_GET_DEFAULT(*iter, "ParentId", int, entry.parentId, kInvalidEntryId); + + auto rvDataEntry = rapidjson::GetProperty(*iter, "Data"sv); + if (!rvDataEntry) return nullptr; + entry.data.CopyFrom(*iter, entry.data.GetAllocator()); + + level->mEntries.push_back(std::move(entry)); + } + } + return ldObj.level.Get(); + } else { + return nullptr; + } +} + +void LevelManager::PrepareLevel(const Uid& uid) { + // TODO +} + +LevelManager::LoadableObject& LevelManager::AddLevel(const Uid& uid) { + auto&& [iter, inserted] = mObjByUid.try_emplace(uid); + auto& ldObj = iter->second; + ldObj.level->mUid = uid; +#if defined(BRUSSEL_DEV_ENV) + ldObj.name = BRUSSEL_DEF_LEVEL_NAME; + ldObj.description = BRUSSEL_DEF_LEVEL_DESC; +#endif + return ldObj; +} + +void LevelManager::SaveLevel(const Uid& uid) const { + auto iter = mObjByUid.find(uid); + if (iter == mObjByUid.end()) return; + auto& obj = iter->second; + + SaveLevelImpl(obj, obj.filePath); +} + +void LevelManager::SaveLevel(const Uid& uid, const std::filesystem::path& path) const { + auto iter = mObjByUid.find(uid); + if (iter == mObjByUid.end()) return; + auto& obj = iter->second; + + SaveLevelImpl(obj, path); +} + +void LevelManager::SaveLevelImpl(const LoadableObject& obj, const std::filesystem::path& path) const { + rapidjson::Document root; + + // TODO + + auto file = Utils::OpenCstdioFile(path, Utils::WriteTruncate); + if (!file) return; + DEFER { fclose(file); }; + + char writerBuffer[65536]; + rapidjson::FileWriteStream stream(file, writerBuffer, sizeof(writerBuffer)); + rapidjson::Writer<rapidjson::FileWriteStream> writer(stream); + root.Accept(writer); +} + +LevelWrapperObject::LevelWrapperObject(GameWorld* world) + : GameObject(KD_LevelWrapper, world) // +{ + mStopFreePropagation = true; +} + +LevelWrapperObject::~LevelWrapperObject() { + // Destruction/freeing of this object is handled by our parent + for (auto child : GetChildren()) { + FreeRecursive(child); + } +} + +void LevelWrapperObject::SetBoundLevel(Level* level) { + if (mLevel != level) { + mLevel.Attach(level); + + // Cleanup old children + // TODO needs to Resleep()? + auto children = RemoveAllChildren(); + for (auto child : children) { + FreeRecursive(child); + } + } + + level->Instanciate(this); + + PodVector<GameObject*> stack; + stack.push_back(this); + + while (!stack.empty()) { + auto obj = stack.back(); + stack.pop_back(); + + for (auto child : obj->GetChildren()) { + stack.push_back(child); + } + + obj->Awaken(); + } +} diff --git a/src/brussel.engine/Level.hpp b/src/brussel.engine/Level.hpp new file mode 100644 index 0000000..c030b8e --- /dev/null +++ b/src/brussel.engine/Level.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include "EditorCore.hpp" +#include "GameObject.hpp" + +#include <MacrosCodegen.hpp> +#include <RcPtr.hpp> +#include <Uid.hpp> + +#include <robin_hood.h> +#include <filesystem> +#include <vector> + +// Forward declarations +class Level; +class LevelManager; + +/// Represents a seralized GameObject tree. +class Level : public RefCounted { + friend class LevelManager; + +private: + struct InstanciationEntry; + + LevelManager* mMan; + Uid mUid; + std::vector<InstanciationEntry> mEntries; + +public: + Level(); + ~Level(); + + void Instanciate(GameObject* relRoot) const; + + LevelManager* GetLinkedLevelManager() const { return mMan; } + const Uid& GetUid() const { return mUid; } + + // Editor stuff + void ShowInstanciationEntries(IEditor& editor); +}; + +class LevelManager { +public: + static inline LevelManager* instance = nullptr; + +public: // NOTE: public for the editor; actual game components should not modify the map using this + // TODO maybe cut this struct to only the first RcPtr<Level> field in release mode? + struct LoadableObject { + RcPtr<Level> level; // TODO make weak pointer + std::filesystem::path filePath; + // NOTE: these fields are only loaded in dev mode + std::string name; + std::string description; + + // Editor book keeping fields + bool edited = false; + }; + // We want pointer stability here for the editor (inspector object) + robin_hood::unordered_node_map<Uid, LoadableObject> mObjByUid; + +public: + void DiscoverFilesDesignatedLocation(); + void DiscoverFiles(const std::filesystem::path& dir); + + Level* FindLevel(const Uid& uid) const; + /// Get or load the given level + Level* LoadLevel(const Uid& uid); + /// Send the given level to be loaded on another thread + void PrepareLevel(const Uid& uid); + + /// Create and add a new level object with the given uid. + /// Should only be used by the editor. + LoadableObject& AddLevel(const Uid& uid); + /// Should only be used by the editor. + void SaveLevel(const Uid& uid) const; + /// Should only be used by the editor. + void SaveLevel(const Uid& uid, const std::filesystem::path& path) const; + +private: + void SaveLevelImpl(const LoadableObject& obj, const std::filesystem::path& path) const; +}; + +class LevelWrapperObject : public GameObject { + BRUSSEL_CLASS() + +private: + RcPtr<Level> mLevel; + +public: + LevelWrapperObject(GameWorld* world); + ~LevelWrapperObject() override; + + Level* GetBoundLevel() const; + void SetBoundLevel(Level* level); +}; diff --git a/src/brussel.engine/Material.cpp b/src/brussel.engine/Material.cpp new file mode 100644 index 0000000..4443ae5 --- /dev/null +++ b/src/brussel.engine/Material.cpp @@ -0,0 +1,526 @@ +#include "Material.hpp" + +#include "AppConfig.hpp" +#include "EditorCore.hpp" +#include "EditorUtils.hpp" + +#include <Metadata.hpp> +#include <RapidJsonHelper.hpp> +#include <ScopeGuard.hpp> +#include <Utils.hpp> + +#include <imgui.h> +#include <rapidjson/document.h> +#include <cstdlib> +#include <cstring> +#include <utility> + +using namespace std::literals; + +Material::Material() { +} + +namespace ProjectBrussel_UNITY_ID { +bool TryFindShaderId(Shader* shader, std::string_view name, int& out) { + auto& info = shader->GetInfo(); + auto iter = info.things.find(name); + if (iter == info.things.end()) return false; + auto& id = iter->second; + + if (id.kind != ShaderThingId::KD_Uniform) return false; + + out = id.index; + return true; +} + +template <typename TUniform> +TUniform& ObtainUniform(Shader* shader, const char* name, std::vector<TUniform>& uniforms, GLint location) { + for (auto& uniform : uniforms) { + if (uniform.location == location) { + return uniform; + } + } + + auto& uniform = uniforms.emplace_back(); + uniform.location = location; + if (!TryFindShaderId(shader, name, uniform.infoUniformIndex)) { + uniform.infoUniformIndex = -1; + } + + return uniform; +} + +rapidjson::Value MakeVectorJson(const Material::VectorUniform& vector, rapidjson::Document& root) { + int len = vector.actualLength; + + rapidjson::Value result(rapidjson::kArrayType); + result.Reserve(len, root.GetAllocator()); + + for (int i = 0; i < len; ++i) { + result.PushBack(vector.value[i], root.GetAllocator()); + } + + return result; +} + +Material::VectorUniform ReadVectorFromJson(const rapidjson::Value& rv) { + assert(rv.IsArray()); + Material::VectorUniform result; + int len = result.actualLength = rv.Size(); + for (int i = 0; i < len; ++i) { + result.value[i] = rv[i].GetFloat(); + } + return result; +} + +rapidjson::Value MakeMatrixJson(const Material::MatrixUniform& matrix, rapidjson::Document& root) { + int w = matrix.actualWidth; + int h = matrix.actualHeight; + + rapidjson::Value result(rapidjson::kArrayType); + result.Reserve(h, root.GetAllocator()); + + for (int y = 0; y < h; ++y) { + rapidjson::Value row(rapidjson::kArrayType); + row.Reserve(w, root.GetAllocator()); + + for (int x = 0; x < w; ++x) { + // Each item in a column is consecutive in memory in glm::mat<> structs + row.PushBack(matrix.value[x * h + y], root.GetAllocator()); + } + + result.PushBack(row, root.GetAllocator()); + } + + return result; +} + +Material::MatrixUniform ReadMatrixFromjson(const rapidjson::Value& rv) { + assert(rv.IsArray()); + assert(rv.Size() > 0); + assert(rv[0].IsArray()); + Material::MatrixUniform result; + int w = result.actualWidth = rv[0].Size(); + int h = result.actualHeight = rv.Size(); + for (int y = 0; y < h; ++y) { + auto& row = rv[y]; + assert(row.IsArray()); + assert(row.Size() == w); + for (int x = 0; x < w; ++x) { + auto& val = row[x]; + assert(val.IsNumber()); + result.value[x * h + y] = val.GetFloat(); + } + } + return result; +} +} // namespace ProjectBrussel_UNITY_ID + +void Material::SetFloat(const char* name, float value) { + assert(IsValid()); + + GLint location = glGetUniformLocation(mShader->GetProgram(), name); + auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundScalars, location); + uniform.floatValue = value; + uniform.actualType = GL_FLOAT; +} + +void Material::SetInt(const char* name, int32_t value) { + assert(IsValid()); + + GLint location = glGetUniformLocation(mShader->GetProgram(), name); + auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundScalars, location); + uniform.intValue = value; + uniform.actualType = GL_INT; +} + +void Material::SetUInt(const char* name, uint32_t value) { + assert(IsValid()); + + GLint location = glGetUniformLocation(mShader->GetProgram(), name); + auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundScalars, location); + uniform.uintValue = value; + uniform.actualType = GL_UNSIGNED_INT; +} + +template <int length> +void Material::SetVector(const char* name, const glm::vec<length, float>& vec) { + assert(IsValid()); + + static_assert(length >= 1 && length <= 4); + + GLint location = glGetUniformLocation(mShader->GetProgram(), name); + auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundVectors, location); + uniform.actualLength = length; + std::memset(uniform.value, 0, sizeof(uniform.value)); + std::memcpy(uniform.value, &vec[0], length * sizeof(float)); +} + +template void Material::SetVector<1>(const char*, const glm::vec<1, float>&); +template void Material::SetVector<2>(const char*, const glm::vec<2, float>&); +template void Material::SetVector<3>(const char*, const glm::vec<3, float>&); +template void Material::SetVector<4>(const char*, const glm::vec<4, float>&); + +template <int width, int height> +void Material::SetMatrix(const char* name, const glm::mat<width, height, float>& mat) { + static_assert(width >= 1 && width <= 4); + static_assert(height >= 1 && height <= 4); + + GLint location = glGetUniformLocation(mShader->GetProgram(), name); + auto& uniform = ProjectBrussel_UNITY_ID::ObtainUniform(mShader.Get(), name, mBoundMatrices, location); + uniform.actualWidth = width; + uniform.actualHeight = height; + std::memset(uniform.value, 0, sizeof(uniform.value)); + std::memcpy(uniform.value, &mat[0][0], width * height * sizeof(float)); +} + +template void Material::SetMatrix<2, 2>(const char*, const glm::mat<2, 2, float>&); +template void Material::SetMatrix<3, 3>(const char*, const glm::mat<3, 3, float>&); +template void Material::SetMatrix<4, 4>(const char*, const glm::mat<4, 4, float>&); + +template void Material::SetMatrix<2, 3>(const char*, const glm::mat<2, 3, float>&); +template void Material::SetMatrix<3, 2>(const char*, const glm::mat<3, 2, float>&); + +template void Material::SetMatrix<2, 4>(const char*, const glm::mat<2, 4, float>&); +template void Material::SetMatrix<4, 2>(const char*, const glm::mat<4, 2, float>&); + +template void Material::SetMatrix<3, 4>(const char*, const glm::mat<3, 4, float>&); +template void Material::SetMatrix<4, 3>(const char*, const glm::mat<4, 3, float>&); + +void Material::SetTexture(const char* name, Texture* texture) { + assert(IsValid()); + + GLint location = glGetUniformLocation(mShader->GetProgram(), name); + + for (auto& uniform : mBoundTextures) { + if (uniform.location == location) { + uniform.value.Attach(texture); + return; + } + } + + auto& uniform = mBoundTextures.emplace_back(); + uniform.value.Attach(texture); + uniform.location = location; +} + +std::span<const Material::VectorUniform> Material::GetVectors() const { + return mBoundVectors; +} + +std::span<const Material::MatrixUniform> Material::GetMatrices() const { + return mBoundMatrices; +} + +std::span<const Material::TextureUniform> Material::GetTextures() const { + return mBoundTextures; +} + +Shader* Material::GetShader() const { + return mShader.Get(); +} + +void Material::SetShader(Shader* shader) { + mShader.Attach(shader); + auto& info = shader->GetInfo(); + + mBoundScalars.clear(); + mBoundVectors.clear(); + mBoundMatrices.clear(); + mBoundTextures.clear(); + for (int i = 0; i < info.uniforms.size(); ++i) { + auto& decl = info.uniforms[i]; + switch (decl->kind) { + case ShaderVariable::KD_Math: { + auto& mathDecl = static_cast<ShaderMathVariable&>(*decl); + if (mathDecl.width == 1) { + if (mathDecl.height == 1) { + // Scalar + auto& scalar = mBoundScalars.emplace_back(); + scalar.location = decl->location; + scalar.infoUniformIndex = i; + } else { + // Vector + auto& vec = mBoundVectors.emplace_back(); + vec.location = decl->location; + vec.infoUniformIndex = i; + vec.actualLength = mathDecl.height; + } + } else { + // Matrix + auto& mat = mBoundMatrices.emplace_back(); + mat.location = decl->location; + mat.infoUniformIndex = i; + mat.actualWidth = mathDecl.width; + mat.actualHeight = mathDecl.height; + } + } break; + + case ShaderVariable::KD_Sampler: { + auto& uniform = mBoundTextures.emplace_back(); + uniform.location = decl->location; + uniform.infoUniformIndex = i; + } break; + } + } +} + +bool Material::IsValid() const { + return mShader != nullptr; +} + +static constexpr int IdentifyMatrixSize(int width, int height) { + return width * 10 + height; +} + +void Material::UseUniforms() const { + for (auto& uniform : mBoundScalars) { + switch (uniform.actualType) { + case GL_FLOAT: glUniform1f(uniform.location, uniform.intValue); break; + case GL_INT: glUniform1i(uniform.location, uniform.intValue); break; + case GL_UNSIGNED_INT: glUniform1ui(uniform.location, uniform.intValue); break; + default: break; + } + } + + for (auto& uniform : mBoundVectors) { + switch (uniform.actualLength) { + case 1: glUniform1fv(uniform.location, 1, &uniform.value[0]); break; + case 2: glUniform2fv(uniform.location, 1, &uniform.value[0]); break; + case 3: glUniform3fv(uniform.location, 1, &uniform.value[0]); break; + case 4: glUniform4fv(uniform.location, 1, &uniform.value[0]); break; + default: break; + } + } + + for (auto& uniform : mBoundMatrices) { + switch (IdentifyMatrixSize(uniform.actualWidth, uniform.actualHeight)) { + case IdentifyMatrixSize(2, 2): glUniformMatrix2fv(uniform.location, 1, GL_FALSE, uniform.value); break; + case IdentifyMatrixSize(3, 3): glUniformMatrix3fv(uniform.location, 1, GL_FALSE, uniform.value); break; + case IdentifyMatrixSize(4, 4): glUniformMatrix4fv(uniform.location, 1, GL_FALSE, uniform.value); break; + + case IdentifyMatrixSize(2, 3): glUniformMatrix2x3fv(uniform.location, 1, GL_FALSE, uniform.value); break; + case IdentifyMatrixSize(3, 2): glUniformMatrix3x2fv(uniform.location, 1, GL_FALSE, uniform.value); break; + + case IdentifyMatrixSize(2, 4): glUniformMatrix2x4fv(uniform.location, 1, GL_FALSE, uniform.value); break; + case IdentifyMatrixSize(4, 2): glUniformMatrix4x2fv(uniform.location, 1, GL_FALSE, uniform.value); break; + + case IdentifyMatrixSize(3, 4): glUniformMatrix3x4fv(uniform.location, 1, GL_FALSE, uniform.value); break; + case IdentifyMatrixSize(4, 3): glUniformMatrix4x3fv(uniform.location, 1, GL_FALSE, uniform.value); break; + + default: break; + } + } + + int i = 0; + for (auto& uniform : mBoundTextures) { + glActiveTexture(GL_TEXTURE0 + i); + glBindTexture(GL_TEXTURE_2D, uniform.value->GetHandle()); + glUniform1i(uniform.location, i); + ++i; + } +} + +IresMaterial::IresMaterial() + : IresObject(KD_Material) + , mInstance(new Material()) { + mInstance->mIres = this; +} + +Material* IresMaterial::GetInstance() const { + return mInstance.Get(); +} + +void IresMaterial::InvalidateInstance() { + if (mInstance != nullptr) { + mInstance->mIres = nullptr; + } + mInstance.Attach(new Material()); + mInstance->mIres = this; +} + +void IresMaterial::ShowEditor(IEditor& editor) { + using namespace Tags; + + IresObject::ShowEditor(editor); + + auto shader = mInstance->GetShader(); + if (shader) { + shader->GetIres()->ShowReference(editor); + } else { + IresObject::ShowReferenceNull(editor); + } + if (ImGui::BeginDragDropTarget()) { + if (auto payload = ImGui::AcceptDragDropPayload(Metadata::EnumToString(KD_Shader).data())) { + auto shader = *static_cast<IresShader* const*>(payload->Data); + mInstance->SetShader(shader->GetInstance()); + } + ImGui::EndDragDropTarget(); + } + + if (!shader) return; + auto& shaderInfo = shader->GetInfo(); + auto shaderIres = shader->GetIres(); + + for (auto& field : mInstance->mBoundScalars) { + auto& decl = static_cast<ShaderMathVariable&>(*shaderInfo.uniforms[field.infoUniformIndex]); + decl.ShowInfo(); + + ImGui::Indent(); + switch (decl.scalarType) { + case GL_FLOAT: ImGui::InputFloat("##", &field.floatValue); break; + case GL_INT: ImGui::InputInt("##", &field.intValue); break; + // TODO proper uint edit? + case GL_UNSIGNED_INT: ImGui::InputInt("##", (int32_t*)(&field.uintValue), 0, std::numeric_limits<int32_t>::max()); break; + default: ImGui::TextUnformatted("Unsupported scalar type"); break; + } + ImGui::Unindent(); + } + for (auto& field : mInstance->mBoundVectors) { + auto& decl = static_cast<ShaderMathVariable&>(*shaderInfo.uniforms[field.infoUniformIndex]); + decl.ShowInfo(); + + ImGui::Indent(); + switch (decl.semantic) { + case VES_Color1: + case VES_Color2: { + ImGui::ColorEdit4("##", field.value); + } break; + + default: { + ImGui::InputFloat4("##", field.value); + } break; + } + ImGui::Unindent(); + } + for (auto& field : mInstance->mBoundMatrices) { + auto& decl = static_cast<ShaderMathVariable&>(*shaderInfo.uniforms[field.infoUniformIndex]); + decl.ShowInfo(); + + // TODO + } + for (auto& field : mInstance->mBoundTextures) { + auto& decl = static_cast<ShaderSamplerVariable&>(*shaderInfo.uniforms[field.infoUniformIndex]); + decl.ShowInfo(); + + // TODO + } +} + +void IresMaterial::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const { + using namespace ProjectBrussel_UNITY_ID; + + IresObject::Write(ctx, value, root); + + if (!mInstance->IsValid()) { + return; + } + + auto& shaderInfo = mInstance->mShader->GetInfo(); + auto shaderUid = mInstance->mShader->GetIres()->GetUid(); + value.AddMember("Shader", shaderUid.Write(root), root.GetAllocator()); + + rapidjson::Value fields(rapidjson::kArrayType); + for (auto& scalar : mInstance->mBoundScalars) { + rapidjson::Value rvField(rapidjson::kObjectType); + rvField.AddMember("Name", shaderInfo.uniforms[scalar.infoUniformIndex]->name, root.GetAllocator()); + rvField.AddMember("Type", "Scalar", root.GetAllocator()); + switch (scalar.actualType) { + case GL_FLOAT: rvField.AddMember("Value", scalar.floatValue, root.GetAllocator()); break; + case GL_INT: rvField.AddMember("Value", scalar.intValue, root.GetAllocator()); break; + case GL_UNSIGNED_INT: rvField.AddMember("Value", scalar.uintValue, root.GetAllocator()); break; + } + fields.PushBack(rvField, root.GetAllocator()); + } + for (auto& vector : mInstance->mBoundVectors) { + rapidjson::Value rvField(rapidjson::kObjectType); + rvField.AddMember("Name", shaderInfo.uniforms[vector.infoUniformIndex]->name, root.GetAllocator()); + rvField.AddMember("Type", "Vector", root.GetAllocator()); + rvField.AddMember("Value", MakeVectorJson(vector, root).Move(), root.GetAllocator()); + fields.PushBack(rvField, root.GetAllocator()); + } + for (auto& matrix : mInstance->mBoundMatrices) { + rapidjson::Value rvField(rapidjson::kObjectType); + rvField.AddMember("Name", shaderInfo.uniforms[matrix.infoUniformIndex]->name, root.GetAllocator()); + rvField.AddMember("Type", "Matrix", root.GetAllocator()); + rvField.AddMember("Value", MakeMatrixJson(matrix, root).Move(), root.GetAllocator()); + fields.PushBack(rvField, root.GetAllocator()); + } + for (auto& texture : mInstance->mBoundTextures) { + // TODO + } + value.AddMember("Fields", fields, root.GetAllocator()); +} + +void IresMaterial::Read(IresLoadingContext& ctx, const rapidjson::Value& value) { + using namespace ProjectBrussel_UNITY_ID; + + IresObject::Read(ctx, value); + + { + auto rvShader = rapidjson::GetProperty(value, "Shader"sv); + if (!rvShader) return; + + Uid uid; + uid.Read(*rvShader); + + auto ires = ctx.FindIres(uid); + if (!ires) return; + if (ires->GetKind() != KD_Shader) return; + auto shader = static_cast<IresShader*>(ires); + + mInstance->mShader.Attach(shader->GetInstance()); + } + auto shader = mInstance->mShader.Get(); + auto& shaderInfo = shader->GetInfo(); + + auto fields = rapidjson::GetProperty(value, rapidjson::kArrayType, "Fields"sv); + if (!fields) return; + + for (auto& rvField : fields->GetArray()) { + if (!rvField.IsObject()) continue; + + auto rvName = rapidjson::GetProperty(rvField, rapidjson::kStringType, "Name"sv); + + auto rvType = rapidjson::GetProperty(rvField, rapidjson::kStringType, "Type"sv); + if (!rvType) continue; + auto type = rapidjson::AsStringView(*rvType); + + auto rvValue = rapidjson::GetProperty(rvField, "Value"sv); + + if (type == "Scalar"sv) { + Material::ScalarUniform uniform; + if (rvName) { + TryFindShaderId(shader, rapidjson::AsStringView(*rvName), uniform.infoUniformIndex); + uniform.location = shaderInfo.uniforms[uniform.infoUniformIndex]->location; + } + if (rvValue->IsFloat()) { + uniform.actualType = GL_FLOAT; + uniform.floatValue = rvValue->GetFloat(); + } else if (rvValue->IsInt()) { + uniform.actualType = GL_INT; + uniform.intValue = rvValue->GetInt(); + } else if (rvValue->IsUint()) { + uniform.actualType = GL_UNSIGNED_INT; + uniform.uintValue = rvValue->GetUint(); + } + mInstance->mBoundScalars.push_back(std::move(uniform)); + } else if (type == "Vector"sv) { + auto uniform = ReadVectorFromJson(*rvValue); + if (rvName) { + TryFindShaderId(shader, rapidjson::AsStringView(*rvName), uniform.infoUniformIndex); + uniform.location = shaderInfo.uniforms[uniform.infoUniformIndex]->location; + } + mInstance->mBoundVectors.push_back(std::move(uniform)); + } else if (type == "Matrix"sv) { + auto uniform = ReadMatrixFromjson(*rvValue); + if (rvName) { + TryFindShaderId(shader, rapidjson::AsStringView(*rvName), uniform.infoUniformIndex); + uniform.location = shaderInfo.uniforms[uniform.infoUniformIndex]->location; + } + mInstance->mBoundMatrices.push_back(uniform); + } else if (type == "Texture"sv) { + // TODO + } + } +} diff --git a/src/brussel.engine/Material.hpp b/src/brussel.engine/Material.hpp new file mode 100644 index 0000000..f1cd7dd --- /dev/null +++ b/src/brussel.engine/Material.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include "Ires.hpp" +#include "RcPtr.hpp" +#include "Shader.hpp" +#include "Texture.hpp" + +#include <glad/glad.h> +#include <robin_hood.h> +#include <cstddef> +#include <cstdint> +#include <glm/glm.hpp> +#include <memory> +#include <span> +#include <string_view> +#include <vector> + +// Forward declarations +class Material; +class IresMaterial; + +class Material : public RefCounted { + friend class IresMaterial; + +public: + // NOTE: specialize between scalar vs matrix vs vector to save memory + + enum UniformType : uint16_t { + UT_Scalar, + UT_Vector, + UT_Matrix, + }; + + struct UniformIndex { + UniformType type; + uint16_t index; + }; + + struct ScalarUniform { + union { + float floatValue; + int32_t intValue; + uint32_t uintValue; + }; + GLenum actualType; + /* Transient */ int infoUniformIndex; + /* Transient */ GLint location; + }; + + struct VectorUniform { + float value[4]; + int actualLength; + /* Transient */ int infoUniformIndex; + /* Transient */ GLint location; + }; + + struct MatrixUniform { + float value[16]; + int actualWidth; + int actualHeight; + /* Transient */ int infoUniformIndex; + /* Transient */ GLint location; + }; + + struct TextureUniform { + RcPtr<Texture> value; + /* Transient */ int infoUniformIndex; + /* Transient */ GLint location; + }; + + IresMaterial* mIres = nullptr; + RcPtr<Shader> mShader; + std::vector<ScalarUniform> mBoundScalars; + std::vector<VectorUniform> mBoundVectors; + std::vector<MatrixUniform> mBoundMatrices; + std::vector<TextureUniform> mBoundTextures; + +public: + Material(); + + void SetFloat(const char* name, float value); + void SetInt(const char* name, int32_t value); + void SetUInt(const char* name, uint32_t value); + + /// Instanciated for length == 1, 2, 3, 4 + template <int length> + void SetVector(const char* name, const glm::vec<length, float>& vec); + + /// Instanciated for sizes (2,2) (3,3) (4,4) (2,3) (3,2) (2,4) (4,2) (3,4) (4,3) + template <int width, int height> + void SetMatrix(const char* name, const glm::mat<width, height, float>& mat); + + void SetTexture(const char* name, Texture* texture); + + std::span<const VectorUniform> GetVectors() const; + std::span<const MatrixUniform> GetMatrices() const; + std::span<const TextureUniform> GetTextures() const; + Shader* GetShader() const; + void SetShader(Shader* shader); + + IresMaterial* GetIres() const { return mIres; } + + bool IsValid() const; + + void UseUniforms() const; +}; + +// Initialized in main() +inline RcPtr<Material> gDefaultMaterial; + +class IresMaterial : public IresObject { +private: + RcPtr<Material> mInstance; + +public: + IresMaterial(); + + Material* GetInstance() const; + void InvalidateInstance(); + + void ShowEditor(IEditor& editor) override; + + void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override; + void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override; +}; diff --git a/src/brussel.engine/Mesh.cpp b/src/brussel.engine/Mesh.cpp new file mode 100644 index 0000000..244e2e3 --- /dev/null +++ b/src/brussel.engine/Mesh.cpp @@ -0,0 +1,54 @@ +#include "Mesh.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/src/brussel.engine/Mesh.hpp b/src/brussel.engine/Mesh.hpp new file mode 100644 index 0000000..f86fd55 --- /dev/null +++ b/src/brussel.engine/Mesh.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "Color.hpp" +#include "VertexIndex.hpp" +#include "PodVector.hpp" +#include "RcPtr.hpp" + +#include <cstddef> +#include <cstdint> +#include <glm/glm.hpp> +#include <memory> + +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/src/brussel.engine/Player.cpp b/src/brussel.engine/Player.cpp new file mode 100644 index 0000000..34c4549 --- /dev/null +++ b/src/brussel.engine/Player.cpp @@ -0,0 +1,139 @@ +#include "Player.hpp" + +#include "AppConfig.hpp" +#include "CommonVertexIndex.hpp" +#include "ScopeGuard.hpp" +#include "Utils.hpp" + +#include <cstdio> +#include <cstdlib> + +// Keep the same number as # of fields in `struct {}` in PlayerKeyBinds +constexpr int kPlayerKeyBindCount = 4; + +// Here be dragons: this treats consecutive fiels as an array, technically UB +std::span<int> PlayerKeyBinds::GetKeyArray() { + return { &keyLeft, kPlayerKeyBindCount }; +} +std::span<bool> PlayerKeyBinds::GetKeyStatusArray() { + return { &pressedLeft, kPlayerKeyBindCount }; +} + +Player::Player(GameWorld* world, int id) + : GameObject(KD_Player, world) + , mId{ id } { + renderObject.SetMaterial(gDefaultMaterial.Get()); + renderObject.SetFormat(gVformatStandard.Get(), Tags::IT_16Bit); + renderObject.RebuildIfNecessary(); +} + +void Player::Awaken() { + LoadFromFile(); +} + +void Player::Resleep() { + SaveToFile(); +} + +void Player::Update() { + using namespace Tags; + + if (keybinds.pressedLeft) { + } + if (keybinds.pressedRight) { + } + + // 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) { + bool pressed; + if (action == GLFW_PRESS) { + pressed = true; + } else if (action == GLFW_REPEAT) { + return; + } else /* action == GLFW_RELEASE */ { + pressed = false; + } + + for (int i = 0; i < kPlayerKeyBindCount; ++i) { + int kbKey = keybinds.GetKeyArray()[i]; + bool& kbStatus = keybinds.GetKeyStatusArray()[i]; + + if (kbKey == key) { + kbStatus = pressed; + break; + } + } +} + +#pragma push_macro("PLAYERKEYBINDS_DO_IO") +#undef PLAYERKEYBINDS_DO_IO +#define PLAYERKEYBINDS_DO_IO(function, fieldPrefix) \ + function(file, "left=%d\n", fieldPrefix keybinds.keyLeft); \ + function(file, "right=%d\n", fieldPrefix keybinds.keyRight); \ + function(file, "jump=%d\n", fieldPrefix keybinds.keyJump); \ + function(file, "attack=%d\n", fieldPrefix keybinds.keyAttack); + +static FILE* OpenPlayerConfigFile(Player* player, Utils::IoMode mode) { + char path[2048]; + snprintf(path, sizeof(path), "%s/player%d.txt", AppConfig::dataDir.c_str(), player->GetId()); + + return Utils::OpenCstdioFile(path, mode); +} + +bool Player::LoadFromFile() { + auto file = OpenPlayerConfigFile(this, Utils::Read); + if (!file) return false; + DEFER { fclose(file); }; + + // TODO input validation + PLAYERKEYBINDS_DO_IO(fscanf, &); + + return true; +} + +bool Player::SaveToFile() { + auto file = OpenPlayerConfigFile(this, Utils::WriteTruncate); + if (!file) return false; + DEFER { fclose(file); }; + + PLAYERKEYBINDS_DO_IO(fprintf, ); + + return true; +} +#pragma pop_macro("PLAYERKEYBINDS_DO_IO") diff --git a/src/brussel.engine/Player.hpp b/src/brussel.engine/Player.hpp new file mode 100644 index 0000000..5a6bab7 --- /dev/null +++ b/src/brussel.engine/Player.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "GameObject.hpp" +#include "Material.hpp" +#include "Sprite.hpp" + +#include <MacrosCodegen.hpp> +#include <RcPtr.hpp> + +#define GLFW_INCLUDE_NONE +#include <GLFW/glfw3.h> + +#include <span> +#include <vector> + +struct PlayerKeyBinds { + int keyLeft = GLFW_KEY_A; + int keyRight = GLFW_KEY_D; + int keyJump = GLFW_KEY_SPACE; + int keyAttack = GLFW_KEY_J; + + bool pressedLeft = 0; + bool pressedRight = 0; + bool pressedJump = 0; + bool pressedAttack = 0; + + std::span<int> GetKeyArray(); + std::span<bool> GetKeyStatusArray(); +}; + +class Player : public GameObject { + BRUSSEL_CLASS() + +public: + std::vector<GLFWkeyboard*> boundKeyboards; + PlayerKeyBinds keybinds; + Sprite sprite; + RenderObject renderObject; + int mId; + +public: + Player(GameWorld* world, int id); + + 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); + + // File is designated by player ID + bool LoadFromFile(); + bool SaveToFile(); +}; diff --git a/src/brussel.engine/Renderer.cpp b/src/brussel.engine/Renderer.cpp new file mode 100644 index 0000000..0454efe --- /dev/null +++ b/src/brussel.engine/Renderer.cpp @@ -0,0 +1,256 @@ +#include "Renderer.hpp" + +#include "GameObject.hpp" + +#include <RapidJsonHelper.hpp> + +#include <rapidjson/document.h> +#include <cassert> +#include <glm/gtc/matrix_transform.hpp> +#include <glm/gtc/quaternion.hpp> +#include <glm/gtx/quaternion.hpp> +#include <string_view> + +using namespace std::literals; + +RenderObject::RenderObject() + : mVao{ 0 } { +} + +RenderObject::~RenderObject() { + DeleteGLObjects(); +} + +GLuint RenderObject::GetGLVao() const { + return mVao; +} + +void RenderObject::RebuildIfNecessary() { + if (mVao != 0) { + return; + } + + assert(mIndexBuf != nullptr); + assert(mVertexFormat != nullptr); + + glGenVertexArrays(1, &mVao); + glBindVertexArray(mVao); + + auto& vBindings = mVertexBufBinding.bindings; + auto& shaderInfo = mMaterial->GetShader()->GetInfo(); + + // Setup vertex buffers + for (auto& elm : mVertexFormat->elements) { + assert(elm.bindingIndex < vBindings.size()); + auto& buffer = vBindings[elm.bindingIndex]; + + int index = shaderInfo.FindInputLocation(elm.semantic); + if (index == -1) { + continue; + } + + glBindBuffer(GL_ARRAY_BUFFER, buffer->handle); + glEnableVertexAttribArray(index); + glVertexAttribPointer( + index, + Tags::VectorLenOf(elm.type), + Tags::FindGLType(elm.type), + Tags::IsNormalized(elm.type), + mVertexFormat->vertexSize, + (void*)(uintptr_t)elm.offset); + } + + // Setup index buffer + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuf->handle); + + glBindVertexArray(0); +} + +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 != 0) { + glDeleteVertexArrays(1, &mVao); + mVao = 0; + } +} + +Renderer::Renderer() + : binding_WireframeMaterial{ gDefaultMaterial } // +{ + mRenderOptions[RO_Shading] = true; + mRenderOptions[RO_Wireframe] = false; +} + +void Renderer::LoadBindings(const rapidjson::Value& bindings) { + if (auto rvWireframe = rapidjson::GetProperty(bindings, "WireframeMaterial"sv)) { + Uid uidWireframe; + uidWireframe.Read(*rvWireframe); + // TODO don't assume + binding_WireframeMaterial.Attach(((IresMaterial*)IresManager::instance->FindIres(uidWireframe))->GetInstance()); + } +} + +void Renderer::SaveBindings(rapidjson::Value& into, rapidjson::Document& root) const { + if (auto ires = binding_WireframeMaterial->GetIres()) { + into.AddMember("WireframeMaterial", ires->GetUid().Write(root), root.GetAllocator()); + } +} + +void Renderer::BeginFrame(Camera& camera, float currentTime, float deltaTime) { + assert(mInsideFrame == false); + mInsideFrame = true; + mFrame.camera = &camera; + mFrame.matrixView = camera.CalcViewMatrix(); + mFrame.matrixProj = camera.CalcProjectionMatrix(); + mFrame.time = currentTime; + mFrame.deltaTime = deltaTime; +} + +void Renderer::EndFrame() { + assert(mInsideFrame == true); + mInsideFrame = false; +} + +void Renderer::Draw(const RenderObject* objects, const GameObject* gameObject, size_t count) { + using namespace Tags; + + assert(mInsideFrame); + + // Desired order: proj * view * (translate * rotate * scale) * vec + // <----- order of application <----- ^^^ input + glm::mat4 objectMatrix(1.0f); + objectMatrix = glm::translate(objectMatrix, gameObject->GetPos()); + objectMatrix *= glm::toMat4(gameObject->GetRotation()); + objectMatrix = glm::scale(objectMatrix, gameObject->GetScale()); + auto mvpMatrix = mFrame.matrixProj * mFrame.matrixView * objectMatrix; + + if (GetRenderOption(RO_Shading)) { + // 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(); + + glUseProgram(shader->GetProgram()); + + // Material uniforms + mat->UseUniforms(); + + // Next available texture unit ID after all material textures + int texIdx = mat->GetTextures().size(); + + // Autofill uniforms + if (shader->autofill_Transform != kInvalidLocation) { + glUniformMatrix4fv(shader->autofill_Transform, 1, GL_FALSE, &mvpMatrix[0][0]); + } + if (shader->autofill_Time != kInvalidLocation) { + glUniform1f(shader->autofill_Time, mFrame.time); + } + if (shader->autofill_DeltaTime != kInvalidLocation) { + glUniform1f(shader->autofill_DeltaTime, mFrame.deltaTime); + } + if (shader->autofill_TextureAtlas != kInvalidLocation && + object.autofill_TextureAtlas != nullptr) + { + glActiveTexture(GL_TEXTURE0 + texIdx); + glBindTexture(GL_TEXTURE_2D, object.autofill_TextureAtlas->GetHandle()); + glUniform1i(shader->autofill_TextureAtlas, texIdx); + ++texIdx; + } + + glBindVertexArray(object.GetGLVao()); + glDrawElements(GL_TRIANGLES, indexBuffer->count, indexBuffer->GetIndexTypeGL(), 0); + } + } + + if (GetRenderOption(RO_Wireframe)) { + auto& mat = *binding_WireframeMaterial; + auto& shader = *mat.GetShader(); + auto& shaderInfo = shader.GetInfo(); + + glUseProgram(shader.GetProgram()); + mat.UseUniforms(); + + // TODO reduce calls with consecutive wireframe setting + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + for (size_t i = 0; i < count; ++i) { + auto& object = objects[i]; + + auto& vBindings = object.GetVertexBufferBindings().bindings; + auto vf = object.GetVertexFormat(); + + // Setup vertex buffers + for (auto& elm : vf->elements) { + assert(elm.bindingIndex < vBindings.size()); + auto& buffer = vBindings[elm.bindingIndex]; + + int index = shaderInfo.FindInputLocation(elm.semantic); + if (index == -1) { + continue; + } + + glBindBuffer(GL_ARRAY_BUFFER, buffer->handle); + glEnableVertexAttribArray(index); + glVertexAttribPointer( + index, + Tags::VectorLenOf(elm.type), + Tags::FindGLType(elm.type), + Tags::IsNormalized(elm.type), + vf->vertexSize, + (void*)(uintptr_t)elm.offset); + } + + // Setup index buffer + auto indexBuffer = object.GetIndexBuffer(); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer->handle); + + glDrawElements(GL_TRIANGLES, indexBuffer->count, indexBuffer->GetIndexTypeGL(), 0); + } + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + return; + } +} + +bool Renderer::GetRenderOption(RenderOption option) const { + return mRenderOptions[option]; +} + +void Renderer::SetRenderOption(RenderOption option, bool flag) { + mRenderOptions[option] = flag; +} diff --git a/src/brussel.engine/Renderer.hpp b/src/brussel.engine/Renderer.hpp new file mode 100644 index 0000000..856dc31 --- /dev/null +++ b/src/brussel.engine/Renderer.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include "Camera.hpp" +#include "Material.hpp" +#include "VertexIndex.hpp" + +#include <RcPtr.hpp> + +#include <glad/glad.h> +#include <rapidjson/fwd.h> +#include <cstddef> +#include <glm/glm.hpp> + +// TODO add optional support for OpenGL separate attrib binding & only depend on vertex format + +class GameObject; + +class RenderObject { +public: + RcPtr<Texture> autofill_TextureAtlas; + +private: + RcPtr<Material> mMaterial; + RcPtr<GpuIndexBuffer> mIndexBuf; + RcPtr<VertexFormat> mVertexFormat; + BufferBindings mVertexBufBinding; + GLuint mVao; + +public: + RenderObject(); + ~RenderObject(); + + GLuint GetGLVao() const; + void RebuildIfNecessary(); + + Material* GetMaterial() const { return mMaterial.Get(); } + void SetMaterial(Material* material); + + 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); + +private: + void DeleteGLObjects(); +}; + +struct RendererFrameInfo { + Camera* camera; + glm::mat4 matrixView; + glm::mat4 matrixProj; + float time; + float deltaTime; +}; + +class Renderer { +public: + // NOTE: see Renderer constructor for default values + enum RenderOption { + /// Render everything directly using objects' provided material and vertex/index data. + RO_Shading, + /// Render everything as wireframes using provided position data. + RO_Wireframe, + RO_COUNT, + }; + +public: + RcPtr<Material> binding_WireframeMaterial; + +private: + RendererFrameInfo mFrame; + bool mInsideFrame = false; + bool mRenderOptions[RO_COUNT] = {}; + +public: + Renderer(); + + void LoadBindings(const rapidjson::Value& bindings); + void SaveBindings(rapidjson::Value& into, rapidjson::Document& root) const; + + void BeginFrame(Camera& camera, float currentTime, float deltaTime); + const RendererFrameInfo& GetLastFrameInfo() const { return mFrame; } + void Draw(const RenderObject* objects, const GameObject* gameObject, size_t count); + void EndFrame(); + + bool GetRenderOption(RenderOption option) const; + void SetRenderOption(RenderOption option, bool flag); +}; diff --git a/src/brussel.engine/SceneThings.cpp b/src/brussel.engine/SceneThings.cpp new file mode 100644 index 0000000..3fa0436 --- /dev/null +++ b/src/brussel.engine/SceneThings.cpp @@ -0,0 +1,142 @@ +#include "SceneThings.hpp" + +#include "CommonVertexIndex.hpp" +#include "Rect.hpp" + +#include <utility> + +SimpleGeometryObject::SimpleGeometryObject(GameWorld* world) + : GameObject(KD_SimpleGeometry, world) + , mRenderObject() + , mSize{ 1.0f, 1.0f, 1.0f } + , mXFaceColor(kXAxisColor) + , mYFaceColor(kYAxisColor) + , mZFaceColor(kZAxisColor) + , mNeedsRebuildMesh{ true } { + mRenderObject.SetMaterial(gDefaultMaterial.Get()); + mRenderObject.SetFormat(gVformatStandard.Get(), Tags::IT_16Bit); + mRenderObject.RebuildIfNecessary(); +} + +void SimpleGeometryObject::SetSize(glm::vec3 size) { + mSize = size; + mNeedsRebuildMesh = true; +} + +void SimpleGeometryObject::SetXFaceColor(RgbaColor color) { + mXFaceColor = color; + mNeedsRebuildMesh = true; +} + +void SimpleGeometryObject::SetYFaceColor(RgbaColor color) { + mYFaceColor = color; + mNeedsRebuildMesh = true; +} + +void SimpleGeometryObject::SetZFaceColor(RgbaColor color) { + mZFaceColor = color; + mNeedsRebuildMesh = true; +} + +std::span<const RenderObject> SimpleGeometryObject::GetRenderObjects() const { + using namespace Tags; + + if (mNeedsRebuildMesh) { + mNeedsRebuildMesh = false; + + Vertex_PTC vertices[4 /*vertices per face*/ * 6 /*faces*/]; + uint16_t indices[3 /*indices per triangle*/ * 2 /*triangles per face*/ * 6 /*faces*/]; + + auto extents = mSize / 2.0f; + + int faceGenVerticesIdx = 0; + int faceGenIndicesIdx = 0; + auto GenerateFace = [&](glm::vec3 faceCenter, glm::vec3 firstExtentVec, glm::vec3 secondExtentVec, RgbaColor color) { + // Generates (if viewing top down on the face): bottom left, top left, bottom right, top right + // (-1, -1) , (-1, 1) , (1, -1) , (1, 1) + // idx=0 , idx=1 , idx=2 , idx=3 + + // These are index offsets, see above comment + constexpr int kBottomLeft = 0; + constexpr int kTopLeft = 1; + constexpr int kBottomRight = 2; + constexpr int kTopRight = 3; + + int startVertIdx = faceGenVerticesIdx; + for (float firstDir : { -1, 1 }) { + for (float secondDir : { -1, 1 }) { + auto vertPos = faceCenter + firstExtentVec * firstDir + secondExtentVec * secondDir; + auto& vert = vertices[faceGenVerticesIdx]; + vert.x = vertPos.x; + vert.y = vertPos.y; + vert.z = vertPos.z; + vert.r = color.r; + vert.g = color.g; + vert.b = color.b; + vert.a = color.a; + faceGenVerticesIdx += 1; + } + } + + // Triangle #1 + indices[faceGenIndicesIdx++] = startVertIdx + kTopRight; + indices[faceGenIndicesIdx++] = startVertIdx + kTopLeft; + indices[faceGenIndicesIdx++] = startVertIdx + kBottomLeft; + // Triangle #2 + indices[faceGenIndicesIdx++] = startVertIdx + kTopRight; + indices[faceGenIndicesIdx++] = startVertIdx + kBottomLeft; + indices[faceGenIndicesIdx++] = startVertIdx + kBottomRight; + }; + for (int xDir : { -1, 1 }) { + float x = xDir * extents.x; + GenerateFace(glm::vec3(x, 0, 0), glm::vec3(0, 0, extents.z), glm::vec3(0, extents.y, 0), mXFaceColor); + } + for (int yDir : { -1, 1 }) { + float y = yDir * extents.y; + GenerateFace(glm::vec3(0, y, 0), glm::vec3(extents.x, 0, 0), glm::vec3(0, 0, extents.z), mYFaceColor); + } + for (int zDir : { -1, 1 }) { + float z = zDir * extents.z; + GenerateFace(glm::vec3(0, 0, z), glm::vec3(extents.x, 0, 0), glm::vec3(0, extents.y, 0), mZFaceColor); + } + + for (auto& vert : vertices) { + vert.u = 0.0f; + vert.v = 0.0f; + } + + mRenderObject.GetVertexBufferBindings().bindings[0]->Upload((const std::byte*)vertices, sizeof(vertices)); + mRenderObject.GetIndexBuffer()->Upload((const std::byte*)indices, IT_16Bit, std::size(indices)); + } + + return { &mRenderObject, 1 }; +} + +BuildingObject::BuildingObject(GameWorld* world) + : GameObject(KD_Building, world) { + mRenderObject.SetMaterial(gDefaultMaterial.Get()); + mRenderObject.SetFormat(gVformatStandard.Get(), Tags::IT_32Bit); + mRenderObject.RebuildIfNecessary(); +} + +// 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/src/brussel.engine/SceneThings.hpp b/src/brussel.engine/SceneThings.hpp new file mode 100644 index 0000000..761eb59 --- /dev/null +++ b/src/brussel.engine/SceneThings.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "Color.hpp" +#include "GameObject.hpp" +#include "Renderer.hpp" + +#include <MacrosCodegen.hpp> + +#include <glm/glm.hpp> +#include <vector> + +class SimpleGeometryObject : public GameObject { + BRUSSEL_CLASS() + +private: + RenderObject mRenderObject; + glm::vec3 mSize; + RgbaColor mXFaceColor; + RgbaColor mYFaceColor; + RgbaColor mZFaceColor; + mutable bool mNeedsRebuildMesh; + +public: + SimpleGeometryObject(GameWorld* world); + + glm::vec3 GetSize() const { return mSize; } + void SetSize(glm::vec3 size); + RgbaColor GetXFaceColor() const { return mXFaceColor; } + void SetXFaceColor(RgbaColor color); + RgbaColor GetYFaceColor() const { return mYFaceColor; } + void SetYFaceColor(RgbaColor color); + RgbaColor GetZFaceColor() const { return mZFaceColor; } + void SetZFaceColor(RgbaColor color); + virtual std::span<const RenderObject> GetRenderObjects() const override; +}; + +class BuildingObject : public GameObject { + BRUSSEL_CLASS() + +private: + RenderObject mRenderObject; + +public: + BuildingObject(GameWorld* world); + + // 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/src/brussel.engine/Shader.cpp b/src/brussel.engine/Shader.cpp new file mode 100644 index 0000000..9bf2e0e --- /dev/null +++ b/src/brussel.engine/Shader.cpp @@ -0,0 +1,711 @@ +#include "Shader.hpp" + +#include "AppConfig.hpp" + +#include <Metadata.hpp> +#include <RapidJsonHelper.hpp> +#include <ScopeGuard.hpp> +#include <Utils.hpp> + +#include <fmt/format.h> +#include <imgui.h> +#include <misc/cpp/imgui_stdlib.h> +#include <rapidjson/document.h> +#include <cassert> +#include <cstddef> +#include <cstdlib> +#include <utility> + +using namespace std::literals; + +void ShaderMathVariable::ShowInfo() const { + ImGui::BulletText("Location: %d\nName: %s\nSemantic: %.*s\nType: %.*s %dx%d", + location, + name.c_str(), + PRINTF_STRING_VIEW(Metadata::EnumToString(semantic)), + PRINTF_STRING_VIEW(Tags::GLTypeToString(scalarType)), + width, + height); +} + +void ShaderSamplerVariable::ShowInfo() const { + ImGui::BulletText("Location: %d\nName: %s\nSemantic: %.*s\nType: Sampler", + location, + name.c_str(), + PRINTF_STRING_VIEW(Metadata::EnumToString(semantic))); +} + +namespace ProjectBrussel_UNITY_ID { +GLuint FindLocation(const std::vector<ShaderMathVariable>& vars, Tags::VertexElementSemantic semantic) { + for (auto& var : vars) { + if (var.semantic == semantic) { + return var.location; + } + } + return Tags::kInvalidLocation; +} + +constexpr auto kAfnTransform = "transform"; +constexpr auto kAfnTime = "time"; +constexpr auto kAfnDeltaTime = "deltaTime"; +constexpr auto kAfnTextureAtlas = "textureAtlas"; + +void InitAutoFill(const char* name, GLuint program, GLuint& location) { + GLint result = glGetUniformLocation(program, name); + if (result != -1) { + location = result; + } +} + +void InitAutoFills(Shader& shader) { + GLuint pg = shader.GetProgram(); + InitAutoFill(kAfnTransform, pg, shader.autofill_Transform); + InitAutoFill(kAfnTime, pg, shader.autofill_Time); + InitAutoFill(kAfnDeltaTime, pg, shader.autofill_DeltaTime); + InitAutoFill(kAfnTextureAtlas, pg, shader.autofill_TextureAtlas); +} +} // namespace ProjectBrussel_UNITY_ID + +GLuint ShaderInfo::FindInputLocation(Tags::VertexElementSemantic semantic) { + using namespace ProjectBrussel_UNITY_ID; + return FindLocation(inputs, semantic); +} + +GLuint ShaderInfo::FindOutputLocation(Tags::VertexElementSemantic semantic) { + using namespace ProjectBrussel_UNITY_ID; + return FindLocation(outputs, semantic); +} + +Shader::Shader() { +} + +Shader::~Shader() { + glDeleteProgram(mProgram); +} + +namespace ProjectBrussel_UNITY_ID { +// Grabs section [begin, end) +Shader::ErrorCode CreateShader(GLuint& out, const char* src, int beginIdx, int endIdx, GLenum type) { + out = glCreateShader(type); + + const GLchar* begin = &src[beginIdx]; + const GLint len = endIdx - beginIdx; + glShaderSource(out, 1, &begin, &len); + + glCompileShader(out); + GLint compileStatus; + glGetShaderiv(out, GL_COMPILE_STATUS, &compileStatus); + if (compileStatus == GL_FALSE) { + GLint len; + glGetShaderiv(out, GL_INFO_LOG_LENGTH, &len); + + std::string log(len, '\0'); + glGetShaderInfoLog(out, len, nullptr, log.data()); + + return Shader ::EC_CompilationFailed; + } + + return Shader::EC_Success; +} + +Shader::ErrorCode CreateShader(GLuint& out, std::string_view str, GLenum type) { + return CreateShader(out, str.data(), 0, str.size(), type); +} + +Shader::ErrorCode LinkShaderProgram(GLuint program) { + glLinkProgram(program); + GLint linkStatus; + glGetProgramiv(program, GL_LINK_STATUS, &linkStatus); + if (linkStatus == GL_FALSE) { + GLint len; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len); + + std::string log(len, '\0'); + glGetProgramInfoLog(program, len, nullptr, log.data()); + + return Shader::EC_LinkingFailed; + } + + return Shader::EC_Success; +} +} // namespace ProjectBrussel_UNITY_ID + +#define CATCH_ERROR_IMPL(x, name) \ + auto name = x; \ + if (name != Shader::EC_Success) { \ + return name; \ + } +#define CATCH_ERROR(x) CATCH_ERROR_IMPL(x, UNIQUE_NAME(result)) + +Shader::ErrorCode Shader::InitFromSources(const ShaderSources& sources) { + using namespace ProjectBrussel_UNITY_ID; + + if (IsValid()) { + return EC_AlreadyInitialized; + } + + GLuint program = glCreateProgram(); + ScopeGuard sg = [&]() { glDeleteProgram(program); }; + + GLuint vertex = 0; + DEFER { + glDeleteShader(vertex); + }; + if (!sources.vertex.empty()) { + CATCH_ERROR(CreateShader(vertex, sources.vertex, GL_VERTEX_SHADER)); + glAttachShader(program, vertex); + } + + GLuint geometry = 0; + DEFER { + glDeleteShader(geometry); + }; + if (!sources.geometry.empty()) { + CATCH_ERROR(CreateShader(geometry, sources.geometry, GL_GEOMETRY_SHADER)); + glAttachShader(program, geometry); + } + + GLuint tessControl = 0; + DEFER { + glDeleteShader(tessControl); + }; + if (!sources.tessControl.empty()) { + CATCH_ERROR(CreateShader(tessControl, sources.tessControl, GL_TESS_CONTROL_SHADER)); + glAttachShader(program, tessControl); + } + + GLuint tessEval = 0; + DEFER { + glDeleteShader(tessEval); + }; + if (!sources.tessEval.empty()) { + CATCH_ERROR(CreateShader(tessEval, sources.tessEval, GL_TESS_EVALUATION_SHADER)); + glAttachShader(program, tessEval); + } + + GLuint fragment = 0; + DEFER { + glDeleteShader(fragment); + }; + if (!sources.fragment.empty()) { + CATCH_ERROR(CreateShader(fragment, sources.fragment, GL_FRAGMENT_SHADER)); + glAttachShader(program, fragment); + } + + CATCH_ERROR(LinkShaderProgram(program)); + + sg.Dismiss(); + mProgram = program; + + InitAutoFills(*this); + + return EC_Success; +} + +Shader::ErrorCode Shader::InitFromSource(std::string_view source) { + using namespace ProjectBrussel_UNITY_ID; + + GLuint vertex = 0; + DEFER { + glDeleteShader(vertex); + }; + + GLuint geometry = 0; + DEFER { + glDeleteShader(geometry); + }; + + GLuint tessControl = 0; + DEFER { + glDeleteShader(tessControl); + }; + + GLuint tessEval = 0; + DEFER { + glDeleteShader(tessEval); + }; + + GLuint fragment = 0; + DEFER { + glDeleteShader(fragment); + }; + + int prevBegin = -1; // Excluding #type marker + int prevEnd = -1; // [begin, end) + std::string prevShaderVariant; + + auto CommitSection = [&]() -> ErrorCode { + if (prevBegin == -1 || prevEnd == -1) { + // Not actually "succeeding" here, but we just want to skip this call and continue + return EC_Success; + } + + if (prevShaderVariant == "vertex" && !vertex) { + CATCH_ERROR(CreateShader(vertex, source.data(), prevBegin, prevEnd, GL_VERTEX_SHADER)); + } else if (prevShaderVariant == "geometry" && !geometry) { + CATCH_ERROR(CreateShader(geometry, source.data(), prevBegin, prevEnd, GL_GEOMETRY_SHADER)); + } else if (prevShaderVariant == "tessellation_control" && !tessControl) { + CATCH_ERROR(CreateShader(tessControl, source.data(), prevBegin, prevEnd, GL_TESS_CONTROL_SHADER)); + } else if (prevShaderVariant == "tessellation_evaluation" && !tessEval) { + CATCH_ERROR(CreateShader(tessEval, source.data(), prevBegin, prevEnd, GL_TESS_EVALUATION_SHADER)); + } else if (prevShaderVariant == "fragment" && !fragment) { + CATCH_ERROR(CreateShader(fragment, source.data(), prevBegin, prevEnd, GL_FRAGMENT_SHADER)); + } else { + return EC_InvalidShaderVariant; + } + + prevBegin = -1; + prevEnd = -1; + prevShaderVariant.clear(); + + return EC_Success; + }; + + constexpr const char* kMarker = "#type "; + bool matchingDirective = true; // If true, we are matching marker pattern; if false, we are accumulating shader variant identifier + int matchIndex = 0; // Current index of the pattern trying to match + std::string shaderVariant; + + // Don't use utf8 iterator, shader sources are expected to be ASCII only + for (size_t i = 0; i < source.size(); ++i) { + char c = source[i]; + + if (matchingDirective) { + if (c == kMarker[matchIndex]) { + // Matched the expected character, go to next char in pattern + matchIndex++; + + // If we are at the end of the marker pattern... + if (kMarker[matchIndex] == '\0') { + matchingDirective = false; + matchIndex = 0; + continue; + } + + // This might be a shader variant directive -> might be end of a section + if (c == '#') { + prevEnd = i; + continue; + } + } else { + // Unexpected character, rollback to beginning + matchIndex = 0; + } + } else { + if (c == '\n') { + // Found complete shader variant directive + + CATCH_ERROR(CommitSection()); // Try commit section, for the first apparent of #type this should do nothing, as `prevEnd` will still be -1 + prevBegin = i + 1; // +1 to skip new line (technically not needed) + prevShaderVariant = std::move(shaderVariant); + + matchingDirective = true; + shaderVariant.clear(); + } else { + // Simply accumulate to shader variant buffer + shaderVariant += c; + } + } + } + + // Commit the last section + prevEnd = static_cast<int>(source.size()); + CATCH_ERROR(CommitSection()); + + GLuint program = glCreateProgram(); + ScopeGuard sg = [&]() { glDeleteProgram(program); }; + + if (vertex) glAttachShader(program, vertex); + if (geometry) glAttachShader(program, geometry); + if (tessControl) glAttachShader(program, tessControl); + if (tessEval) glAttachShader(program, tessEval); + if (fragment) glAttachShader(program, fragment); + + CATCH_ERROR(LinkShaderProgram(program)); + + sg.Dismiss(); + mProgram = program; + + InitAutoFills(*this); + + return EC_Success; +} + +#undef CATCH_ERROR + +namespace ProjectBrussel_UNITY_ID { +bool QueryMathInfo(GLenum type, GLenum& scalarType, int& width, int& height) { + auto DoOutput = [&](GLenum scalarTypeIn, int widthIn, int heightIn) { + width = widthIn; + height = heightIn; + scalarType = scalarTypeIn; + }; + + switch (type) { + case GL_FLOAT: + case GL_DOUBLE: + case GL_INT: + case GL_UNSIGNED_INT: + case GL_BOOL: { + DoOutput(type, 1, 1); + return true; + } + + case GL_FLOAT_VEC2: DoOutput(GL_FLOAT, 1, 2); return true; + case GL_FLOAT_VEC3: DoOutput(GL_FLOAT, 1, 3); return true; + case GL_FLOAT_VEC4: DoOutput(GL_FLOAT, 1, 4); return true; + case GL_DOUBLE_VEC2: DoOutput(GL_DOUBLE, 1, 2); return true; + case GL_DOUBLE_VEC3: DoOutput(GL_DOUBLE, 1, 3); return true; + case GL_DOUBLE_VEC4: DoOutput(GL_DOUBLE, 1, 4); return true; + case GL_INT_VEC2: DoOutput(GL_INT, 1, 2); return true; + case GL_INT_VEC3: DoOutput(GL_INT, 1, 3); return true; + case GL_INT_VEC4: DoOutput(GL_INT, 1, 4); return true; + case GL_UNSIGNED_INT_VEC2: DoOutput(GL_UNSIGNED_INT, 1, 2); return true; + case GL_UNSIGNED_INT_VEC3: DoOutput(GL_UNSIGNED_INT, 1, 3); return true; + case GL_UNSIGNED_INT_VEC4: DoOutput(GL_UNSIGNED_INT, 1, 4); return true; + case GL_BOOL_VEC2: DoOutput(GL_BOOL, 1, 2); return true; + case GL_BOOL_VEC3: DoOutput(GL_BOOL, 1, 3); return true; + case GL_BOOL_VEC4: DoOutput(GL_BOOL, 1, 4); return true; + + case GL_FLOAT_MAT2: DoOutput(GL_FLOAT, 2, 2); return true; + case GL_FLOAT_MAT3: DoOutput(GL_FLOAT, 3, 3); return true; + case GL_FLOAT_MAT4: DoOutput(GL_FLOAT, 4, 4); return true; + case GL_FLOAT_MAT2x3: DoOutput(GL_FLOAT, 2, 3); return true; + case GL_FLOAT_MAT2x4: DoOutput(GL_FLOAT, 2, 4); return true; + case GL_FLOAT_MAT3x2: DoOutput(GL_FLOAT, 3, 2); return true; + case GL_FLOAT_MAT3x4: DoOutput(GL_FLOAT, 3, 4); return true; + case GL_FLOAT_MAT4x2: DoOutput(GL_FLOAT, 4, 2); return true; + case GL_FLOAT_MAT4x3: DoOutput(GL_FLOAT, 4, 3); return true; + + case GL_DOUBLE_MAT2: DoOutput(GL_DOUBLE, 2, 2); return true; + case GL_DOUBLE_MAT3: DoOutput(GL_DOUBLE, 3, 3); return true; + case GL_DOUBLE_MAT4: DoOutput(GL_DOUBLE, 4, 4); return true; + case GL_DOUBLE_MAT2x3: DoOutput(GL_DOUBLE, 2, 3); return true; + case GL_DOUBLE_MAT2x4: DoOutput(GL_DOUBLE, 2, 4); return true; + case GL_DOUBLE_MAT3x2: DoOutput(GL_DOUBLE, 3, 2); return true; + case GL_DOUBLE_MAT3x4: DoOutput(GL_DOUBLE, 3, 4); return true; + case GL_DOUBLE_MAT4x2: DoOutput(GL_DOUBLE, 4, 2); return true; + case GL_DOUBLE_MAT4x3: DoOutput(GL_DOUBLE, 4, 3); return true; + + default: break; + } + + return false; +} + +bool QuerySamplerInfo(GLenum type) { + switch (type) { + case GL_SAMPLER_1D: + case GL_SAMPLER_2D: + case GL_SAMPLER_3D: + case GL_SAMPLER_CUBE: + case GL_SAMPLER_1D_SHADOW: + case GL_SAMPLER_2D_SHADOW: + case GL_SAMPLER_1D_ARRAY: + case GL_SAMPLER_2D_ARRAY: + case GL_SAMPLER_1D_ARRAY_SHADOW: + case GL_SAMPLER_2D_ARRAY_SHADOW: + case GL_SAMPLER_2D_MULTISAMPLE: + case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: + case GL_SAMPLER_CUBE_SHADOW: + case GL_SAMPLER_BUFFER: + case GL_SAMPLER_2D_RECT: + case GL_SAMPLER_2D_RECT_SHADOW: + + case GL_INT_SAMPLER_1D: + case GL_INT_SAMPLER_2D: + case GL_INT_SAMPLER_3D: + case GL_INT_SAMPLER_CUBE: + case GL_INT_SAMPLER_1D_ARRAY: + case GL_INT_SAMPLER_2D_ARRAY: + case GL_INT_SAMPLER_2D_MULTISAMPLE: + case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: + case GL_INT_SAMPLER_BUFFER: + case GL_INT_SAMPLER_2D_RECT: + + case GL_UNSIGNED_INT_SAMPLER_1D: + case GL_UNSIGNED_INT_SAMPLER_2D: + case GL_UNSIGNED_INT_SAMPLER_3D: + case GL_UNSIGNED_INT_SAMPLER_CUBE: + case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: + case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: + case GL_UNSIGNED_INT_SAMPLER_BUFFER: + case GL_UNSIGNED_INT_SAMPLER_2D_RECT: + return true; + + default: break; + } + + return false; +} + +std::variant<ShaderMathVariable, ShaderSamplerVariable> CreateVariable(GLenum type, GLuint loc) { + GLenum scalarType; + int width; + int height; + if (QueryMathInfo(type, scalarType, width, height)) { + ShaderMathVariable res; + res.location = loc; + res.scalarType = type; + res.width = width; + res.height = height; + return res; + } + + if (QuerySamplerInfo(type)) { + ShaderSamplerVariable res; + res.location = loc; + res.samplerType = type; + return res; + } + + throw std::runtime_error(fmt::format("Unknown OpenGL shader uniform type {}", type)); +} +} // namespace ProjectBrussel_UNITY_ID + +bool Shader::GatherInfoShaderIntrospection() { + using namespace ProjectBrussel_UNITY_ID; + + mInfo = {}; + + // TODO handle differnt types of variables with the same name + + // TODO work with OpenGL < 4.3, possibly with glslang + return true; + + int inputCount; + glGetProgramInterfaceiv(mProgram, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES, &inputCount); + int outputCount; + glGetProgramInterfaceiv(mProgram, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES, &outputCount); + int uniformBlockCount; + glGetProgramInterfaceiv(mProgram, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES, &uniformBlockCount); + int uniformCount; + glGetProgramInterfaceiv(mProgram, GL_UNIFORM, GL_ACTIVE_RESOURCES, &uniformCount); + + // Gather inputs + auto GatherMathVars = [&](int count, GLenum resourceType, ShaderThingId::Kind resourceKind, std::vector<ShaderMathVariable>& list) { + for (int i = 0; i < count; ++i) { + const GLenum query[] = { GL_NAME_LENGTH, GL_TYPE, GL_LOCATION, GL_TOP_LEVEL_ARRAY_SIZE }; + GLint props[std::size(query)]; + glGetProgramResourceiv(mProgram, resourceType, i, std::size(query), query, std::size(props), nullptr, props); + auto& nameLength = props[0]; + auto& type = props[1]; + auto& loc = props[2]; + auto& arrayLength = props[3]; + + std::string fieldName(nameLength - 1, '\0'); + glGetProgramResourceName(mProgram, GL_UNIFORM, i, nameLength, nullptr, fieldName.data()); + + mInfo.things.try_emplace(fieldName, ShaderThingId{ resourceKind, i }); + + auto& thing = list.emplace_back(); + thing.name = std::move(fieldName); + thing.arrayLength = arrayLength; + QueryMathInfo(type, thing.scalarType, thing.width, thing.height); + } + }; + GatherMathVars(inputCount, GL_PROGRAM_INPUT, ShaderThingId::KD_Input, mInfo.inputs); + GatherMathVars(outputCount, GL_PROGRAM_OUTPUT, ShaderThingId::KD_Output, mInfo.outputs); + + // Gather uniform variables + for (int i = 0; i < uniformCount; ++i) { + const GLenum query[] = { GL_BLOCK_INDEX, GL_NAME_LENGTH, GL_TYPE, GL_LOCATION, GL_TOP_LEVEL_ARRAY_SIZE }; + GLint props[std::size(query)]; + glGetProgramResourceiv(mProgram, GL_UNIFORM, i, std::size(query), query, std::size(props), nullptr, props); + auto& blockIndex = props[0]; // Index in interface block + if (blockIndex != -1) { // If this is an interface block uniform, skip because it will be handled by our uniform blocks inspector + continue; + } + auto& nameLength = props[1]; + auto& type = props[2]; + auto& loc = props[3]; + auto& arrayLength = props[4]; + + std::string fieldName(nameLength - 1, '\0'); + glGetProgramResourceName(mProgram, GL_UNIFORM, i, nameLength, nullptr, fieldName.data()); + + mInfo.things.try_emplace(fieldName, ShaderThingId{ ShaderThingId::KD_Uniform, i }); + mInfo.uniforms.push_back(CreateVariable(type, loc)); + } + + return true; +} + +bool Shader::IsValid() const { + return mProgram != 0; +} + +IresShader::IresShader() + : IresObject(KD_Shader) { + InvalidateInstance(); +} + +Shader* IresShader::GetInstance() const { + return mInstance.Get(); +} + +void IresShader::InvalidateInstance() { + if (mInstance != nullptr) { + mInstance->mIres = nullptr; + } + mInstance.Attach(new Shader()); + mInstance->mIres = this; +} + +void IresShader::ShowEditor(IEditor& editor) { + using namespace Tags; + using namespace ProjectBrussel_UNITY_ID; + + IresObject::ShowEditor(editor); + + if (ImGui::Button("Gather info")) { + mInstance->GatherInfoShaderIntrospection(); + } + + if (ImGui::InputText("Source file", &mSourceFile, ImGuiInputTextFlags_EnterReturnsTrue)) { + InvalidateInstance(); + } + // In other cases, mSourceFile will be reverted to before edit + + // TODO macros + + ImGui::Separator(); + + auto& info = mInstance->GetInfo(); + if (ImGui::CollapsingHeader("General")) { + ImGui::Text("OpenGL program ID: %u", mInstance->GetProgram()); + } + if (ImGui::CollapsingHeader("Inputs")) { + for (auto& input : info.inputs) { + input.ShowInfo(); + } + } + if (ImGui::CollapsingHeader("Outputs")) { + for (auto& output : info.outputs) { + output.ShowInfo(); + } + } + if (ImGui::CollapsingHeader("Uniforms")) { + for (auto& uniform : info.uniforms) { + std::visit([](auto&& v) { v.ShowInfo(); }, uniform); + } + if (auto loc = mInstance->autofill_Transform; loc != kInvalidLocation) { + ImGui::BulletText("(Autofill)\nLocation: %d\nName: %s", loc, kAfnTransform); + } + if (auto loc = mInstance->autofill_Time; loc != kInvalidLocation) { + ImGui::BulletText("(Autofill)\nLocation: %d\nName: %s", loc, kAfnTime); + } + if (auto loc = mInstance->autofill_DeltaTime; loc != kInvalidLocation) { + ImGui::BulletText("(Autofill)\nLocation: %d\nName: %s", loc, kAfnDeltaTime); + } + if (auto loc = mInstance->autofill_TextureAtlas; loc != kInvalidLocation) { + ImGui::BulletText("(Autofill)\nLocation: %d\nName: %s", loc, kAfnTextureAtlas); + } + } +} + +void IresShader::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const { + using namespace ProjectBrussel_UNITY_ID; + + IresObject::Write(ctx, value, root); + json_dto::json_output_t out( value, root.GetAllocator() ); + out << mInstance->mInfo; +} + +void IresShader::Read(IresLoadingContext& ctx, const rapidjson::Value& value) { + using namespace ProjectBrussel_UNITY_ID; + + IresObject::Read(ctx, value); + + auto rvSourceFile = rapidjson::GetProperty(value, rapidjson::kStringType, "SourceFile"sv); + if (!rvSourceFile) { + return; + } else { + this->mSourceFile = rapidjson::AsString(*rvSourceFile); + + char shaderFilePath[256]; + snprintf(shaderFilePath, sizeof(shaderFilePath), "%s/%s", AppConfig::assetDir.c_str(), rvSourceFile->GetString()); + + auto shaderFile = Utils::OpenCstdioFile(shaderFilePath, Utils::Read); + if (!shaderFile) return; + DEFER { + fclose(shaderFile); + }; + + fseek(shaderFile, 0, SEEK_END); + auto shaderFileSize = ftell(shaderFile); + rewind(shaderFile); + + // Also add \0 ourselves + auto buffer = std::make_unique<char[]>(shaderFileSize + 1); + fread(buffer.get(), shaderFileSize, 1, shaderFile); + buffer[shaderFileSize] = '\0'; + std::string_view source(buffer.get(), shaderFileSize); + + if (mInstance->InitFromSource(source) != Shader::EC_Success) { + return; + } + } + + auto& shaderInfo = mInstance->mInfo; + auto shaderProgram = mInstance->GetProgram(); + + auto LoadMathVars = [&](std::string_view name, ShaderThingId::Kind kind, std::vector<ShaderMathVariable>& vars) { + auto rvThings = rapidjson::GetProperty(value, rapidjson::kArrayType, name); + if (!rvThings) return; + + for (auto& rv : rvThings->GetArray()) { + if (!rv.IsObject()) continue; + ShaderMathVariable thing; + ReadShaderMathVariable(rv, thing); + + shaderInfo.things.try_emplace(thing.name, ShaderThingId{ kind, (int)vars.size() }); + vars.push_back(std::move(thing)); + } + }; + LoadMathVars("Inputs"sv, ShaderThingId::KD_Input, shaderInfo.inputs); + LoadMathVars("Outputs"sv, ShaderThingId::KD_Output, shaderInfo.outputs); + + auto rvUniforms = rapidjson::GetProperty(value, rapidjson::kArrayType, "Uniforms"sv); + if (!rvUniforms) return; + for (auto& rvUniform : rvUniforms->GetArray()) { + if (!rvUniform.IsObject()) continue; + + auto rvType = rapidjson::GetProperty(rvUniform, rapidjson::kStringType, "Type"sv); + if (!rvType) continue; + auto type = rapidjson::AsStringView(*rvType); + + auto rvValue = rapidjson::GetProperty(rvUniform, rapidjson::kObjectType, "Value"sv); + if (!rvValue) continue; + + bool autoFill; // TODO store autofill uniforms somewhere else + BRUSSEL_JSON_GET_DEFAULT(rvUniform, "AutoFill", bool, autoFill, false); + if (autoFill) continue; + + auto uniform = [&]() -> std::unique_ptr<ShaderVariable> { + if (type == "Math"sv) { + auto uniform = std::make_unique<ShaderMathVariable>(); + ReadShaderMathVariable(*rvValue, *uniform); + + return uniform; + } else if (type == "Sampler"sv) { + auto uniform = std::make_unique<ShaderSamplerVariable>(); + ReadShaderSamplerVariable(*rvValue, *uniform); + + return uniform; + } + + return nullptr; + }(); + if (uniform) { + shaderInfo.things.try_emplace(uniform->name, ShaderThingId{ ShaderThingId::KD_Uniform, (int)shaderInfo.uniforms.size() }); + shaderInfo.uniforms.emplace_back(std::move(uniform)); + } + } + + for (auto& uniform : shaderInfo.uniforms) { + uniform->location = glGetUniformLocation(shaderProgram, uniform->name.c_str()); + } +} diff --git a/src/brussel.engine/Shader.hpp b/src/brussel.engine/Shader.hpp new file mode 100644 index 0000000..cb980cd --- /dev/null +++ b/src/brussel.engine/Shader.hpp @@ -0,0 +1,180 @@ +#pragma once + +#include "GraphicsTags.hpp" +#include "Ires.hpp" +#include "RcPtr.hpp" +#include "Utils.hpp" + +#include <glad/glad.h> +#include <robin_hood.h> +#include <json_dto/pub.hpp> +#include <memory> +#include <string_view> +#include <variant> +#include <vector> + +// TODO move to variable after pattern matching is in the language + +// Forward declarations +class Shader; +class IresShader; + +struct ShaderMathVariable { + std::string name; + GLuint location; + Tags::VertexElementSemantic semantic = Tags::VES_Generic; + GLenum scalarType; + int width; + int height; + int arrayLength = 1; + + void ShowInfo() const; + + template <typename TJsonIo> + void json_io(TJsonIo& io) { + io& json_dto::mandatory("Name", name); + io& json_dto::mandatory("Semantic", static_cast<int>(semantic)); + io& json_dto::mandatory("ScalarType", scalarType); + io& json_dto::mandatory("Width", width); + io& json_dto::mandatory("Height", height); + io& json_dto::optional("ArrayLength", arrayLength, 1); + } +}; + +struct ShaderSamplerVariable { + std::string name; + GLuint location; + Tags::VertexElementSemantic semantic = Tags::VES_Generic; + GLenum samplerType; + int arrayLength = 1; + + void ShowInfo() const; + + template <typename TJsonIo> + void json_io(TJsonIo& io) { + io& json_dto::mandatory("Name", name); + io& json_dto::mandatory("Semantic", static_cast<int>(semantic)); + io& json_dto::mandatory("SamplerType", samplerType); + io& json_dto::optional("ArrayLength", arrayLength, 1); + } +}; + +struct ShaderThingId { + enum Kind { + KD_Input, + KD_Output, + KD_Uniform, + }; + + Kind kind; + int index; +}; + +struct ShaderInfo { + robin_hood::unordered_map<std::string, ShaderThingId, StringHash, StringEqual> things; + std::vector<ShaderMathVariable> inputs; + std::vector<ShaderMathVariable> outputs; + std::vector<std::variant<ShaderMathVariable, ShaderSamplerVariable>> uniforms; + + // Find the first variable with the matching semantic + GLuint FindInputLocation(Tags::VertexElementSemantic semantic); + GLuint FindOutputLocation(Tags::VertexElementSemantic semantic); + + template <typename TJsonIo> + void json_io(TJsonIo& io) { + io& json_dto::mandatory("Inputs", inputs); + io& json_dto::mandatory("Outputs", outputs); + // TODO make json_dto support std::variant +// io& json_dto::mandatory("Uniforms", uniforms); + } +}; + +class Shader : public RefCounted { + friend class IresShader; + +private: + IresShader* mIres = nullptr; + ShaderInfo mInfo; + GLuint mProgram = 0; + +public: + GLuint autofill_Transform = Tags::kInvalidLocation; + GLuint autofill_Time = Tags::kInvalidLocation; + GLuint autofill_DeltaTime = Tags::kInvalidLocation; + GLuint autofill_TextureAtlas = Tags::kInvalidLocation; + +public: + Shader(); + ~Shader(); + Shader(const Shader&) = delete; + Shader& operator=(const Shader&) = delete; + Shader(Shader&&) = default; + Shader& operator=(Shader&&) = default; + + enum ErrorCode { + EC_Success, + /// Generated when Init*() functions are called on an already initialized Shader object. + EC_AlreadyInitialized, + /// Generated when the one-source-file text contains invalid or duplicate shader variants. + EC_InvalidShaderVariant, + EC_FileIoFailed, + EC_CompilationFailed, + EC_LinkingFailed, + }; + + struct ShaderSources { + std::string_view vertex; + std::string_view geometry; + std::string_view tessControl; + std::string_view tessEval; + std::string_view fragment; + }; + + /// Create shader by compiling each shader source file separately and then combining them together + /// into a Shader object. + ErrorCode InitFromSources(const ShaderSources& sources); + + /// The given text will be split into different shader sections according to #type directives, + /// and combined to form a Shader object. + /// For OpenGL, this process involves compililng each section separately and then linking them + /// together. + /// + /// There are a directive for each shader type + /// - `#type vertex`: Vertex shader + /// - `#type geometry`: Geometry shader + /// - `#type tessellation_control`: Tessellation control shader + /// - `#type tessellation_evaluation`: Tessellation evaluation shader + /// - `#type fragment`: Fragment shader + ErrorCode InitFromSource(std::string_view source); + + /// Rebuild info object using OpenGL shader introspection API. Requires OpenGL 4.3 or above. Overrides existing info object. + bool GatherInfoShaderIntrospection(); + const ShaderInfo& GetInfo() const { return mInfo; } + ShaderInfo& GetInfo() { return mInfo; } + /// If not empty, this name must not duplicate with any other shader object in the process. + GLuint GetProgram() const { return mProgram; } + + IresShader* GetIres() const { return mIres; } + + bool IsValid() const; +}; + +// Initialized in main() +inline RcPtr<Shader> gDefaultShader; + +class IresShader : public IresObject { +private: + RcPtr<Shader> mInstance; + std::string mSourceFile; + +public: + IresShader(); + + Shader* GetInstance() const; + void InvalidateInstance(); + + void ShowEditor(IEditor& editor) override; + + void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override; + void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override; +}; diff --git a/src/brussel.engine/Sprite.cpp b/src/brussel.engine/Sprite.cpp new file mode 100644 index 0000000..2b4923c --- /dev/null +++ b/src/brussel.engine/Sprite.cpp @@ -0,0 +1,328 @@ +#include "Sprite.hpp" + +#include "AppConfig.hpp" +#include "CommonVertexIndex.hpp" +#include "EditorCore.hpp" +#include "EditorUtils.hpp" +#include "Image.hpp" +#include "RapidJsonHelper.hpp" +#include "Rect.hpp" + +#include <imgui.h> +#include <misc/cpp/imgui_stdlib.h> +#include <rapidjson/document.h> +#include <memory> + +using namespace std::literals; + +bool SpriteDefinition::IsValid() const { + return mAtlas != nullptr; +} + +bool IresSpriteFiles::IsValid() const { + return !spriteFiles.empty(); +} + +SpriteDefinition* IresSpriteFiles::CreateInstance() const { + if (IsValid()) { + return nullptr; + } + + std::vector<Texture::AtlasSource> sources; + sources.resize(spriteFiles.size()); + for (auto& file : spriteFiles) { + } + + Texture::AtlasOutput atlasOut; + Texture::AtlasInput atlasIn{ + .sources = sources, + .packingMode = Texture::PM_KeepSquare, + }; + atlasIn.sources = sources; + auto atlas = std::make_unique<Texture>(); + if (atlas->InitAtlas(atlasIn, &atlasOut) != Texture::EC_Success) { + return nullptr; + } + + auto sprite = std::make_unique<SpriteDefinition>(); + sprite->mAtlas.Attach(atlas.release()); + sprite->mBoundingBox = atlasOut.elements[0].subregionSize; + sprite->mFrames.reserve(atlasOut.elements.size()); + for (auto& elm : atlasOut.elements) { + // Validate bounding box + if (sprite->mBoundingBox != elm.subregionSize) { + return nullptr; + } + + // Copy frame subregion + sprite->mFrames.push_back(elm.subregion); + } + return sprite.release(); +} + +SpriteDefinition* IresSpriteFiles::GetInstance() { + if (mInstance == nullptr) { + mInstance.Attach(CreateInstance()); + } + return mInstance.Get(); +} + +void IresSpriteFiles::InvalidateInstance() { + mInstance.Attach(nullptr); +} + +void IresSpriteFiles::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const { + IresObject::Write(ctx, value, root); + value.AddMember("Sprites", rapidjson::WriteVectorPrimitives(root, spriteFiles.begin(), spriteFiles.end()), root.GetAllocator()); +} + +void IresSpriteFiles::Read(IresLoadingContext& ctx, const rapidjson::Value& value) { + InvalidateInstance(); + + IresObject::Read(ctx, value); + + auto rvFileList = rapidjson::GetProperty(value, rapidjson::kArrayType, "Sprites"sv); + if (!rvFileList) return; + spriteFiles.clear(); + rapidjson::ReadVectorPrimitives<decltype(spriteFiles)>(*rvFileList, spriteFiles); +} + +bool IresSpritesheet::IsValid() const { + return !spritesheetFile.empty() && + sheetWSplit != 0 && + sheetHSplit != 0; +} + +void IresSpritesheet::ResplitSpritesheet(SpriteDefinition* sprite, const IresSpritesheet* conf) { + ResplitSpritesheet(sprite, conf->sheetWSplit, conf->sheetHSplit, conf->frameCountOverride); +} + +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; + int frameHeight = size.y / hSplit; + + sprite->mBoundingBox = { frameWidth, frameHeight }; + sprite->mFrames.clear(); + sprite->mFrames.reserve(wSplit * hSplit); + + // Width and height in UV coordinates for each frame + float deltaU = 1.0f / wSplit; + float deltaV = 1.0f / hSplit; + int i = 0; + if (frameCount < 0) { + frameCount = std::numeric_limits<int>::max(); + } + for (int y = 0; y < hSplit; ++y) { + for (int x = 0; x < wSplit; ++x) { + auto& subregion = sprite->mFrames.emplace_back(); + // Top left + subregion.u0 = deltaU * x; + subregion.v0 = deltaV * y; + // Bottom right + subregion.u1 = subregion.u0 + deltaU; + subregion.v1 = subregion.v0 + deltaV; + + if ((i + 1) >= frameCount) { + return; + } + + ++i; + } + } +} + +SpriteDefinition* IresSpritesheet::CreateInstance() const { + if (!IsValid()) { + return nullptr; + } + + char path[2048]; + 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<SpriteDefinition>(); + sprite->mAtlas.Attach(atlas.release()); + ResplitSpritesheet(sprite.get(), this); + return sprite.release(); +} + +SpriteDefinition* IresSpritesheet::GetInstance() { + if (mInstance == nullptr) { + mInstance.Attach(CreateInstance()); + } + return mInstance.Get(); +} + +void IresSpritesheet::InvalidateInstance() { + mInstance.Attach(nullptr); +} + +bool IresSpritesheet::IsFrameCountOverriden() const { + return frameCountOverride > 0; +} + +int IresSpritesheet::GetFrameCount() const { + if (IsFrameCountOverriden()) { + return frameCountOverride; + } else { + return sheetWSplit * sheetHSplit; + } +} + +void IresSpritesheet::ShowEditor(IEditor& editor) { + IresObject::ShowEditor(editor); + + bool doInvalidateInstance = false; + auto instance = GetInstance(); // NOTE: may be null + + if (ImGui::Button("View Sprite", instance == nullptr)) { + editor.OpenSpriteViewer(instance); + } + + if (ImGui::InputText("Spritesheet", &spritesheetFile)) { + doInvalidateInstance = true; + } + + if (ImGui::InputInt("Horizontal split", &sheetWSplit)) { + sheetWSplit = std::max(sheetWSplit, 1); + if (instance) IresSpritesheet::ResplitSpritesheet(instance, this); + } + + if (ImGui::InputInt("Vertical split", &sheetHSplit)) { + sheetHSplit = std::max(sheetHSplit, 1); + if (instance) IresSpritesheet::ResplitSpritesheet(instance, this); + } + + bool frameCountOverriden = frameCountOverride > 0; + if (ImGui::Checkbox("##", &frameCountOverriden)) { + if (frameCountOverriden) { + frameCountOverride = sheetWSplit * sheetHSplit; + } else { + frameCountOverride = 0; + } + } + ImGui::SameLine(); + if (frameCountOverriden) { + if (ImGui::InputInt("Frame count", &frameCountOverride)) { + frameCountOverride = std::max(frameCountOverride, 1); + if (instance) IresSpritesheet::ResplitSpritesheet(instance, this); + } + } else { + int dummy = sheetWSplit * sheetHSplit; + ImGui::PushDisabled(); + ImGui::InputInt("Frame count", &dummy, ImGuiInputTextFlags_ReadOnly); + ImGui::PopDisabled(); + } + + if (instance) { + auto atlas = instance->GetAtlas(); + auto imageSize = Utils::FitImage(atlas->GetInfo().size); + auto imagePos = ImGui::GetCursorScreenPos(); + ImGui::Image((ImTextureID)(uintptr_t)atlas->GetHandle(), imageSize); + + auto drawlist = ImGui::GetWindowDrawList(); + float deltaX = imageSize.x / sheetWSplit; + for (int ix = 0; ix < sheetWSplit + 1; ++ix) { + float x = ix * deltaX; + ImVec2 start{ imagePos.x + x, imagePos.y }; + ImVec2 end{ imagePos.x + x, imagePos.y + imageSize.y }; + drawlist->AddLine(start, end, IM_COL32(255, 255, 0, 255)); + } + float deltaY = imageSize.y / sheetHSplit; + for (int iy = 0; iy < sheetHSplit + 1; ++iy) { + float y = iy * deltaY; + ImVec2 start{ imagePos.x, imagePos.y + y }; + ImVec2 end{ imagePos.x + imageSize.x, imagePos.y + y }; + drawlist->AddLine(start, end, IM_COL32(255, 255, 0, 255)); + } + + int i = sheetWSplit * sheetHSplit; + int frameCount = GetFrameCount(); + for (int y = sheetHSplit - 1; y >= 0; --y) { + for (int x = sheetWSplit - 1; x >= 0; --x) { + if (i > frameCount) { + ImVec2 tl{ imagePos.x + x * deltaX + 1.0f, imagePos.y + y * deltaY + 1.0f }; + ImVec2 br{ imagePos.x + (x + 1) * deltaX, imagePos.y + (y + 1) * deltaY }; + drawlist->AddRectFilled(tl, br, IM_COL32(255, 0, 0, 100)); + } + --i; + } + } + } else { + ImGui::TextUnformatted("Sprite configuration invalid"); + } + + if (doInvalidateInstance) { + InvalidateInstance(); + } +} + +void IresSpritesheet::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const { + IresObject::Write(ctx, value, root); + value.AddMember("SpriteSheet", spritesheetFile, root.GetAllocator()); + value.AddMember("WSplit", sheetWSplit, root.GetAllocator()); + value.AddMember("HSplit", sheetHSplit, root.GetAllocator()); + if (frameCountOverride > 0) { + value.AddMember("FrameCount", frameCountOverride, root.GetAllocator()); + } +} + +void IresSpritesheet::Read(IresLoadingContext& ctx, const rapidjson::Value& value) { + InvalidateInstance(); + + IresObject::Read(ctx, value); + BRUSSEL_JSON_GET(value, "SpriteSheet", std::string, spritesheetFile, return ); + BRUSSEL_JSON_GET(value, "WSplit", int, sheetWSplit, return ); + BRUSSEL_JSON_GET(value, "HSplit", int, sheetHSplit, return ); + BRUSSEL_JSON_GET_DEFAULT(value, "FrameCount", int, frameCountOverride, 0); +} + +Sprite::Sprite() + : mDefinition(nullptr) { +} + +bool Sprite::IsValid() const { + return mDefinition != nullptr; +} + +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 Sprite::SetPlaybackSpeed(int speed) { + // TODO +} diff --git a/src/brussel.engine/Sprite.hpp b/src/brussel.engine/Sprite.hpp new file mode 100644 index 0000000..e163a01 --- /dev/null +++ b/src/brussel.engine/Sprite.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include "Ires.hpp" +#include "PodVector.hpp" +#include "RcPtr.hpp" +#include "Renderer.hpp" +#include "Texture.hpp" + +#include <rapidjson/fwd.h> +#include <glm/glm.hpp> +#include <string> +#include <string_view> +#include <vector> + +class SpriteDefinition : public RefCounted { + friend class IresSpriteFiles; + friend class IresSpritesheet; + +private: + RcPtr<Texture> mAtlas; + glm::ivec2 mBoundingBox; + std::vector<Subregion> mFrames; + +public: + bool IsValid() const; + Texture* GetAtlas() const { return mAtlas.Get(); } + glm::ivec2 GetBoundingBox() const { return mBoundingBox; } + const decltype(mFrames)& GetFrames() const { return mFrames; } +}; + +class IresSpriteFiles : public IresObject { +public: + RcPtr<SpriteDefinition> mInstance; + std::vector<std::string> spriteFiles; + +public: + IresSpriteFiles() + : IresObject(KD_SpriteFiles) {} + + // NOTE: does not check whether all specified files have the same dimensions + bool IsValid() const; + + SpriteDefinition* CreateInstance() const; + SpriteDefinition* GetInstance(); + void InvalidateInstance(); + + void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override; + void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override; +}; + +class IresSpritesheet : public IresObject { +public: + RcPtr<SpriteDefinition> mInstance; + std::string spritesheetFile; + int sheetWSplit = 1; + int sheetHSplit = 1; + int frameCountOverride = 0; + +public: + IresSpritesheet() + : IresObject(KD_Spritesheet) {} + + bool IsValid() const; + + static void ResplitSpritesheet(SpriteDefinition* sprite, const IresSpritesheet* conf); + static void ResplitSpritesheet(SpriteDefinition* sprite, int wSplit, int hSplit, int frameCountOverride = -1); + + SpriteDefinition* CreateInstance() const; + SpriteDefinition* GetInstance(); + void InvalidateInstance(); + + bool IsFrameCountOverriden() const; + int GetFrameCount() const; + + void ShowEditor(IEditor& editor) override; + + void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override; + void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override; +}; + +// TODO +class SpriteCollection { +private: + std::vector<SpriteDefinition> mSprites; +}; + +class Sprite { +private: + RcPtr<SpriteDefinition> mDefinition; + int mCurrentFrame = 0; + int mTimeElapsed = 0; + // # of frames per second + int mPlaybackSpeed = 5; + +public: + Sprite(); + + bool IsValid() const; + + SpriteDefinition* GetDefinition() const { return mDefinition.Get(); } + void SetDefinition(SpriteDefinition* definition); + + int GetFrame() const; + const Subregion& GetFrameSubregion() const; + void SetFrame(int frame); + // Update as if a render frame has passed + void PlayFrame(); + + int GetPlaybackSpeed() const; + void SetPlaybackSpeed(int speed); +}; diff --git a/src/brussel.engine/Texture.cpp b/src/brussel.engine/Texture.cpp new file mode 100644 index 0000000..6fa7c8a --- /dev/null +++ b/src/brussel.engine/Texture.cpp @@ -0,0 +1,250 @@ +#include "Texture.hpp" + +#include "Macros.hpp" +#include "PodVector.hpp" +#include "ScopeGuard.hpp" + +#include <stb_image.h> +#include <stb_rect_pack.h> +#include <bit> +#include <cstring> +#include <utility> + +Texture::~Texture() { + glDeleteTextures(1, &mHandle); +} + +static GLenum MapTextureFilteringToGL(Tags::TexFilter option) { + using namespace Tags; + switch (option) { + case TF_Linear: return GL_LINEAR; + case TF_Nearest: return GL_NEAREST; + } + return 0; +} + +Texture::ErrorCode Texture::InitFromFile(const char* filePath) { + if (IsValid()) { + return EC_AlreadyInitialized; + } + + int width, height; + int channels; + + auto result = (uint8_t*)stbi_load(filePath, &width, &height, &channels, 4); + if (!result) { + return EC_FileIoFailed; + } + DEFER { stbi_image_free(result); }; + + glGenTextures(1, &mHandle); + glBindTexture(GL_TEXTURE_2D, mHandle); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, result); + + mInfo.size = { width, height }; + + return EC_Success; +} + +Texture::ErrorCode Texture::InitFromImage(const Image& image) { + if (IsValid()) { + return EC_AlreadyInitialized; + } + + GLenum sourceFormat; + switch (image.GetChannels()) { + case 1: sourceFormat = GL_RED; break; + case 2: sourceFormat = GL_RG; break; + case 3: sourceFormat = GL_RGB; break; + case 4: sourceFormat = GL_RGBA; break; + default: return EC_InvalidImage; + } + + auto size = image.GetSize(); + uint8_t* dataPtr = image.GetDataPtr(); + + glGenTextures(1, &mHandle); + glBindTexture(GL_TEXTURE_2D, mHandle); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, sourceFormat, size.x, size.y, 0, sourceFormat, GL_UNSIGNED_BYTE, dataPtr); + + mInfo.size = size; + + return EC_Success; +} + +Texture::ErrorCode Texture::InitAtlas(const AtlasInput& in, AtlasOutput* out) { + // Force RGBA for easier time uploading to GL texture + constexpr int kDesiredChannels = 4; + + PodVector<stbrp_rect> rects; + rects.resize(in.sources.size()); + + for (size_t i = 0; i < in.sources.size(); ++i) { + auto size = in.sources[i].image.GetSize(); + auto& rect = rects[i]; + rect.w = static_cast<stbrp_coord>(size.x); + rect.h = static_cast<stbrp_coord>(size.y); + } + + int atlasWidth; + int atlasHeight; + + // 1. Pack the candidate rectanges onto the (not yet allocated) atlas + // Note that the coordinates here are top-left origin + switch (in.packingMode) { + case PM_KeepSquare: { + atlasWidth = 512; + atlasHeight = 512; + + PodVector<stbrp_node> nodes; + while (true) { + // No need to zero initialize stbrp_node, library will take care of that + nodes.resize(atlasWidth); + + stbrp_context ctx; + stbrp_init_target(&ctx, atlasWidth, atlasHeight, &nodes[0], (int)nodes.size()); + int result = stbrp_pack_rects(&ctx, rects.data(), (int)rects.size()); + + if (result != 1) { + atlasWidth *= 2; + atlasHeight *= 2; + } else { + // Break out of the while loop + break; + } + } + } break; + + case PM_VerticalExtension: + case PM_HorizontalExtension: { + constexpr int kMaxHeight = 1024 * 32; + atlasWidth = 0; + atlasHeight = 0; + + PodVector<stbrp_node> nodes; + stbrp_context ctx; + stbrp_init_target(&ctx, atlasWidth, atlasHeight, &nodes[0], nodes.size()); + stbrp_pack_rects(&ctx, rects.data(), rects.size()); + + // Calculate width/height needed for atlas + auto& limiter = in.packingMode == PM_VerticalExtension ? atlasHeight : atlasWidth; + for (auto& rect : rects) { + int bottom = rect.y + rect.h; + limiter = std::max(limiter, bottom); + } + limiter = std::bit_ceil<uint32_t>(limiter); + } break; + } + + // 2. Allocate atlas bitmap + + // Number of bytes in *bitmap* + auto bytes = atlasWidth * atlasHeight * kDesiredChannels * sizeof(uint8_t); + // Note that the origin (first pixel) is the bottom-left corner, to be consistent with OpenGL + auto bitmap = std::make_unique<uint8_t[]>(bytes); + std::memset(bitmap.get(), 0, bytes * sizeof(uint8_t)); + + // 3. Put all candidate images to the atlas bitmap + // TODO don't flip + // We essentially flip the candidate images vertically when putting into the atlas bitmap, so that when OpenGL reads + // these bytes, it sees the "bottom row" (if talking in top-left origin) first + // (empty spots are set with 0, "flipping" doesn't apply to them) + // + // Conceptually, we flip the atlas bitmap vertically so that the origin is at bottom-left + // i.e. all the coordinates we talk (e.g. rect.x/y) are still in top-left origin + + // Unit: bytes + size_t bitmapRowStride = atlasWidth * kDesiredChannels * sizeof(uint8_t); + for (size_t i = 0; i < in.sources.size(); ++i) { + auto& rect = rects[i]; + // Data is assumed to be stored in top-left origin + auto data = in.sources[i].image.GetDataPtr(); + + // We need to copy row by row, because the candidate image bytes won't land in a continuous chunk in our atlas bitmap + // Unit: bytes + size_t incomingRowStride = rect.w * kDesiredChannels * sizeof(uint8_t); + // Unit: bytes + size_t bitmapX = rect.x * kDesiredChannels * sizeof(uint8_t); + for (int y = 0; y < rect.h; ++y) { + auto src = data + y * incomingRowStride; + + int bitmapY = y; + auto dst = bitmap.get() + bitmapY * bitmapRowStride + bitmapX; + + std::memcpy(dst, src, incomingRowStride); + } + } + + // 4. Upload to VRAM + GLuint atlasTexture; + glGenTextures(1, &atlasTexture); + glBindTexture(GL_TEXTURE_2D, atlasTexture); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, atlasWidth, atlasHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, bitmap.get()); + + // 5. Generate atlas texture info + mHandle = atlasTexture; + mInfo.size = { atlasWidth, atlasHeight }; + + // 6. Generate output information + if (out) { + out->elements.reserve(in.sources.size()); + for (size_t i = 0; i < in.sources.size(); ++i) { + auto& rect = rects[i]; + auto& source = in.sources[i]; + out->elements.push_back(AltasElement{ + .name = source.name, + .subregion = Subregion{ + .u0 = (float)(rect.x) / atlasWidth, + .v0 = (float)(rect.y + rect.h) / atlasHeight, + .u1 = (float)(rect.x + rect.w) / atlasWidth, + .v1 = (float)(rect.y) / atlasHeight, + }, + .subregionSize = glm::ivec2(rect.w, rect.h), + }); + } + } + + return EC_Success; +} + +const TextureInfo& Texture::GetInfo() const { + return mInfo; +} + +GLuint Texture::GetHandle() const { + return mHandle; +} + +bool Texture::IsValid() const { + return mHandle != 0; +} + +Texture* IresTexture::CreateInstance() const { + return new Texture(); +} + +Texture* IresTexture::GetInstance() { + if (mInstance == nullptr) { + mInstance.Attach(CreateInstance()); + } + return mInstance.Get(); +} + +void IresTexture::Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const { + IresObject::Write(ctx, value, root); + // TODO +} + +void IresTexture::Read(IresLoadingContext& ctx, const rapidjson::Value& value) { + IresObject::Read(ctx, value); + // TODO +} diff --git a/src/brussel.engine/Texture.hpp b/src/brussel.engine/Texture.hpp new file mode 100644 index 0000000..108dfa7 --- /dev/null +++ b/src/brussel.engine/Texture.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include "GraphicsTags.hpp" +#include "Image.hpp" +#include "Ires.hpp" +#include "RcPtr.hpp" + +#include <glad/glad.h> +#include <cstdint> +#include <glm/glm.hpp> +#include <span> + +// TODO abstract texture traits such as component sizes from OpenGL + +struct Subregion { + float u0 = 0.0f; + float v0 = 0.0f; + float u1 = 0.0f; + float v1 = 0.0f; +}; + +struct TextureInfo { + glm::ivec2 size; + Tags::TexFilter minifyingFilter = Tags::TF_Linear; + Tags::TexFilter magnifyingFilter = Tags::TF_Linear; +}; + +class Texture : public RefCounted { + friend class TextureStitcher; + +private: + TextureInfo mInfo; + GLuint mHandle = 0; + +public: + Texture() = default; + ~Texture(); + + Texture(const Texture&) = delete; + Texture& operator=(const Texture&) = delete; + Texture(Texture&&) = default; + Texture& operator=(Texture&&) = default; + + enum ErrorCode { + EC_Success, + EC_AlreadyInitialized, + EC_FileIoFailed, + EC_InvalidImage, + }; + + ErrorCode InitFromFile(const char* filePath); + ErrorCode InitFromImage(const Image& image); + + struct AtlasSource { + std::string name; + Image image; + }; + + struct AltasElement { + std::string name; + Subregion subregion; + glm::ivec2 subregionSize; + }; + + enum PackingMode { + PM_KeepSquare, + PM_VerticalExtension, + PM_HorizontalExtension, + }; + + struct AtlasInput { + std::span<AtlasSource> sources; + PackingMode packingMode; + }; + struct AtlasOutput { + std::vector<AltasElement> elements; + }; + ErrorCode InitAtlas(const AtlasInput& in, AtlasOutput* out = nullptr); + + const TextureInfo& GetInfo() const; + GLuint GetHandle() const; + + bool IsValid() const; +}; + +class IresTexture : public IresObject { +public: + RcPtr<Texture> mInstance; + +public: + IresTexture() + : IresObject(KD_Texture) {} + + Texture* CreateInstance() const; + Texture* GetInstance(); + + void Write(IresWritingContext& ctx, rapidjson::Value& value, rapidjson::Document& root) const override; + void Read(IresLoadingContext& ctx, const rapidjson::Value& value) override; +}; diff --git a/src/brussel.engine/VertexIndex.cpp b/src/brussel.engine/VertexIndex.cpp new file mode 100644 index 0000000..ac68289 --- /dev/null +++ b/src/brussel.engine/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/src/brussel.engine/VertexIndex.hpp b/src/brussel.engine/VertexIndex.hpp new file mode 100644 index 0000000..2d65617 --- /dev/null +++ b/src/brussel.engine/VertexIndex.hpp @@ -0,0 +1,67 @@ +#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); +}; diff --git a/src/brussel.engine/World.cpp b/src/brussel.engine/World.cpp new file mode 100644 index 0000000..83b9a10 --- /dev/null +++ b/src/brussel.engine/World.cpp @@ -0,0 +1,79 @@ +#include "World.hpp" + +#include "GameObject.hpp" +#include "PodVector.hpp" + +#include <glad/glad.h> + +namespace ProjectBrussel_UNITY_ID { +template <typename TFunction> +void CallGameObjectRecursive(GameObject* start, TFunction&& func) { + PodVector<GameObject*> stack; + stack.push_back(start); + + while (!stack.empty()) { + auto obj = stack.back(); + stack.pop_back(); + + for (auto child : obj->GetChildren()) { + stack.push_back(child); + } + + func(obj); + } +} +} // namespace ProjectBrussel_UNITY_ID + +GameWorld::GameWorld() + : mRoot{ new GameObject(this) } { +} + +GameWorld::~GameWorld() { + if (mAwakened) { + Resleep(); + } + + delete mRoot; +} + +const GameObject& GameWorld::GetRoot() const { + return *mRoot; +}; + +void GameWorld::Awaken() { + using namespace ProjectBrussel_UNITY_ID; + + if (mAwakened) { + return; + } + + CallGameObjectRecursive(mRoot, [](GameObject* obj) { obj->Awaken(); }); + mAwakened = true; +} + +void GameWorld::Resleep() { + using namespace ProjectBrussel_UNITY_ID; + + if (!mAwakened) { + return; + } + + CallGameObjectRecursive(mRoot, [](GameObject* obj) { obj->Resleep(); }); + mAwakened = false; +} + +void GameWorld::Update() { + using namespace ProjectBrussel_UNITY_ID; + + CallGameObjectRecursive(mRoot, [this](GameObject* obj) { + obj->Update(); + }); +} + +GameObject& GameWorld::GetRoot() { + return *mRoot; +} + +bool GameWorld::IsAwake() const { + return mAwakened; +} diff --git a/src/brussel.engine/World.hpp b/src/brussel.engine/World.hpp new file mode 100644 index 0000000..288142e --- /dev/null +++ b/src/brussel.engine/World.hpp @@ -0,0 +1,25 @@ +#pragma once + +class GameObject; +class GameWorld { +private: + GameObject* mRoot; + bool mAwakened = false; + +public: + GameWorld(); + ~GameWorld(); + + GameWorld(const GameWorld&) = delete; + GameWorld& operator=(const GameWorld&) = delete; + GameWorld(GameWorld&&) = default; + GameWorld& operator=(GameWorld&&) = default; + + bool IsAwake() const; + void Awaken(); + void Resleep(); + void Update(); + + const GameObject& GetRoot() const; + GameObject& GetRoot(); +}; diff --git a/src/brussel.engine/main.cpp b/src/brussel.engine/main.cpp new file mode 100644 index 0000000..30ba9a6 --- /dev/null +++ b/src/brussel.engine/main.cpp @@ -0,0 +1,545 @@ +#include "App.hpp" + +#include "AppConfig.hpp" +#include "CommonVertexIndex.hpp" +#include "ImGuiGuizmo.hpp" +#include "Input.hpp" +#include "Ires.hpp" +#include "Level.hpp" +#include "Log.hpp" +#include "Material.hpp" +#include "Shader.hpp" + +#define GLFW_INCLUDE_NONE +#include <GLFW/glfw3.h> + +#include <backends/imgui_impl_glfw.h> +#include <backends/imgui_impl_opengl2.h> +#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> + +#include <tracy/Tracy.hpp> +#include <tracy/TracyClient.cpp> + +namespace fs = std::filesystem; +using namespace std::literals; + +struct GlfwUserData { + App* app = nullptr; +}; + +void GlfwErrorCallback(int error, const char* description) { + fprintf(stderr, "[GLFW] Error %d: %s\n", error, description); +} + +void GlfwKeyboardCallback(GLFWkeyboard* keyboard, int event) { + if (InputState::instance == nullptr) { + // Called before initialization, skipping because we'll do a collect pass anyways when initializing + return; + } + + switch (event) { + case GLFW_CONNECTED: { + InputState::instance->ConnectKeyboard(keyboard); + } break; + + case GLFW_DISCONNECTED: { + InputState::instance->DisconnectKeyboard(keyboard); + } break; + } +} + +void OpenGLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { + fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n", (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""), type, severity, message); +} + +void GlfwFramebufferResizeCallback(GLFWwindow* window, int width, int height) { + AppConfig::mainWindowWidth = width; + AppConfig::mainWindowHeight = height; + AppConfig::mainWindowAspectRatio = (float)width / height; +} + +void GlfwMouseCallback(GLFWwindow* window, int button, int action, int mods) { + if (ImGui::GetIO().WantCaptureMouse) { + return; + } + + auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window)); + auto app = userData->app; + app->HandleMouse(button, action); +} + +void GlfwMouseMotionCallback(GLFWwindow* window, double xOff, double yOff) { + if (ImGui::GetIO().WantCaptureMouse) { + return; + } + + auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window)); + auto app = userData->app; + app->HandleMouseMotion(xOff, yOff); +} + +void GlfwKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { + if (ImGui::GetIO().WantCaptureKeyboard) { + return; + } + + GLFWkeyboard* keyboard = glfwGetLastActiveKeyboard(); + if (keyboard) { + auto userData = static_cast<GlfwUserData*>(glfwGetWindowUserPointer(window)); + auto app = userData->app; + app->HandleKey(keyboard, key, action); + } +} + +// For platform data path selection below +// 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; + +#if BRUSSEL_DEV_ENV + Log::gDefaultBuffer.messages.resize(1024); + Log::gDefaultBufferId = Log::RegisterBuffer(Log::gDefaultBuffer); +#endif + + constexpr auto kOpenGLDebug = "opengl-debug"; + constexpr auto kImGuiBackend = "imgui-backend"; + constexpr auto kGameDataDir = "game-data-dir"; + constexpr auto kGameAssetDir = "game-asset-dir"; + + cxxopts::Options options(std::string(AppConfig::kAppName), ""); + // clang-format off + options.add_options() + (kOpenGLDebug, "Enable OpenGL debugging messages.") + (kImGuiBackend, "ImGui backend. Options: opengl2, opengl3. Leave empty to default.", cxxopts::value<std::string>()) + (kGameAssetDir, "Directory in which assets are looked up from. Can be relative paths to the executable.", cxxopts::value<std::string>()->default_value(".")) + (kGameDataDir, "Directory in which game data (such as saves and options) are saved to. Leave empty to use the default directory on each platform.", cxxopts::value<std::string>()) + ; + // clang-format on + 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; + } else if (imguiBackend == "opengl3") { + imguiUseOpenGL3 = true; + } else { + // TODO support more backends? + imguiUseOpenGL3 = true; + } + } else { + imguiUseOpenGL3 = true; + } + + if (args.count(kGameAssetDir) > 0) { + auto assetDir = args[kGameAssetDir].as<std::string>(); + + fs::path assetDirPath(assetDir); + if (!fs::exists(assetDirPath)) { + fprintf(stderr, "Invalid asset directory.\n"); + return -4; + } + + AppConfig::assetDir = std::move(assetDir); + AppConfig::assetDirPath = std::move(assetDirPath); + } else { + AppConfig::assetDir = "."; + AppConfig::assetDirPath = fs::path("."); + } + + if (args.count(kGameDataDir) > 0) { + auto dataDir = args[kGameDataDir].as<std::string>(); + + fs::path dataDirPath(dataDir); + fs::create_directories(dataDir); + + AppConfig::dataDir = std::move(dataDir); + AppConfig::dataDirPath = std::move(dataDirPath); + } else { +#if BRUSSEL_DEV_ENV + AppConfig::dataDir = "."; + AppConfig::dataDirPath = fs::path("."); +#else +// In a regular build, use default platform data paths +# if defined(_WIN32) + fs::path dataDirPath; + + PWSTR path = nullptr; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path); + if (SUCCEEDED(hr)) { + dataDirPath = fs::path(path) / AppConfig::kAppName; + CoTaskMemFree(path); + + fs::create_directories(dataDirPath); + } 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 dataDirPath = fs::path("~/Library/Application Support/") / AppConfig::kAppName; + fs::create_directories(dataDirPath); +# elif defined(__linux__) + auto dataDirPath = GetEnvVar("XDG_DATA_HOME", "~/.local/share") / AppConfig::kAppName; + fs::create_directories(dataDirPath); +# endif + AppConfig::dataDir = dataDirPath.string(); + AppConfig::dataDirPath = dataDirPath; +#endif + } + + if (!glfwInit()) { + return -1; + } + + glfwSetErrorCallback(&GlfwErrorCallback); + glfwSetKeyboardCallback(&GlfwKeyboardCallback); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); + +#if defined(__APPLE__) + const char* imguiGlslVersion = "#version 150"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac +#else + const char* imguiGlslVersion = "#version 130"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); +#endif + + GlfwUserData glfwUserData; + + GLFWwindow* window = glfwCreateWindow(1280, 720, AppConfig::kAppNameC, nullptr, nullptr); + if (window == nullptr) { + return -2; + } + + 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); + + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { + return -3; + } + +#if defined(BRUSSEL_DEV_ENV) + auto glVersionString = glGetString(GL_VERSION); + + int glMajorVersion; + glGetIntegerv(GL_MAJOR_VERSION, &glMajorVersion); + int glMinorVersion; + glGetIntegerv(GL_MINOR_VERSION, &glMinorVersion); + + printf("OpenGL version (via glGetString(GL_VERSION)): %s\n", glVersionString); + printf("OpenGL version (via glGetIntegerv() with GL_MAJOR_VERSION and GL_MINOR_VERSION): %d.%d\n", glMajorVersion, glMinorVersion); +#endif + + bool useOpenGLDebug = args[kOpenGLDebug].as<bool>(); + if (useOpenGLDebug) { + printf("Using OpenGL debugging\n --%s", kOpenGLDebug); + + // TODO check extension KHR_debug availability + // TODO conan glad is not including any extensions + // NOTE: KHR_debug is a core extension, which means it may be available in lower version even though the feature is added in 4.3 + + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageCallback(&OpenGLDebugCallback, 0); + } + + IMGUI_CHECKVERSION(); + auto ctx = ImGui::CreateContext(); + auto& io = ImGui::GetIO(); + ImGuizmo::SetImGuiContext(ctx); + + ImGui_ImplGlfw_InitForOpenGL(window, true); + if (imguiUseOpenGL3) { + ImGui_ImplOpenGL3_Init(imguiGlslVersion); + } else { + ImGui_ImplOpenGL2_Init(); + } + + InputState::instance = new InputState(); + { + int count; + GLFWkeyboard** list = glfwGetKeyboards(&count); + for (int i = 0; i < count; ++i) { + GLFWkeyboard* keyboard = list[i]; + InputState::instance->ConnectKeyboard(keyboard); + } + } + + IresManager::instance = new IresManager(); + IresManager::instance->DiscoverFilesDesignatedLocation(); + + LevelManager::instance = new LevelManager(); + LevelManager::instance->DiscoverFilesDesignatedLocation(); + + gVformatStandard.Attach(new VertexFormat()); + gVformatStandard->AddElement(VertexElementFormat{ + .bindingIndex = 0, + .type = VET_Float3, + .semantic = VES_Position, + }); + gVformatStandard->AddElement(VertexElementFormat{ + .bindingIndex = 0, + .type = VET_Float2, + .semantic = VES_TexCoords1, + }); + gVformatStandard->AddElement(VertexElementFormat{ + .bindingIndex = 0, + .type = VET_Ubyte4Norm, + .semantic = VES_Color1, + }); + + gVformatStandardSplit.Attach(new VertexFormat()); + gVformatStandardSplit->AddElement(VertexElementFormat{ + .bindingIndex = 0, + .type = VET_Float3, + .semantic = VES_Position, + }); + gVformatStandardSplit->AddElement(VertexElementFormat{ + .bindingIndex = 1, + .type = VET_Float2, + .semantic = VES_TexCoords1, + }); + gVformatStandardSplit->AddElement(VertexElementFormat{ + .bindingIndex = 1, + .type = VET_Ubyte4Norm, + .semantic = VES_Color1, + }); + + gVformatLines.Attach(new VertexFormat()); + gVformatLines->AddElement(VertexElementFormat{ + .bindingIndex = 0, + .type = VET_Float3, + .semantic = VES_Position, + }); + gVformatLines->AddElement(VertexElementFormat{ + .bindingIndex = 0, + .type = VET_Ubyte4Norm, + .semantic = VES_Color1, + }); + + // Matches gVformatStandard + gDefaultShader.Attach(new Shader()); + gDefaultShader->InitFromSources(Shader::ShaderSources{ + .vertex = R"""( +#version 330 core +layout(location = 0) in vec3 pos; +layout(location = 1) in vec4 color; +out vec4 v2fColor; +uniform mat4 transform; +void main() { + gl_Position = transform * 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 vec4 color; + ShaderMathVariable var; + var.scalarType = GL_FLOAT; + var.width = 1; + var.height = 4; + var.arrayLength = 1; + var.semantic = VES_Color1; + var.location = 1; + 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 + + gDefaultMaterial.Attach(new Material()); + gDefaultMaterial->SetShader(gDefaultShader.Get()); + + { // Main loop + App app; + glfwUserData.app = &app; + + // NOTE: don't enable backface culling, because the game mainly runs in 2D and sometimes we'd like to flip sprites around + // it also helps with debugging layers in 3D view + glEnable(GL_DEPTH_TEST); + + // 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)) { + { + ZoneScopedN("GameInput"); + glfwPollEvents(); + } + + double currTime = glfwGetTime(); + double deltaTime = prevTime - currTime; + + // 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(); + { + ZoneScopedN("GameUpdate"); + 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; + } + } + + int fbWidth = AppConfig::mainWindowWidth; + 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 | GL_DEPTH_BUFFER_BIT); + + { // Regular draw + ZoneScopedN("Render"); + app.Draw(currTime, deltaTime); + } + + { // ImGui draw + ZoneScopedN("ImGui"); + 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); + FrameMark; + + prevTime = currTime; + } + } + + if (imguiUseOpenGL3) { + ImGui_ImplOpenGL3_Shutdown(); + } else { + ImGui_ImplOpenGL2_Shutdown(); + } + ImGui_ImplGlfw_Shutdown(); + + ImGui::DestroyContext(); + + glfwDestroyWindow(window); + glfwTerminate(); + + return 0; +} |