aboutsummaryrefslogtreecommitdiff
path: root/src/brussel.engine
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2023-10-19 22:50:07 -0700
committerrtk0c <[email protected]>2023-10-19 22:50:07 -0700
commit2c92e07f337e42cf58970443f9de678f85a9b2a4 (patch)
tree075d5407e1e12a9d35cbee6e4c20ad34e0765c42 /src/brussel.engine
parent615809c036f604bce4582cea8ad49c64693f4f45 (diff)
The great renaming: switch to "module style"
Diffstat (limited to 'src/brussel.engine')
-rw-r--r--src/brussel.engine/App.cpp168
-rw-r--r--src/brussel.engine/App.hpp62
-rw-r--r--src/brussel.engine/AppConfig.hpp26
-rw-r--r--src/brussel.engine/Camera.cpp46
-rw-r--r--src/brussel.engine/Camera.hpp35
-rw-r--r--src/brussel.engine/CommonVertexIndex.cpp152
-rw-r--r--src/brussel.engine/CommonVertexIndex.hpp77
-rw-r--r--src/brussel.engine/EditorAccessories.cpp26
-rw-r--r--src/brussel.engine/EditorAccessories.hpp8
-rw-r--r--src/brussel.engine/EditorAttachment.hpp12
-rw-r--r--src/brussel.engine/EditorAttachmentImpl.cpp23
-rw-r--r--src/brussel.engine/EditorAttachmentImpl.hpp34
-rw-r--r--src/brussel.engine/EditorCommandPalette.cpp406
-rw-r--r--src/brussel.engine/EditorCommandPalette.hpp94
-rw-r--r--src/brussel.engine/EditorCore.hpp39
-rw-r--r--src/brussel.engine/EditorCorePrivate.cpp1171
-rw-r--r--src/brussel.engine/EditorCorePrivate.hpp136
-rw-r--r--src/brussel.engine/EditorUtils.cpp447
-rw-r--r--src/brussel.engine/EditorUtils.hpp84
-rw-r--r--src/brussel.engine/EditorWorldGuides.cpp26
-rw-r--r--src/brussel.engine/EditorWorldGuides.hpp16
-rw-r--r--src/brussel.engine/FuzzyMatch.cpp174
-rw-r--r--src/brussel.engine/FuzzyMatch.hpp10
-rw-r--r--src/brussel.engine/GameObject.cpp230
-rw-r--r--src/brussel.engine/GameObject.hpp107
-rw-r--r--src/brussel.engine/GraphicsTags.cpp273
-rw-r--r--src/brussel.engine/GraphicsTags.hpp111
-rw-r--r--src/brussel.engine/Image.cpp101
-rw-r--r--src/brussel.engine/Image.hpp38
-rw-r--r--src/brussel.engine/Input.cpp23
-rw-r--r--src/brussel.engine/Input.hpp23
-rw-r--r--src/brussel.engine/Ires.cpp436
-rw-r--r--src/brussel.engine/Ires.hpp130
-rw-r--r--src/brussel.engine/Level.cpp228
-rw-r--r--src/brussel.engine/Level.hpp95
-rw-r--r--src/brussel.engine/Material.cpp526
-rw-r--r--src/brussel.engine/Material.hpp125
-rw-r--r--src/brussel.engine/Mesh.cpp54
-rw-r--r--src/brussel.engine/Mesh.hpp45
-rw-r--r--src/brussel.engine/Player.cpp139
-rw-r--r--src/brussel.engine/Player.hpp59
-rw-r--r--src/brussel.engine/Renderer.cpp256
-rw-r--r--src/brussel.engine/Renderer.hpp92
-rw-r--r--src/brussel.engine/SceneThings.cpp142
-rw-r--r--src/brussel.engine/SceneThings.hpp52
-rw-r--r--src/brussel.engine/Shader.cpp711
-rw-r--r--src/brussel.engine/Shader.hpp180
-rw-r--r--src/brussel.engine/Sprite.cpp328
-rw-r--r--src/brussel.engine/Sprite.hpp111
-rw-r--r--src/brussel.engine/Texture.cpp250
-rw-r--r--src/brussel.engine/Texture.hpp99
-rw-r--r--src/brussel.engine/VertexIndex.cpp84
-rw-r--r--src/brussel.engine/VertexIndex.hpp67
-rw-r--r--src/brussel.engine/World.cpp79
-rw-r--r--src/brussel.engine/World.hpp25
-rw-r--r--src/brussel.engine/main.cpp545
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;
+}