From 4a624cfe70ce7e373e92ba3d3233970fe2a9e4ed Mon Sep 17 00:00:00 2001 From: Erris Date: Tue, 3 Mar 2026 08:51:02 +0100 Subject: [PATCH] adding materials, adding serialization for meshes and materials, random fixes --- editor/include/editor.hpp | 8 ++- editor/include/editor_component.hpp | 1 + editor/include/panels/content_browser.hpp | 1 + editor/src/editor_component.cpp | 65 +++++++++++++++++- editor/src/panels/content_browser.cpp | 28 +++++--- editor/src/panels/scene_hierarchy.cpp | 6 ++ .../open_engine/renderer/renderer3d.hpp | 20 +++++- .../include/open_engine/scene/components.hpp | 15 +++- .../include/open_engine/scene/scene.hpp | 2 +- .../src/open_engine/renderer/renderer3d.cpp | 10 ++- open_engine/src/open_engine/scene/scene.cpp | 26 +++++-- .../open_engine/scene/scene_serializer.cpp | 62 +++++++++++++++++ resources/textures/icons/file.png | Bin 0 -> 2153 bytes resources/textures/icons/folder2.png | Bin 0 -> 2212 bytes resources/textures/icons/play.png | Bin 320 -> 2784 bytes resources/textures/icons/stop.png | Bin 156 -> 1727 bytes 16 files changed, 222 insertions(+), 22 deletions(-) mode change 100644 => 100755 editor/src/panels/content_browser.cpp create mode 100644 resources/textures/icons/file.png create mode 100644 resources/textures/icons/folder2.png diff --git a/editor/include/editor.hpp b/editor/include/editor.hpp index 0820057..982ce97 100755 --- a/editor/include/editor.hpp +++ b/editor/include/editor.hpp @@ -1,7 +1,6 @@ #ifndef EDITOR_HPP #define EDITOR_HPP -#include #include #include "open_engine/renderer/renderer3d.hpp" @@ -92,9 +91,11 @@ namespace OpenEngine { editor_camera = EditorCamera(30.0f, 1920.0f/1080.0f, 0.1f, 1000.0f); + // TODO: Add license icons["play"] = Texture2D::Create("resources/textures/icons/play.png"); icons["stop"] = Texture2D::Create("resources/textures/icons/stop.png"); + // ============================================= Entity cube = scene->CreateEntity("cube"); @@ -104,6 +105,7 @@ namespace OpenEngine { cube.AddComponent(mesh); + // ============================================= Entity quad = scene->CreateEntity("quad"); quad.AddComponent(); quad.GetComponents().translation = {0, 0, 0}; @@ -112,6 +114,7 @@ namespace OpenEngine { quad.AddComponent(quad_mesh); + // ============================================= Entity cube2 = scene->CreateEntity("cube2"); cube2.AddComponent(); cube2.GetComponents().translation = {2, 0, 0}; @@ -129,7 +132,6 @@ namespace OpenEngine { } */ - // TODO: Add file texture. Get free icons and add license //icons["folder"] = Texture2D::Create("resources/textures/icons/folder.png"); /* for (float i = 0; i < 200; i++) { @@ -352,7 +354,7 @@ namespace OpenEngine { bool was_focused = viewport_focused; viewport_focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow); viewport_hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow); - Application::Get().GetImGuiLayer()->SetBlockEvents((!viewport_focused && !viewport_hovered) || ImGui::IsAnyItemActive()); + Application::Get().GetImGuiLayer()->SetBlockEvents(ImGui::IsAnyItemActive()); if (viewport_focused && !was_focused) editor_camera.ResetMousePosition(); diff --git a/editor/include/editor_component.hpp b/editor/include/editor_component.hpp index bc5623e..c9fd455 100644 --- a/editor/include/editor_component.hpp +++ b/editor/include/editor_component.hpp @@ -17,6 +17,7 @@ namespace OpenEngine { void SpriteOnImGuiRender(entt::registry& registry, entt::entity entity); void CameraOnImGuiRender(entt::registry& registry, entt::entity entity); void MeshOnImGuiRender(entt::registry®istry, entt::entity entity); + void MaterialOnImGuiRender(entt::registry& registry, entt::entity entity); } #endif // EDITOR_COMPONENT_HPP diff --git a/editor/include/panels/content_browser.hpp b/editor/include/panels/content_browser.hpp index f68ca4d..72a9fc6 100644 --- a/editor/include/panels/content_browser.hpp +++ b/editor/include/panels/content_browser.hpp @@ -18,6 +18,7 @@ namespace OpenEngine { private: std::filesystem::path current_directory; Ref folder_icon; + Ref file_icon; }; } diff --git a/editor/src/editor_component.cpp b/editor/src/editor_component.cpp index 051f377..68de901 100644 --- a/editor/src/editor_component.cpp +++ b/editor/src/editor_component.cpp @@ -1,5 +1,9 @@ #include "imgui.h" +#include "open_engine/renderer/renderer3d.hpp" +#include "open_engine/scene/components.hpp" #include +#include +#include #include namespace OpenEngine { @@ -101,6 +105,7 @@ namespace OpenEngine { DrawVec3Control("Rotation", rotation_comp); transform.rotation = glm::radians(rotation_comp); DrawVec3Control("Scale", transform.scale); + ImGui::Spacing(); }; void SpriteOnImGuiRender(entt::registry& registry, entt::entity entity) @@ -171,8 +176,66 @@ namespace OpenEngine { }; - void MeshOnImGuiRender(entt::registry®istry, entt::entity entity) + void MeshOnImGuiRender(entt::registry& registry, entt::entity entity) { + auto& mesh_component = registry.get(entity); + const char* items[] = { "Quad", "Cube" }; + static int item_selected_idx = 0; + + if (ImGui::BeginCombo("Mesh", items[item_selected_idx])) { + for (int n = 0; n < 2; n++) + { + const bool is_selected = (item_selected_idx == n); + if (ImGui::Selectable(items[n], is_selected)) + item_selected_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + + if ((int)mesh_component.primitive_type == (item_selected_idx + 1)) + return; + + mesh_component.primitive_type = (PrimitiveType)(item_selected_idx + 1); + switch (mesh_component.primitive_type) { + case OpenEngine::PrimitiveType::Quad: + { + mesh_component.mesh = CreateQuad((uint32_t)entity); + break; + } + case OpenEngine::PrimitiveType::Cube: + { + mesh_component.mesh = CreateCube((uint32_t)entity); + break; + } + default: + break; + } + } + } + + void MaterialOnImGuiRender(entt::registry& registry, entt::entity entity) + { + auto& material = registry.get(entity).material; + + auto& albedo = material.albedo; + auto& roughness = material.roughness; + auto& metallic = material.metallic; + auto& ambient = material.ambient_strength; + auto& specular = material.specular_strength; + + if (ImGui::SliderFloat4("Albedo", glm::value_ptr(albedo), 0, 1)) + material.albedo = albedo; + if (ImGui::SliderFloat("Roughness", &roughness, 0, 1)) + material.roughness = roughness; + if (ImGui::SliderFloat("Metallic", &metallic, 0, 1)) + material.metallic = metallic; + if (ImGui::SliderFloat("Ambient strength", &ambient, 0, 1)) + material.ambient_strength = ambient; + if (ImGui::SliderFloat("Specular strength", &specular, 0, 10)) + material.specular_strength = specular; } } diff --git a/editor/src/panels/content_browser.cpp b/editor/src/panels/content_browser.cpp old mode 100644 new mode 100755 index acb26cd..7d7c056 --- a/editor/src/panels/content_browser.cpp +++ b/editor/src/panels/content_browser.cpp @@ -15,8 +15,9 @@ namespace OpenEngine { ContentBrowserPanel::ContentBrowserPanel() : current_directory(assets_directory) { - // TODO: Add file texture. Get free icons and add license - folder_icon = Texture2D::Create("resources/textures/icons/folder.png"); + // TODO: Add license + folder_icon = Texture2D::Create("resources/textures/icons/folder2.png"); + file_icon = Texture2D::Create("resources/textures/icons/file.png"); } void ContentBrowserPanel::OnImGuiRender() @@ -30,6 +31,16 @@ namespace OpenEngine { current_directory = current_directory.parent_path(); auto directory_it = std::filesystem::directory_iterator(current_directory); + std::vector entries; + + for (auto entry : directory_it) + entries.emplace_back(entry); + + std::sort(entries.begin(), entries.end(), + [](const std::filesystem::directory_entry& a, + const std::filesystem::directory_entry& b) { + return (a.is_directory() && !b.is_directory()); + }); ImVec2 button_size = { 100, 100 }; auto panel_width = ImGui::GetContentRegionAvail().x; @@ -42,9 +53,12 @@ namespace OpenEngine { ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, padding); + float margin = 20.0f; + ImGui::Indent(margin); + if (ImGui::BeginTable("table1", table_columns, ImGuiTableFlags_SizingFixedSame)) { - for (auto& entry : directory_it) { + for (auto& entry : entries) { auto file_name = entry.path().filename().string(); ImGui::PushID(file_name.c_str()); @@ -56,17 +70,14 @@ namespace OpenEngine { ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0.270f, 0.278f, 0.352f, 1.0f }); ImGui::BeginGroup(); - ImGui::ImageButton("##X", (ImTextureID)folder_icon->GetID(), button_size, { 0, 1 }, { 1, 0 }); + Ref icon = entry.is_directory() ? folder_icon : file_icon; + ImGui::ImageButton("##X", (ImTextureID)icon->GetID(), button_size, { 0, 1 }, { 1, 0 }); if (entry.is_regular_file() && ImGui::BeginDragDropSource()) { const char* source = entry.path().c_str(); ImGui::SetDragDropPayload("CONTENT_BROWSER_PAYLOAD", source, (strlen(source) + 1) * sizeof(char), ImGuiCond_Once); ImGui::EndDragDropSource(); } - - float columnWidth = ImGui::GetColumnWidth(); - float textWidth = ImGui::CalcTextSize(file_name.c_str()).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (columnWidth - textWidth) * 0.5f); ImGui::TextWrapped("%s", file_name.c_str()); ImGui::EndGroup(); @@ -81,6 +92,7 @@ namespace OpenEngine { } ImGui::EndTable(); } + ImGui::Unindent(); ImGui::PopStyleVar(); ImGui::End(); diff --git a/editor/src/panels/scene_hierarchy.cpp b/editor/src/panels/scene_hierarchy.cpp index f00d60e..0c716ad 100644 --- a/editor/src/panels/scene_hierarchy.cpp +++ b/editor/src/panels/scene_hierarchy.cpp @@ -23,6 +23,7 @@ namespace OpenEngine { RegisterDrawer("Sprite Renderer", &SpriteOnImGuiRender); RegisterDrawer("Camera", &CameraOnImGuiRender); RegisterDrawer("Mesh", &MeshOnImGuiRender); + RegisterDrawer("Material", &MaterialOnImGuiRender); scene = context; selected_context = {}; @@ -51,6 +52,11 @@ namespace OpenEngine { selected_context.AddComponent(); ImGui::CloseCurrentPopup(); } + if (!selected_context.HasComponent()) + if (ImGui::MenuItem("Material")) { + selected_context.AddComponent(); + ImGui::CloseCurrentPopup(); + } } void EntityPopup(Ref& scene) diff --git a/open_engine/include/open_engine/renderer/renderer3d.hpp b/open_engine/include/open_engine/renderer/renderer3d.hpp index c79cd39..2ee90df 100644 --- a/open_engine/include/open_engine/renderer/renderer3d.hpp +++ b/open_engine/include/open_engine/renderer/renderer3d.hpp @@ -10,6 +10,14 @@ namespace OpenEngine { // SHOULD BE MOVED ================================== + + enum class PrimitiveType + { + None = 0, + Quad, + Cube + }; + struct MeshVertex { glm::vec3 position; @@ -19,6 +27,15 @@ namespace OpenEngine { uint32_t id = -1; }; + struct Material + { + glm::vec4 albedo = { 1.0f, 1.0f, 1.0f, 1.0f }; + float roughness = 1.0f; + float metallic = 1.0f; + float ambient_strength = 1.0f; + float specular_strength = 1.0f; + }; + struct Mesh { Ref vertex_array; @@ -46,7 +63,8 @@ namespace OpenEngine { static void BeginScene(const EditorCamera& camera); static void EndScene(); - static void DrawMesh(const Ref& mesh, const glm::mat4& transform); + static void DrawMesh(const Ref& mesh, Material& material, + const glm::mat4& transform); }; } diff --git a/open_engine/include/open_engine/scene/components.hpp b/open_engine/include/open_engine/scene/components.hpp index b1e55f1..aae8457 100644 --- a/open_engine/include/open_engine/scene/components.hpp +++ b/open_engine/include/open_engine/scene/components.hpp @@ -86,17 +86,28 @@ namespace OpenEngine { (delete nsc->instance); nsc->instance = nullptr; }; - } + }; }; struct MeshComponent { Ref mesh; + PrimitiveType primitive_type = PrimitiveType::None; MeshComponent() = default; MeshComponent(const MeshComponent&) = default; MeshComponent(const Ref& mesh) - : mesh(mesh) {} + : mesh(mesh) {}; + }; + + struct MaterialComponent + { + Material material; + + MaterialComponent() = default; + MaterialComponent(const MaterialComponent&) = default; + MaterialComponent(const Material& material) + : material(material) {}; }; } diff --git a/open_engine/include/open_engine/scene/scene.hpp b/open_engine/include/open_engine/scene/scene.hpp index cf299a8..a4cb2cc 100644 --- a/open_engine/include/open_engine/scene/scene.hpp +++ b/open_engine/include/open_engine/scene/scene.hpp @@ -40,7 +40,7 @@ namespace OpenEngine { uint32_t viewport_width = 0, viewport_height = 0; - std::vector entities_to_be_deleted; + std::vector pending_deletion; friend class SceneSerializer; friend class Entity; diff --git a/open_engine/src/open_engine/renderer/renderer3d.cpp b/open_engine/src/open_engine/renderer/renderer3d.cpp index cc14723..58120ba 100644 --- a/open_engine/src/open_engine/renderer/renderer3d.cpp +++ b/open_engine/src/open_engine/renderer/renderer3d.cpp @@ -176,6 +176,9 @@ namespace OpenEngine { }; TransformData transform_buffer; Ref transform_uniform_buffer; + + Material material_buffer; + Ref material_uniform_buffer; }; static Renderer3DData renderer_data; @@ -190,6 +193,7 @@ namespace OpenEngine { renderer_data.color_shader_3d = Shader::Create("assets/shaders/color3d.glsl"); renderer_data.camera_uniform_buffer = UniformBuffer::Create(sizeof(Renderer3DData::CameraData), 0); renderer_data.transform_uniform_buffer = UniformBuffer::Create(sizeof(Renderer3DData::TransformData), 1); + renderer_data.material_uniform_buffer = UniformBuffer::Create(sizeof(Material), 2); glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); @@ -230,7 +234,8 @@ namespace OpenEngine { } - void Renderer3D::DrawMesh(const Ref& mesh, const glm::mat4& transform) + void Renderer3D::DrawMesh(const Ref& mesh, Material& material, + const glm::mat4& transform) { OE_PROFILE_FUNCTION(); @@ -239,6 +244,9 @@ namespace OpenEngine { renderer_data.transform_buffer.transform = transform; renderer_data.transform_uniform_buffer->SetData(&renderer_data.transform_buffer, sizeof(Renderer3DData::TransformData)); + renderer_data.material_buffer = material; + renderer_data.material_uniform_buffer->SetData(&renderer_data.material_buffer, sizeof(Material)); + mesh->vertex_array->Bind(); RenderCommand::DrawIndexed(mesh->vertex_array, mesh->indices.size()); diff --git a/open_engine/src/open_engine/scene/scene.cpp b/open_engine/src/open_engine/scene/scene.cpp index d6a114e..5fe217d 100644 --- a/open_engine/src/open_engine/scene/scene.cpp +++ b/open_engine/src/open_engine/scene/scene.cpp @@ -26,15 +26,15 @@ namespace OpenEngine { void Scene::MarkEntityForDeletion(Entity entity) { - entities_to_be_deleted.emplace_back(entity); + pending_deletion.emplace_back(entity); } void Scene::UpdateEntities() { - for (auto& entity : entities_to_be_deleted) + for (auto& entity : pending_deletion) DeleteEntity(entity); - entities_to_be_deleted.clear(); + pending_deletion.clear(); } void Scene::OnUpdateRuntime() @@ -75,8 +75,13 @@ namespace OpenEngine { for (const auto& entity : view) { auto [transform, mesh] = view.get(entity); + Material material; - Renderer3D::DrawMesh(mesh.mesh, GetTransformFromComp(transform)); + Entity _entity(entity, this); + if (_entity.HasComponent()) + material = _entity.GetComponents().material; + + Renderer3D::DrawMesh(mesh.mesh, material, GetTransformFromComp(transform)); /* if (sprite.texture) Renderer2D::DrawQuad(GetTransformFromComp(transform), @@ -101,7 +106,13 @@ namespace OpenEngine { for (const auto& entity : view) { auto [transform, mesh] = view.get(entity); - Renderer3D::DrawMesh(mesh.mesh, GetTransformFromComp(transform)); + Material material; + + Entity _entity(entity, this); + if (_entity.HasComponent()) + material = _entity.GetComponents().material; + + Renderer3D::DrawMesh(mesh.mesh, material, GetTransformFromComp(transform)); } Renderer3D::EndScene(); @@ -170,4 +181,9 @@ namespace OpenEngine { void Scene::OnComponentAdded(Entity entity, MeshComponent& component) { } + + template<> + void Scene::OnComponentAdded(Entity entity, MaterialComponent& component) + { + } } diff --git a/open_engine/src/open_engine/scene/scene_serializer.cpp b/open_engine/src/open_engine/scene/scene_serializer.cpp index 270273e..26600b5 100644 --- a/open_engine/src/open_engine/scene/scene_serializer.cpp +++ b/open_engine/src/open_engine/scene/scene_serializer.cpp @@ -1,7 +1,9 @@ +#include #include #include "core.hpp" #include +#include "renderer/renderer3d.hpp" #include "scene/components.hpp" #include "scene/entity.hpp" @@ -149,6 +151,32 @@ namespace OpenEngine { out << YAML::EndMap; // SpriteRendererComponent } + if (entity.HasComponent()) + { + out << YAML::Key << "MeshComponent"; + out << YAML::BeginMap; // MeshComponent + + auto& mesh_component = entity.GetComponents(); + out << YAML::Key << "MeshType" << YAML::Value << (int)mesh_component.primitive_type; + + out << YAML::EndMap; // MeshComponent + } + + if (entity.HasComponent()) + { + out << YAML::Key << "MaterialComponent"; + out << YAML::BeginMap; // MaterialComponent + + auto& material = entity.GetComponents().material; + out << YAML::Key << "Albedo" << YAML::Value << material.albedo; + out << YAML::Key << "Roughness" << YAML::Value << material.roughness; + out << YAML::Key << "Metalic" << YAML::Value << material.metallic; + out << YAML::Key << "AmbiantStrength" << YAML::Value << material.ambient_strength; + out << YAML::Key << "SpecularStrength" << YAML::Value << material.specular_strength; + + out << YAML::EndMap; // MaterialComponent + } + out << YAML::EndMap; } @@ -241,6 +269,40 @@ namespace OpenEngine { auto& src = deserializedEntity.AddComponent(); src.color = spriteRendererComponent["Color"].as(); } + + auto mesh_component = entity["MeshComponent"]; + if (mesh_component) + { + auto& mesh = deserializedEntity.AddComponent(); + switch (mesh.primitive_type) { + case OpenEngine::PrimitiveType::Quad: + { + mesh.mesh = CreateQuad((uint32_t)deserializedEntity); + break; + } + case OpenEngine::PrimitiveType::Cube: + { + mesh.mesh = CreateCube((uint32_t)deserializedEntity); + break; + } + default: + break; + } + } + + auto material_component = entity["MaterialComponent"]; + if (material_component) + { + auto& material = deserializedEntity.AddComponent(); + material.material.albedo = material_component["Albedo"].as(); + material.material.roughness = material_component["Roughness"].as(); + material.material.metallic = material_component["Metalic"].as(); + material.material.ambient_strength = material_component["AmbiantStrength"].as(); + material.material.specular_strength = material_component["SpecularStrength"].as(); + } + + auto mesh = CreateCube((uint32_t)deserializedEntity); + deserializedEntity.AddComponent(mesh); } } diff --git a/resources/textures/icons/file.png b/resources/textures/icons/file.png new file mode 100644 index 0000000000000000000000000000000000000000..4c885f7c11be50e741f31ba58becf8dc59b822da GIT binary patch literal 2153 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 literal 0 HcmV?d00001 diff --git a/resources/textures/icons/play.png b/resources/textures/icons/play.png index 274b12ad7df072565455f6728fe37f60d1034c9a..f9058d9c363453afbfb247d9ad6f2426dacde475 100644 GIT binary patch literal 2784 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 Ar~m)}