From 46b96729efbce684a5e4d30a2c8e18aed94cfd19 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Wed, 26 Nov 2025 06:54:46 +0100 Subject: [PATCH 01/27] W.I.P. --- .../assets/shaders/common/light_uniforms.glsl | 2 + editor/assets/shaders/phong.frag | 36 +- editor/assets/shaders/shadow.frag | 5 + editor/assets/shaders/shadow.vert | 9 + .../src/UI/Panels/Viewport/ViewportPanel.cpp | 4 +- engine/docs/new_project_architecture.md | 76 ++++ engine/src/hellfire/ecs/LightComponent.h | 60 ++- .../graphics/backends/opengl/Framebuffer.cpp | 2 +- .../graphics/lighting/DirectionalLight.cpp | 22 + .../hellfire/graphics/renderer/Renderer.cpp | 406 +++++++++++------- .../src/hellfire/graphics/renderer/Renderer.h | 97 +++-- sandbox/include/GamePlugin.h | 2 +- sandbox/src/main.cpp | 4 +- 13 files changed, 527 insertions(+), 198 deletions(-) create mode 100644 editor/assets/shaders/shadow.frag create mode 100644 editor/assets/shaders/shadow.vert create mode 100644 engine/docs/new_project_architecture.md diff --git a/editor/assets/shaders/common/light_uniforms.glsl b/editor/assets/shaders/common/light_uniforms.glsl index 4158116..ca0a8f1 100644 --- a/editor/assets/shaders/common/light_uniforms.glsl +++ b/editor/assets/shaders/common/light_uniforms.glsl @@ -19,4 +19,6 @@ uniform int numDirectionalLights; uniform int numPointLights; uniform DirectionalLight directionalLights[MAX_DIRECTIONAL_LIGHTS]; uniform PointLight pointLights[MAX_POINT_LIGHTS]; +uniform sampler2D uShadowMap[MAX_DIRECTIONAL_LIGHTS]; +uniform mat4 uLightSpaceMatrix[MAX_DIRECTIONAL_LIGHTS]; diff --git a/editor/assets/shaders/phong.frag b/editor/assets/shaders/phong.frag index d081d61..b3e47be 100644 --- a/editor/assets/shaders/phong.frag +++ b/editor/assets/shaders/phong.frag @@ -11,16 +11,44 @@ layout(location=1) out uint objectID; uniform uint uObjectID; +float calculate_shadow(int light_index, vec3 frag_pos) { + // Transform fragmenet position to light space + vec4 frag_pos_light_space = uLightSpaceMatrix[light_index] * vec4(frag_pos, 1.0); + + // Perspective divide + vec3 proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; + + // Transform to [0,1] range for depth map sampling + proj_coords = proj_coords * 0.5 + 0.5; + + if (proj_coords.z > 1.0) return 0.0; + + float closest_depth = texture(uShadowMap[light_index], proj_coords.xy).r; + + // Get depth of current fragment from light's perspective + float current_depth = proj_coords.z; + + // Bias to prevent shadow acne (object's casting shadows onto itself) + float bias = 0.001; + float shadow = current_depth - bias > closest_depth ? 1.0 : 0.0; + + return shadow; +} + void main() { - // Sample base textures vec4 diffuseValue = sampleDiffuseTexture(fs_in.TexCoords); vec4 baseColor = applyVertexColors(diffuseValue, fs_in.Color); - - // Calculate surface normal vec3 normal = calculateSurfaceNormal(fs_in.TexCoords, fs_in.Normal, fs_in.TBN); - // Calculate lighting vec3 result = calculateBlinnPhongLighting(normal, baseColor.rgb, fs_in.FragPos); + float shadow_factor = 0.0; + if (numDirectionalLights > 0) { + shadow_factor = calculate_shadow(0, fs_in.FragPos); + } + + float ambient_strength = 0.2; + result *= (ambient_strength + (1.0 - ambient_strength) * (1.0 - shadow_factor)); + fragColor = vec4(result, uOpacity); objectID = uObjectID; } \ No newline at end of file diff --git a/editor/assets/shaders/shadow.frag b/editor/assets/shaders/shadow.frag new file mode 100644 index 0000000..726309e --- /dev/null +++ b/editor/assets/shaders/shadow.frag @@ -0,0 +1,5 @@ +#version 430 core + +void main() { + gl_FragDepth = gl_FragCoord.z; +} \ No newline at end of file diff --git a/editor/assets/shaders/shadow.vert b/editor/assets/shaders/shadow.vert new file mode 100644 index 0000000..a997441 --- /dev/null +++ b/editor/assets/shaders/shadow.vert @@ -0,0 +1,9 @@ +#version 430 core +layout (location = 0) in vec3 aPos; + +uniform mat4 uLightViewProjMatrix; +uniform mat4 uModelMatrix; + +void main() { + gl_Position = uLightViewProjMatrix * uModelMatrix * vec4(aPos, 1.0); +} \ No newline at end of file diff --git a/editor/src/UI/Panels/Viewport/ViewportPanel.cpp b/editor/src/UI/Panels/Viewport/ViewportPanel.cpp index d841b97..2ab2ece 100644 --- a/editor/src/UI/Panels/Viewport/ViewportPanel.cpp +++ b/editor/src/UI/Panels/Viewport/ViewportPanel.cpp @@ -127,7 +127,7 @@ namespace hellfire::editor { if (constexpr float RESIZE_DELAY = 0.016f; (viewport_size.x != last_size.x || viewport_size.y != last_size.y) && (current_time - last_resize_time_) > RESIZE_DELAY) { - engine_renderer_->resize_scene_framebuffer( + engine_renderer_->resize_main_framebuffer( static_cast(viewport_size.x), static_cast(viewport_size.y) ); @@ -143,7 +143,7 @@ namespace hellfire::editor { } } // Render using editor camera - const uint32_t scene_texture = engine_renderer_->get_scene_texture(); + const uint32_t scene_texture = engine_renderer_->get_main_output_texture(); if (!context_->active_scene) { ImGui::SetCursorPos(ImVec2( diff --git a/engine/docs/new_project_architecture.md b/engine/docs/new_project_architecture.md new file mode 100644 index 0000000..8753f79 --- /dev/null +++ b/engine/docs/new_project_architecture.md @@ -0,0 +1,76 @@ +```mermaid +erDiagram +GAMES ||--o{ GAMES_VENDORS_JOINT : has +VENDORS ||--o{ GAMES_VENDORS_JOINT : has +GAMES ||--o{ GAMES_GENRE_JOINT : has +GENRE ||--o{ GAMES_GENRE_JOINT : has +GAMES ||--o{ CHANGE_REQUESTS : has +GAMES ||--o{ GAMES_EDITORS : has +USER ||--o{ GAMES_EDITORS : has +USER ||--o{ CHANGE_REQUESTS : "requested by" +USER ||--o{ CHANGE_REQUESTS : "reviewed by" + + GAMES { + text Id PK + text Title + text Description + date ReleaseDate "Nullable" + bool IsReleased "Default false" + binary Logo + binary CoverBackground + datetime CreatedAt + datetime UpdatedAt + } + + VENDORS { + text Id PK + text Name + text Description + binary Logo + } + + GAMES_VENDORS_JOINT { + text Id PK + text VendorsId FK + text GamesId FK + } + + GENRE { + text Id PK + text Name + } + + GAMES_GENRE_JOINT { + text Id PK + text GamesId FK + text GenreId FK + } + + GAMES_EDITORS { + text Id PK + text GamesId FK + text UserId FK + datetime AssignedAt + bool IsActive "Default true" + } + + USER { + text Id PK "OutSystems User.Id" + text Username + text Email + text Role "Admin, Editor, User" + } + + CHANGE_REQUESTS { + text Id PK + text GamesId FK "Nullable - new games" + text RequestedByUserId FK + text Status "Pending, Approved, Rejected" + text ChangeType "Create, Update, Delete" + text ProposedChanges "JSON" + datetime CreatedAt + datetime ReviewedAt "Nullable" + text ReviewedByUserId FK "Nullable" + text ReviewNotes "Nullable" + } +``` \ No newline at end of file diff --git a/engine/src/hellfire/ecs/LightComponent.h b/engine/src/hellfire/ecs/LightComponent.h index f199241..b9c9d93 100644 --- a/engine/src/hellfire/ecs/LightComponent.h +++ b/engine/src/hellfire/ecs/LightComponent.h @@ -11,8 +11,14 @@ #include "hellfire/graphics/shader/Shader.h" #include "Component.h" #include "Entity.h" +#include "hellfire/graphics/backends/opengl/Framebuffer.h" namespace hellfire { + struct ShadowMapData { + std::unique_ptr framebuffer; + glm::mat4 light_view_proj; + }; + class LightComponent : public Component { public: enum LightType { @@ -38,6 +44,8 @@ namespace hellfire { float inner_cone_angle_ = 30.0f; float outer_cone_angle_ = 45.0f; + glm::mat4 light_view_proj_matrix = glm::mat4(1.0f); + // Shadow mapping properties bool cast_shadows_ = true; public: @@ -129,10 +137,60 @@ namespace hellfire { } } + const glm::mat4& get_light_view_proj_matrix() const { return light_view_proj_matrix; } + void set_light_view_proj_matrix(const glm::mat4& lvpm) { light_view_proj_matrix = lvpm; } + + void compute_directional_light_matrix() { + // For directional lights, use orthographic projection + // covering the scene bounds + const float ortho_size = 10.0f; + const float near_plane = 0.1f; + const float far_plane = 512.0f; + + glm::mat4 light_projection = glm::ortho(-ortho_size, ortho_size, + -ortho_size, ortho_size, near_plane, far_plane); + + glm::vec3 light_pos = -get_direction() * 10.0f; // Position light "backwards" along direction + glm::vec3 target = glm::vec3(0.0f); // Look at scene center + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); + + // If direction is too close to up vector, use different up + if (glm::abs(glm::dot(glm::normalize(get_direction()), up)) > 0.99f) { + up = glm::vec3(1.0f, 0.0f, 0.0f); + } + + glm::mat4 light_view = glm::lookAt(light_pos, target, up); + + light_view_proj_matrix = light_projection * light_view; + } + // Factory methods for convenience static LightComponent *create_directional(const glm::vec3 &direction = glm::vec3(0.0f, -1.0f, 0.0f)) { - auto *light = new LightComponent(DIRECTIONAL); + auto* light = new LightComponent(DIRECTIONAL); light->set_direction(direction); + + // Much larger coverage to include your plane + float ortho_size = 50.0f; // Increased from 10 to 50 + float near_plane = 0.1f; + float far_plane = 100.0f; // Increased from 7.5 to 100 + + glm::mat4 light_projection = glm::ortho( + -ortho_size, ortho_size, + -ortho_size, ortho_size, + near_plane, far_plane + ); + + // Position light higher and further back to see more of the scene + glm::vec3 light_pos = glm::vec3(0.0f, 20.0f, 10.0f); + glm::vec3 look_at = glm::vec3(0.0f, 0.0f, 0.0f); + + glm::mat4 light_view = glm::lookAt( + light_pos, + look_at, + glm::vec3(0.0f, 0.0f, -1.0f) // Up vector + ); + + light->light_view_proj_matrix = light_projection * light_view; return light; } diff --git a/engine/src/hellfire/graphics/backends/opengl/Framebuffer.cpp b/engine/src/hellfire/graphics/backends/opengl/Framebuffer.cpp index 949c99f..41863f8 100644 --- a/engine/src/hellfire/graphics/backends/opengl/Framebuffer.cpp +++ b/engine/src/hellfire/graphics/backends/opengl/Framebuffer.cpp @@ -70,7 +70,7 @@ namespace hellfire { // Override format settings for depth texture const GLenum internal_format = settings.internal_format == GL_RGBA8 - ? GL_DEPTH_COMPONENT24 + ? GL_DEPTH_COMPONENT32F : settings.internal_format; constexpr GLenum format = GL_DEPTH_COMPONENT; const GLenum type = settings.type == GL_UNSIGNED_BYTE ? GL_FLOAT : settings.type; diff --git a/engine/src/hellfire/graphics/lighting/DirectionalLight.cpp b/engine/src/hellfire/graphics/lighting/DirectionalLight.cpp index 93a29b1..454d9bd 100644 --- a/engine/src/hellfire/graphics/lighting/DirectionalLight.cpp +++ b/engine/src/hellfire/graphics/lighting/DirectionalLight.cpp @@ -15,6 +15,28 @@ namespace hellfire { light->set_direction(direction); light->set_color(color); light->set_intensity(intensity); + + // Much larger coverage to include your plane + float ortho_size = 50.0f; + float near_plane = 0.1f; + float far_plane = 100.0f; + + glm::mat4 light_projection = glm::ortho( + -ortho_size, ortho_size, + -ortho_size, ortho_size, + near_plane, far_plane + ); + + glm::vec3 light_pos = glm::vec3(0.0f, 20.0f, 10.0f); + glm::vec3 look_at = glm::vec3(0.0f, 0.0f, 0.0f); + + glm::mat4 light_view = glm::lookAt( + light_pos, + look_at, + glm::vec3(0.0f, 0.0f, -1.0f) // Up vector + ); + + light->set_light_view_proj_matrix(light_projection * light_view); return id; } diff --git a/engine/src/hellfire/graphics/renderer/Renderer.cpp b/engine/src/hellfire/graphics/renderer/Renderer.cpp index e7382e2..b450c7e 100644 --- a/engine/src/hellfire/graphics/renderer/Renderer.cpp +++ b/engine/src/hellfire/graphics/renderer/Renderer.cpp @@ -27,24 +27,28 @@ namespace hellfire { // Enable debugging for OpenGL glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); // Makes errors appear immediately - glDebugMessageCallback([](GLenum source, GLenum type, GLuint id, - GLenum severity, GLsizei length, - const GLchar* message, const void* userParam) { + glDebugMessageCallback([](GLenum source, GLenum type, GLuint id, + GLenum severity, GLsizei length, + const GLchar *message, const void *userParam) { if (type == GL_DEBUG_TYPE_ERROR) { std::cerr << "GL ERROR: " << message << std::endl; - __debugbreak(); + __debugbreak(); } }, nullptr); + + // Setup shadow pass shader + shadow_material_ = MaterialBuilder::create_custom("Shadow Material", "assets/shaders/shadow.vert", + "assets/shaders/shadow.frag"); + skybox_renderer_.initialize(); } - void Renderer::render(Scene &scene, const Entity* camera_override = nullptr) { - const Entity* camera_entity = camera_override; + void Renderer::render(Scene &scene, const Entity *camera_override = nullptr) { + const Entity *camera_entity = camera_override; if (!camera_entity) { const EntityID camera_id = scene.get_default_camera_entity_id(); camera_entity = scene.get_entity(camera_id); - } if (!camera_entity) { @@ -58,13 +62,7 @@ namespace hellfire { std::cerr << "Camera entity missing CameraComponent" << std::endl; } - if (render_to_framebuffer_) { - render_scene_to_framebuffer(scene, *camera_comp); - } else { - begin_frame(); - render_internal(scene, *camera_comp); - end_frame(); - } + render_frame(scene, *camera_comp); } void Renderer::reset_framebuffer_data() { @@ -72,7 +70,7 @@ namespace hellfire { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); } - void Renderer::clear_drawable_objects_list() { + void Renderer::clear_draw_list() { opaque_objects_.clear(); transparent_objects_.clear(); opaque_instanced_objects_.clear(); @@ -86,19 +84,14 @@ namespace hellfire { glDepthMask(GL_TRUE); glDepthFunc(GL_LESS); - clear_drawable_objects_list(); + clear_draw_list(); } void Renderer::end_frame() { glFlush(); } - Shader *Renderer::get_default_shader() const { - return fallback_shader_; - } - - - void Renderer::store_lights_in_context(const std::vector &light_entities, CameraComponent &camera) const { + void Renderer::store_lights_in_context(const std::vector &light_entities, CameraComponent &camera) { if (!context_) return; // Separate lights by type @@ -109,6 +102,7 @@ namespace hellfire { const auto *light = entity->get_component(); if (!light) continue; + // Sort lights into their respective vectors switch (light->get_light_type()) { case LightComponent::LightType::DIRECTIONAL: if (directional_lights.size() < 4) { @@ -143,38 +137,10 @@ namespace hellfire { context_->camera_component = &camera; } - void Renderer::render_internal(Scene &scene, CameraComponent &camera) { - clear_drawable_objects_list(); - // Store this for collect methods - scene_ = &scene; - - // Collect lights - const std::vector light_entity_ids = scene.find_entities_with_component(); - std::vector light_entities; - for (const EntityID id: light_entity_ids) { - if (Entity *e = scene.get_entity(id)) { - light_entities.push_back(e); - } - } - store_lights_in_context(light_entities, camera); - - // Get camera position - const auto *camera_transform = camera.get_owner().transform(); - const glm::vec3 camera_pos = camera_transform ? camera_transform->get_world_position() : glm::vec3(0.0f); - - // Collect render commands from root entities + void Renderer::collect_geometry_from_scene(Scene &scene, const glm::vec3 camera_pos) { for (const EntityID root_id: scene.get_root_entities()) { collect_render_commands_recursive(root_id, camera_pos); } - - // create_shadow_map(); - // Render - const glm::mat4 view = camera.get_view_matrix(); - const glm::mat4 projection = camera.get_projection_matrix(); - - render_opaque_pass(view, projection); - render_skybox_pass(&scene, view, projection, &camera); - render_transparent_pass(view, projection); } void Renderer::collect_render_commands_recursive(EntityID entity_id, const glm::vec3 &camera_pos) { @@ -231,81 +197,27 @@ namespace hellfire { } } - void Renderer::render_opaque_pass(const glm::mat4 &view, const glm::mat4 &projection) { - glEnable(GL_DEPTH_TEST); - glDepthMask(GL_TRUE); - glDepthFunc(GL_LESS); - glDisable(GL_BLEND); - - - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - glFrontFace(GL_CCW); - - glEnable(GL_STENCIL_TEST); - glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); - - std::ranges::sort(opaque_objects_, - [](const RenderCommand &a, const RenderCommand &b) { - return a.distance_to_camera < b.distance_to_camera; - }); - - for (const auto &cmd: opaque_objects_) { - draw_render_command(cmd, view, projection); - } - - for (const auto &cmd: opaque_instanced_objects_) { - draw_instanced_command(cmd, view, projection); + void Renderer::ensure_shadow_map(Entity *light_entity, const LightComponent &light) { + if (!shadow_maps_.contains(light_entity)) { + auto shadow_map = std::make_unique(); + FrameBufferAttachmentSettings settings; + settings.width = 1024; + settings.height = 1024; + + settings.min_filter = GL_NEAREST; + settings.mag_filter = GL_NEAREST; + settings.wrap_s = GL_CLAMP_TO_BORDER; + settings.wrap_t = GL_CLAMP_TO_BORDER; + shadow_map->attach_depth_texture(settings); + + glBindTexture(GL_TEXTURE_2D, shadow_map->get_depth_attachment()); + float border_color[] = { 1.0f, 1.0f, 1.0f, 1.0f }; + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color); + glBindTexture(GL_TEXTURE_2D, 0); + shadow_maps_[light_entity] = {std::move(shadow_map), glm::mat4(1.0f)}; } } - void Renderer::render_transparent_pass(const glm::mat4 &view, const glm::mat4 &projection) { - // Configure blending: enable for color input, disable for object ID output - glEnablei(GL_BLEND, 0); // Enable blending for fragColor (location 0) - glDisablei(GL_BLEND, 1); // Disable blending for objectID (location 1) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - // Sort the transparent objects from back-to-front relative to camera - // This ensures proper blending order between different objects - std::ranges::sort(transparent_objects_, - [](const RenderCommand &a, const RenderCommand &b) { - return a.distance_to_camera > b.distance_to_camera; - }); - - // Render non-instanced transparent objects with two-pass rendering - glDisable(GL_CULL_FACE); - for (const auto &cmd: transparent_objects_) { - // Pass 1: Draw back faces, to depth buffer - glCullFace(GL_FRONT); - glDepthMask(GL_TRUE); - glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(2.0f, 2.0f); - draw_render_command(cmd, view, projection); - glDisable(GL_POLYGON_OFFSET_FILL); - - // Pass 2: Draw front faces, don't write to depth buffer - glCullFace(GL_BACK); - glDepthMask(GL_FALSE); - draw_render_command(cmd, view, projection); - } - - // Render instanced transparent objects with two-pass rendering - for (const auto &cmd: transparent_instanced_objects_) { - // Pass 1: Draw back faces, to depth buffer - glCullFace(GL_FRONT); - glDepthMask(GL_TRUE); - draw_instanced_command(cmd, view, projection); - - // Pass 2: Draw front faces, don't write to depth buffer - glCullFace(GL_BACK); - glDepthMask(GL_FALSE); - draw_instanced_command(cmd, view, projection); - } - - // Restore depth writing - glDepthMask(GL_TRUE); - } - void Renderer::draw_render_command(const RenderCommand &cmd, const glm::mat4 &view, const glm::mat4 &projection) { const Entity *entity = scene_->get_entity(cmd.entity_id); if (!entity) return; @@ -313,12 +225,30 @@ namespace hellfire { const auto *transform = entity->get_component(); if (!transform) return; - Shader& shader = get_shader_for_material(cmd.material); + Shader &shader = get_shader_for_material(cmd.material); shader.use(); // Upload lights if (context_) { RenderingUtils::upload_lights_to_shader(shader, *context_); + + // Bind shadow maps for directional lights + for (int i = 0; i < context_->num_directional_lights; i++) { + Entity* light_entity = context_->directional_light_entities[i]; + + if (shadow_maps_.contains(light_entity)) { + auto& shadow_data = shadow_maps_[light_entity]; + + // Bind depth texture to texture unit + int texture_unit = 10 + i; // Start at unit 10 to avoid conflicts + glActiveTexture(GL_TEXTURE0 + texture_unit); + glBindTexture(GL_TEXTURE_2D, shadow_data.framebuffer->get_depth_attachment()); + + // Set shader uniforms + shader.set_int("uShadowMap[" + std::to_string(i) + "]", texture_unit); + shader.set_mat4("uLightSpaceMatrix[" + std::to_string(i) + "]", shadow_data.light_view_proj); + } + } } shader.set_vec3("uAmbientLight", scene_->environment()->get_ambient_light()); @@ -338,7 +268,7 @@ namespace hellfire { const glm::mat4 &projection) { if (const Entity *entity = scene_->get_entity(cmd.entity_id); !entity) return; - Shader& shader = get_shader_for_material(cmd.material); + Shader &shader = get_shader_for_material(cmd.material); shader.use(); // Upload light data as uniforms to shader @@ -366,8 +296,8 @@ namespace hellfire { cmd.material->unbind(); } - - void Renderer::render_skybox_pass(Scene *scene, const glm::mat4 &view, const glm::mat4 &projection, CameraComponent* camera_comp) const { + void Renderer::execute_skybox_pass(Scene *scene, const glm::mat4 &view, const glm::mat4 &projection, + CameraComponent *camera_comp) const { if (!scene || !scene->environment()->has_skybox()) return; glDisable(GL_CULL_FACE); @@ -377,7 +307,191 @@ namespace hellfire { } } - void Renderer::create_scene_framebuffer(uint32_t width, uint32_t height) { + + void Renderer::collect_lights_from_scene(Scene &scene, CameraComponent &camera) { + const std::vector light_entity_ids = scene.find_entities_with_component(); + std::vector light_entities; + for (const EntityID id: light_entity_ids) { + if (Entity *e = scene.get_entity(id)) { + light_entities.push_back(e); + } + } + store_lights_in_context(light_entities, camera); + } + + void Renderer::execute_main_pass(Scene &scene, CameraComponent &camera) { + clear_draw_list(); + scene_ = &scene; + + + // Gather lights and geometry + collect_lights_from_scene(scene, camera); + collect_geometry_from_scene(scene, camera.get_owner().transform()->get_position()); + + // Execute rendering passes + const glm::mat4 view = camera.get_view_matrix(); + const glm::mat4 projection = camera.get_projection_matrix(); + + execute_geometry_pass(view, projection); + execute_skybox_pass(&scene, view, projection, &camera); + execute_transparency_pass(view, projection); + } + + + + void Renderer::execute_shadow_passes(Scene &scene) { + // Gather all lights that cast shadows + const std::vector light_entity_ids = scene.find_entities_with_component(); + + std::vector shadow_casting_lights; + for (const EntityID id : light_entity_ids) { + Entity* entity = scene.get_entity(id); + if (!entity) continue; + + const auto* light = entity->get_component(); + if (light && light->should_cast_shadows()) { + shadow_casting_lights.push_back(entity); + } + } + + clear_draw_list(); + scene_ = &scene; + const glm::vec3 dummy_camera_pos(0.0f); // Distance doesn't matter for shadows + for (const EntityID root_id : scene.get_root_entities()) { + collect_render_commands_recursive(root_id, dummy_camera_pos); + } + + // Render each light's shadow map + for (Entity* light_entity : shadow_casting_lights) { + const auto* light = light_entity->get_component(); + if (!light) continue; + + ensure_shadow_map(light_entity, *light); + + auto& shadow_data = shadow_maps_[light_entity]; + shadow_data.framebuffer->bind(); + + glViewport(0, 0, 1024, 1024); + glClear(GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + glEnable(GL_CULL_FACE); + glCullFace(GL_FRONT); + + // Use light's view-projection matrix + const glm::mat4 light_view_proj = light->get_light_view_proj_matrix(); + shadow_data.light_view_proj = light_view_proj; // Store for main pass + + // Render geometry to depth texture + draw_shadow_geometry(light_view_proj); + + shadow_data.framebuffer->unbind(); + + glCullFace(GL_BACK); + } + glViewport(0, 0, framebuffer_width_, framebuffer_height_); + } + + void Renderer::draw_shadow_geometry(const glm::mat4 &light_view_proj) { + Shader& shadow_shader = get_shader_for_material(shadow_material_); + shadow_shader.use(); + shadow_shader.set_mat4("uLightViewProjMatrix", light_view_proj); + + shadow_material_->bind(); + + for (const auto &cmd : opaque_objects_) { + const Entity *entity = scene_->get_entity(cmd.entity_id); + if (!entity) continue; + + const auto* transform = entity->get_component(); + if (!transform) continue; + + // Set model matrix for this object + shadow_shader.set_mat4("uModelMatrix", transform->get_world_matrix()); + + cmd.mesh->draw(); + } + + shadow_material_->unbind(); + } + + void Renderer::execute_geometry_pass(const glm::mat4 &view, const glm::mat4 &proj) { + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + glDepthFunc(GL_LESS); + glDisable(GL_BLEND); + + + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CCW); + + glEnable(GL_STENCIL_TEST); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + + std::ranges::sort(opaque_objects_, + [](const RenderCommand &a, const RenderCommand &b) { + return a.distance_to_camera < b.distance_to_camera; + }); + + for (const auto &cmd: opaque_objects_) { + draw_render_command(cmd, view, proj); + } + + for (const auto &cmd: opaque_instanced_objects_) { + draw_instanced_command(cmd, view, proj); + } + } + + void Renderer::execute_transparency_pass(const glm::mat4 &view, const glm::mat4 &proj) { + // Configure blending: enable for color input, disable for object ID output + glEnablei(GL_BLEND, 0); // Enable blending for fragColor (location 0) + glDisablei(GL_BLEND, 1); // Disable blending for objectID (location 1) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Sort the transparent objects from back-to-front relative to camera + // This ensures proper blending order between different objects + std::ranges::sort(transparent_objects_, + [](const RenderCommand &a, const RenderCommand &b) { + return a.distance_to_camera > b.distance_to_camera; + }); + + // Render non-instanced transparent objects with two-pass rendering + glDisable(GL_CULL_FACE); + for (const auto &cmd: transparent_objects_) { + // Pass 1: Draw back faces, to depth buffer + glCullFace(GL_FRONT); + glDepthMask(GL_TRUE); + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(2.0f, 2.0f); + draw_render_command(cmd, view, proj); + glDisable(GL_POLYGON_OFFSET_FILL); + + // Pass 2: Draw front faces, don't write to depth buffer + glCullFace(GL_BACK); + glDepthMask(GL_FALSE); + draw_render_command(cmd, view, proj); + } + + // Render instanced transparent objects with two-pass rendering + for (const auto &cmd: transparent_instanced_objects_) { + // Pass 1: Draw back faces, to depth buffer + glCullFace(GL_FRONT); + glDepthMask(GL_TRUE); + draw_instanced_command(cmd, view, proj); + + // Pass 2: Draw front faces, don't write to depth buffer + glCullFace(GL_BACK); + glDepthMask(GL_FALSE); + draw_instanced_command(cmd, view, proj); + } + + // Restore depth writing + glDepthMask(GL_TRUE); + } + + void Renderer::create_main_framebuffer(uint32_t width, uint32_t height) { framebuffer_width_ = width; framebuffer_height_ = height; @@ -395,14 +509,14 @@ namespace hellfire { scene_framebuffers_[SCREEN_TEXTURE_1]->attach_color_texture(settings); scene_framebuffers_[SCREEN_TEXTURE_1]->attach_color_texture(object_id_attachment_settings); scene_framebuffers_[SCREEN_TEXTURE_1]->attach_depth_texture(settings); - + scene_framebuffers_[SCREEN_TEXTURE_2] = std::make_unique(); scene_framebuffers_[SCREEN_TEXTURE_2]->attach_color_texture(settings); scene_framebuffers_[SCREEN_TEXTURE_2]->attach_color_texture(object_id_attachment_settings); scene_framebuffers_[SCREEN_TEXTURE_2]->attach_depth_texture(settings); } - void Renderer::resize_scene_framebuffer(uint32_t width, uint32_t height) { + void Renderer::resize_main_framebuffer(uint32_t width, uint32_t height) { framebuffer_width_ = width; framebuffer_height_ = height; @@ -414,7 +528,7 @@ namespace hellfire { } } - uint32_t Renderer::get_scene_texture() const { + uint32_t Renderer::get_main_output_texture() const { const int display_index = 1 - current_fb_index_; if (scene_framebuffers_[display_index]) { return scene_framebuffers_[display_index]->get_color_attachment(0); @@ -430,36 +544,29 @@ namespace hellfire { return 0; } - void Renderer::render_to_texture(Scene &scene, CameraComponent &camera, uint32_t width, uint32_t height) { - if (!scene_framebuffers_[SCREEN_TEXTURE_1] || scene_framebuffers_[current_fb_index_]->get_width() != width || scene_framebuffers_[current_fb_index_]->get_height() != - height) { - resize_scene_framebuffer(width, height); - } - - render_scene_to_framebuffer(scene, camera); - } - - void Renderer::render_scene_to_framebuffer(Scene &scene, CameraComponent &camera) { + void Renderer::render_frame(Scene &scene, CameraComponent &camera) { if (!scene_framebuffers_[SCREEN_TEXTURE_1]) { - create_scene_framebuffer(framebuffer_width_, framebuffer_height_); + create_main_framebuffer(framebuffer_width_, framebuffer_height_); } // Check if the OTHER buffer (display buffer) needs resizing const int display_index = 1 - current_fb_index_; - if (scene_framebuffers_[display_index] && + if (scene_framebuffers_[display_index] && (scene_framebuffers_[display_index]->get_width() != framebuffer_width_ || scene_framebuffers_[display_index]->get_height() != framebuffer_height_)) { scene_framebuffers_[display_index]->resize(framebuffer_width_, framebuffer_height_); - } - + } + + execute_shadow_passes(scene); + scene_framebuffers_[current_fb_index_]->bind(); reset_framebuffer_data(); // Clear the object ID buffer (color attachment 1) to 0 constexpr GLuint clear_value = 0; glClearBufferuiv(GL_COLOR, 1, &clear_value); - - render_internal(scene, camera); + + execute_main_pass(scene, camera); scene_framebuffers_[current_fb_index_]->unbind(); glFlush(); @@ -475,7 +582,7 @@ namespace hellfire { } } - Shader& Renderer::get_shader_for_material(const std::shared_ptr &material) { + Shader &Renderer::get_shader_for_material(const std::shared_ptr &material) { if (!material) { return *fallback_shader_; } @@ -493,7 +600,7 @@ namespace hellfire { // Try to compile the material's shader if (const uint32_t compiled_id = compile_material_shader(material); compiled_id != 0) { material->set_compiled_shader_id(compiled_id); - const auto shader = shader_registry_.get_shader_from_id(compiled_id); + const auto shader = shader_registry_.get_shader_from_id(compiled_id); return *shader; } } @@ -524,5 +631,4 @@ namespace hellfire { // Compile using shader manager return shader_manager_.load_shader(variant); } - } diff --git a/engine/src/hellfire/graphics/renderer/Renderer.h b/engine/src/hellfire/graphics/renderer/Renderer.h index 033fa5c..a57852c 100644 --- a/engine/src/hellfire/graphics/renderer/Renderer.h +++ b/engine/src/hellfire/graphics/renderer/Renderer.h @@ -5,6 +5,7 @@ #include "RendererContext.h" #include "hellfire/ecs/Entity.h" +#include "hellfire/ecs/LightComponent.h" #include "hellfire/ecs/RenderableComponent.h" #include "hellfire/graphics/backends/opengl/Framebuffer.h" #include "hellfire/graphics/renderer/SkyboxRenderer.h" @@ -19,13 +20,13 @@ namespace hellfire { using EntityID = uint32_t; struct RenderCommand { - EntityID entity_id; // The entity being rendered - std::shared_ptr mesh; // Direct reference to renderable component - std::shared_ptr material; // Material for sorting and rendering - float distance_to_camera; // Distance for sorting - bool is_transparent; // Transparency flag for render pass + EntityID entity_id; // The entity being rendered + std::shared_ptr mesh; // Direct reference to renderable component + std::shared_ptr material; // Material for sorting and rendering + float distance_to_camera; // Distance for sorting + bool is_transparent; // Transparency flag for render pass - bool operator<(const RenderCommand& other) const { + bool operator<(const RenderCommand &other) const { if (is_transparent && other.is_transparent) { return distance_to_camera > other.distance_to_camera; } @@ -35,12 +36,12 @@ namespace hellfire { struct InstancedRenderCommand { EntityID entity_id; - InstancedRenderableComponent* instanced_renderable; + InstancedRenderableComponent *instanced_renderable; std::shared_ptr material; float distance_to_camera; bool is_transparent; - bool operator<(const RenderCommand& other) const { + bool operator<(const RenderCommand &other) const { if (is_transparent && other.is_transparent) { return distance_to_camera > other.distance_to_camera; } @@ -51,78 +52,102 @@ namespace hellfire { class Renderer { public: Renderer(); + ~Renderer() = default; - + void init(); + void render(Scene &scene, const Entity *camera_override); + void render_frame(Scene &scene, CameraComponent &camera); + void reset_framebuffer_data(); - void clear_drawable_objects_list(); + void clear_draw_list(); void begin_frame(); + void end_frame(); - [[nodiscard]] Shader *get_default_shader() const; + [[nodiscard]] Shader *get_fallback_shader() const { + return fallback_shader_; + } // Framebuffer management - void create_scene_framebuffer(uint32_t width, uint32_t height); + void create_main_framebuffer(uint32_t width, uint32_t height); + void set_render_to_framebuffer(bool enable) { render_to_framebuffer_ = enable; } - uint32_t get_scene_texture() const; + + uint32_t get_main_output_texture() const; uint32_t get_object_id_texture() const; - void resize_scene_framebuffer(uint32_t width, uint32_t height); - void render_to_texture(Scene& scene, CameraComponent& camera, uint32_t width, uint32_t height); - void render_scene_to_framebuffer(Scene& scene, CameraComponent& camera); + void resize_main_framebuffer(uint32_t width, uint32_t height); void set_fallback_shader(Shader &fallback_shader); - Shader& get_shader_for_material(const std::shared_ptr &material); + + Shader &get_shader_for_material(const std::shared_ptr &material); + uint32_t compile_material_shader(std::shared_ptr material); - ShaderManager& get_shader_manager() { return shader_manager_; } - ShaderRegistry& get_shader_registry() { return shader_registry_; } - + ShaderManager &get_shader_manager() { return shader_manager_; } + ShaderRegistry &get_shader_registry() { return shader_registry_; } + private: enum RendererFboId : uint32_t { SCREEN_TEXTURE_1 = 0, SCREEN_TEXTURE_2 = 1, SHADOW_MAP = 3 }; + ShaderManager shader_manager_; ShaderRegistry shader_registry_; - - Shader* fallback_shader_; + + Shader *fallback_shader_; uint32_t fallback_program_; std::unique_ptr context_; - Scene* scene_; // Current scene being rendered + Scene *scene_; // Current scene being rendered std::unique_ptr scene_framebuffers_[2]; int current_fb_index_ = 0; - + bool render_to_framebuffer_; uint32_t framebuffer_width_; uint32_t framebuffer_height_; - + // Render command lists std::vector opaque_objects_; std::vector transparent_objects_; std::vector opaque_instanced_objects_; std::vector transparent_instanced_objects_; + std::unordered_map shadow_maps_; SkyboxRenderer skybox_renderer_; - - void collect_render_commands_recursive(EntityID entity_id, const glm::vec3& camera_pos); - void store_lights_in_context(const std::vector& light_entities, CameraComponent& camera) const; - void render_internal(Scene& scene, CameraComponent& camera); - - // Rendering passes - void render_opaque_pass(const glm::mat4& view, const glm::mat4& projection); - void render_transparent_pass(const glm::mat4& view, const glm::mat4& projection); - void render_skybox_pass(Scene* scene, const glm::mat4& view, const glm::mat4& projection, CameraComponent* camera_comp) const; + std::shared_ptr shadow_material_; + + void collect_render_commands_recursive(EntityID entity_id, const glm::vec3 &camera_pos); + + void ensure_shadow_map(Entity *light_entity, const LightComponent &light); + + void store_lights_in_context(const std::vector &light_entities, CameraComponent &camera); + + void collect_lights_from_scene(Scene & scene, CameraComponent & camera); + void collect_geometry_from_scene(Scene &scene, const glm::vec3 camera_pos); + + void execute_main_pass(Scene& scene, CameraComponent& camera); + + void draw_shadow_geometry(const glm::mat4& light_view_proj); + + void execute_shadow_passes(Scene& scene); + void execute_geometry_pass(const glm::mat4 &view, const glm::mat4 &proj); + void execute_skybox_pass(Scene *scene, const glm::mat4 &view, const glm::mat4 &projection, + CameraComponent *camera_comp) const; + void execute_transparency_pass(const glm::mat4 &view, const glm::mat4 &proj); // Draw methods - void draw_render_command(const RenderCommand& cmd, const glm::mat4& view, const glm::mat4& projection); - void draw_instanced_command(const InstancedRenderCommand& cmd, const glm::mat4& view, const glm::mat4& projection); + void draw_render_command(const RenderCommand &cmd, const glm::mat4 &view, const glm::mat4 &projection); + + void draw_instanced_command(const InstancedRenderCommand &cmd, const glm::mat4 &view, + const glm::mat4 &projection); }; } diff --git a/sandbox/include/GamePlugin.h b/sandbox/include/GamePlugin.h index 9592837..bd322f7 100644 --- a/sandbox/include/GamePlugin.h +++ b/sandbox/include/GamePlugin.h @@ -17,7 +17,7 @@ class GamePlugin : public hellfire::IApplicationPlugin { private: hellfire::SceneManager *scene_manager_; std::unordered_map scenes_; - const hellfire::AppInfo* app_info_ = nullptr; + const hellfire::AppInfo* app_info_ = nullptr; void on_scene_activated(hellfire::Scene *scene); diff --git a/sandbox/src/main.cpp b/sandbox/src/main.cpp index f9af20d..fb33cc7 100644 --- a/sandbox/src/main.cpp +++ b/sandbox/src/main.cpp @@ -3,7 +3,7 @@ #include "hellfire/scene/CameraFactory.h" #include "hellfire/graphics/Geometry/Cube.h" -int main() { +void main() { // Create the main application window // Parameters: width (pixels), height (pixels), window title hellfire::Application app(800, 600, "Custom Engine - Hellfire"); @@ -20,6 +20,4 @@ int main() { app.run(); std::clog << "Terminating the application" << std::endl; - - return 0; } From ad2c12b89fdc1f46d233864b262f1b55e7e29577 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Thu, 27 Nov 2025 07:13:23 +0100 Subject: [PATCH 02/27] W.I.P. Shadowmapping --- editor/assets/shaders/phong.frag | 44 ++++++------ editor/src/Scenes/DefaultScene.h | 5 +- .../src/UI/Panels/Viewport/ViewportPanel.cpp | 2 +- engine/src/hellfire/ecs/LightComponent.h | 68 ------------------- .../graphics/lighting/DirectionalLight.cpp | 22 ------ .../hellfire/graphics/renderer/Renderer.cpp | 57 ++++++++++++---- .../src/hellfire/graphics/renderer/Renderer.h | 4 +- 7 files changed, 75 insertions(+), 127 deletions(-) diff --git a/editor/assets/shaders/phong.frag b/editor/assets/shaders/phong.frag index b3e47be..c198a18 100644 --- a/editor/assets/shaders/phong.frag +++ b/editor/assets/shaders/phong.frag @@ -11,43 +11,45 @@ layout(location=1) out uint objectID; uniform uint uObjectID; -float calculate_shadow(int light_index, vec3 frag_pos) { - // Transform fragmenet position to light space +float calculate_shadow(int light_index, vec3 frag_pos, vec3 normal, vec3 light_dir) { vec4 frag_pos_light_space = uLightSpaceMatrix[light_index] * vec4(frag_pos, 1.0); - - // Perspective divide vec3 proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; - - // Transform to [0,1] range for depth map sampling proj_coords = proj_coords * 0.5 + 0.5; - + if (proj_coords.z > 1.0) return 0.0; - - float closest_depth = texture(uShadowMap[light_index], proj_coords.xy).r; - - // Get depth of current fragment from light's perspective + float current_depth = proj_coords.z; - - // Bias to prevent shadow acne (object's casting shadows onto itself) - float bias = 0.001; - float shadow = current_depth - bias > closest_depth ? 1.0 : 0.0; - - return shadow; + float bias = 0.005; + + // PCF: sample a 3x3 area + float shadow = 0.0; + vec2 texel_size = 1.0 / textureSize(uShadowMap[light_index], 0); + + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + float pcf_depth = texture(uShadowMap[light_index], proj_coords.xy + vec2(x, y) * texel_size).r; + shadow += current_depth - bias > pcf_depth ? 1.0 : 0.0; + } + } + + return shadow / 9.0; } void main() { vec4 diffuseValue = sampleDiffuseTexture(fs_in.TexCoords); vec4 baseColor = applyVertexColors(diffuseValue, fs_in.Color); vec3 normal = calculateSurfaceNormal(fs_in.TexCoords, fs_in.Normal, fs_in.TBN); - vec3 result = calculateBlinnPhongLighting(normal, baseColor.rgb, fs_in.FragPos); + vec3 ambient = uAmbientLight * baseColor.rgb; + + // Calculate direct lighting (diffuse + specular) + vec3 direct = calculateBlinnPhongLighting(normal, baseColor.rgb, fs_in.FragPos); float shadow_factor = 0.0; if (numDirectionalLights > 0) { - shadow_factor = calculate_shadow(0, fs_in.FragPos); + shadow_factor = calculate_shadow(0, fs_in.FragPos, normal, -directionalLights[0].direction); } - float ambient_strength = 0.2; - result *= (ambient_strength + (1.0 - ambient_strength) * (1.0 - shadow_factor)); + vec3 result = ambient + direct * (1.0 - shadow_factor); fragColor = vec4(result, uOpacity); objectID = uObjectID; diff --git a/editor/src/Scenes/DefaultScene.h b/editor/src/Scenes/DefaultScene.h index 1efc8f1..d8fa072 100644 --- a/editor/src/Scenes/DefaultScene.h +++ b/editor/src/Scenes/DefaultScene.h @@ -11,8 +11,11 @@ #include "hellfire/utilities/FileDialog.h" inline void setup_default_scene_with_default_entities(hellfire::Scene *scene) { - hellfire::DirectionalLight::create( + auto light_id = hellfire::DirectionalLight::create( scene, "Sol Light", glm::vec3(-0.22f, 1.0f, 0.22f), glm::vec3(0.0f, 0.4f, 0.5f), 1.0f); + const auto light_entity = scene->get_entity(light_id); + light_entity->transform()->set_rotation(-35, -28, 0); + const hellfire::EntityID cube_id = hellfire::Cube::create(scene, "Rotating cube", {}); diff --git a/editor/src/UI/Panels/Viewport/ViewportPanel.cpp b/editor/src/UI/Panels/Viewport/ViewportPanel.cpp index 2ab2ece..6e62e57 100644 --- a/editor/src/UI/Panels/Viewport/ViewportPanel.cpp +++ b/editor/src/UI/Panels/Viewport/ViewportPanel.cpp @@ -42,7 +42,7 @@ namespace hellfire::editor { cam_component->set_mouse_sensitivity(0.1f); // Set initial position and orientation - editor_camera_->transform()->set_position(20.0f, 30.0f, 200.0f); + editor_camera_->transform()->set_position(20.0f, 30.0f, 50.0f); cam_component->look_at(glm::vec3(0.0f)); // Add Camera Control Script diff --git a/engine/src/hellfire/ecs/LightComponent.h b/engine/src/hellfire/ecs/LightComponent.h index b9c9d93..97162e4 100644 --- a/engine/src/hellfire/ecs/LightComponent.h +++ b/engine/src/hellfire/ecs/LightComponent.h @@ -140,74 +140,6 @@ namespace hellfire { const glm::mat4& get_light_view_proj_matrix() const { return light_view_proj_matrix; } void set_light_view_proj_matrix(const glm::mat4& lvpm) { light_view_proj_matrix = lvpm; } - void compute_directional_light_matrix() { - // For directional lights, use orthographic projection - // covering the scene bounds - const float ortho_size = 10.0f; - const float near_plane = 0.1f; - const float far_plane = 512.0f; - - glm::mat4 light_projection = glm::ortho(-ortho_size, ortho_size, - -ortho_size, ortho_size, near_plane, far_plane); - - glm::vec3 light_pos = -get_direction() * 10.0f; // Position light "backwards" along direction - glm::vec3 target = glm::vec3(0.0f); // Look at scene center - glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); - - // If direction is too close to up vector, use different up - if (glm::abs(glm::dot(glm::normalize(get_direction()), up)) > 0.99f) { - up = glm::vec3(1.0f, 0.0f, 0.0f); - } - - glm::mat4 light_view = glm::lookAt(light_pos, target, up); - - light_view_proj_matrix = light_projection * light_view; - } - - // Factory methods for convenience - static LightComponent *create_directional(const glm::vec3 &direction = glm::vec3(0.0f, -1.0f, 0.0f)) { - auto* light = new LightComponent(DIRECTIONAL); - light->set_direction(direction); - - // Much larger coverage to include your plane - float ortho_size = 50.0f; // Increased from 10 to 50 - float near_plane = 0.1f; - float far_plane = 100.0f; // Increased from 7.5 to 100 - - glm::mat4 light_projection = glm::ortho( - -ortho_size, ortho_size, - -ortho_size, ortho_size, - near_plane, far_plane - ); - - // Position light higher and further back to see more of the scene - glm::vec3 light_pos = glm::vec3(0.0f, 20.0f, 10.0f); - glm::vec3 look_at = glm::vec3(0.0f, 0.0f, 0.0f); - - glm::mat4 light_view = glm::lookAt( - light_pos, - look_at, - glm::vec3(0.0f, 0.0f, -1.0f) // Up vector - ); - - light->light_view_proj_matrix = light_projection * light_view; - return light; - } - - static LightComponent *create_point(const float range = 10.0f, const float attenuation = 1.0f) { - auto *light = new LightComponent(POINT); - light->set_range(range); - light->set_attenuation(attenuation); - return light; - } - - static LightComponent *create_spot(const float inner_angle = 30.0f, const float outer_angle = 45.0f) { - auto *light = new LightComponent(SPOT); - light->set_inner_cone_angle(inner_angle); - light->set_outer_cone_angle(outer_angle); - return light; - } - private: void upload_directional_to_shader(Shader& shader, const int light_index) const { // Get Euler angles and convert to direction diff --git a/engine/src/hellfire/graphics/lighting/DirectionalLight.cpp b/engine/src/hellfire/graphics/lighting/DirectionalLight.cpp index 454d9bd..93a29b1 100644 --- a/engine/src/hellfire/graphics/lighting/DirectionalLight.cpp +++ b/engine/src/hellfire/graphics/lighting/DirectionalLight.cpp @@ -15,28 +15,6 @@ namespace hellfire { light->set_direction(direction); light->set_color(color); light->set_intensity(intensity); - - // Much larger coverage to include your plane - float ortho_size = 50.0f; - float near_plane = 0.1f; - float far_plane = 100.0f; - - glm::mat4 light_projection = glm::ortho( - -ortho_size, ortho_size, - -ortho_size, ortho_size, - near_plane, far_plane - ); - - glm::vec3 light_pos = glm::vec3(0.0f, 20.0f, 10.0f); - glm::vec3 look_at = glm::vec3(0.0f, 0.0f, 0.0f); - - glm::mat4 light_view = glm::lookAt( - light_pos, - look_at, - glm::vec3(0.0f, 0.0f, -1.0f) // Up vector - ); - - light->set_light_view_proj_matrix(light_projection * light_view); return id; } diff --git a/engine/src/hellfire/graphics/renderer/Renderer.cpp b/engine/src/hellfire/graphics/renderer/Renderer.cpp index b450c7e..03b8b8f 100644 --- a/engine/src/hellfire/graphics/renderer/Renderer.cpp +++ b/engine/src/hellfire/graphics/renderer/Renderer.cpp @@ -201,8 +201,8 @@ namespace hellfire { if (!shadow_maps_.contains(light_entity)) { auto shadow_map = std::make_unique(); FrameBufferAttachmentSettings settings; - settings.width = 1024; - settings.height = 1024; + settings.width = 4096; + settings.height = 4096; settings.min_filter = GL_NEAREST; settings.mag_filter = GL_NEAREST; @@ -339,7 +339,7 @@ namespace hellfire { - void Renderer::execute_shadow_passes(Scene &scene) { + void Renderer::execute_shadow_passes(Scene &scene, CameraComponent& camera) { // Gather all lights that cast shadows const std::vector light_entity_ids = scene.find_entities_with_component(); @@ -363,7 +363,7 @@ namespace hellfire { // Render each light's shadow map for (Entity* light_entity : shadow_casting_lights) { - const auto* light = light_entity->get_component(); + auto* light = light_entity->get_component(); if (!light) continue; ensure_shadow_map(light_entity, *light); @@ -371,7 +371,7 @@ namespace hellfire { auto& shadow_data = shadow_maps_[light_entity]; shadow_data.framebuffer->bind(); - glViewport(0, 0, 1024, 1024); + glViewport(0, 0, 4096, 4096); glClear(GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); @@ -380,17 +380,19 @@ namespace hellfire { glCullFace(GL_FRONT); // Use light's view-projection matrix - const glm::mat4 light_view_proj = light->get_light_view_proj_matrix(); - shadow_data.light_view_proj = light_view_proj; // Store for main pass + shadow_data.light_view_proj = calculate_light_view_proj(light_entity, light, camera); // Store for main pass + + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(1.5f, 2.0f); // Render geometry to depth texture - draw_shadow_geometry(light_view_proj); + draw_shadow_geometry(shadow_data.light_view_proj); shadow_data.framebuffer->unbind(); - - glCullFace(GL_BACK); } - glViewport(0, 0, framebuffer_width_, framebuffer_height_); + glDisable(GL_POLYGON_OFFSET_FILL); + glCullFace(GL_BACK); + glViewport(0, 0, framebuffer_width_, framebuffer_height_); } void Renderer::draw_shadow_geometry(const glm::mat4 &light_view_proj) { @@ -416,6 +418,37 @@ namespace hellfire { shadow_material_->unbind(); } + glm::mat4 Renderer::calculate_light_view_proj(Entity *light_entity, LightComponent *light, const CameraComponent &camera) { + glm::vec3 light_dir = glm::normalize(light->get_direction()); + + // Center shadow map on camera position + glm::vec3 camera_pos = camera.get_owner().transform()->get_position(); + + float ortho_size = 100.0f; + float texel_size = (ortho_size * 2.0f) / 4096.0f; + + glm::vec3 look_at; + look_at.x = floor(camera_pos.x / texel_size) * texel_size; + look_at.y = 0.0f; + look_at.z = float(camera_pos.z / texel_size) * texel_size; + + glm::vec3 light_pos = look_at - light_dir * 30.0f; + + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); + if (glm::abs(glm::dot(light_dir, up)) > 0.99f) { + up = glm::vec3(1.0f, 0.0f, 0.0f); + } + + glm::mat4 light_projection = glm::ortho( + -ortho_size, ortho_size, + -ortho_size, ortho_size, + 1.0f, 60.0f); + + glm::mat4 light_view = glm::lookAt(light_pos, look_at, up); + + return light_projection * light_view; + } + void Renderer::execute_geometry_pass(const glm::mat4 &view, const glm::mat4 &proj) { glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); @@ -557,7 +590,7 @@ namespace hellfire { scene_framebuffers_[display_index]->resize(framebuffer_width_, framebuffer_height_); } - execute_shadow_passes(scene); + execute_shadow_passes(scene, camera); scene_framebuffers_[current_fb_index_]->bind(); reset_framebuffer_data(); diff --git a/engine/src/hellfire/graphics/renderer/Renderer.h b/engine/src/hellfire/graphics/renderer/Renderer.h index a57852c..a7e344b 100644 --- a/engine/src/hellfire/graphics/renderer/Renderer.h +++ b/engine/src/hellfire/graphics/renderer/Renderer.h @@ -135,10 +135,10 @@ namespace hellfire { void collect_geometry_from_scene(Scene &scene, const glm::vec3 camera_pos); void execute_main_pass(Scene& scene, CameraComponent& camera); - + glm::mat4 calculate_light_view_proj(Entity *light_entity, LightComponent *light, const CameraComponent &camera); void draw_shadow_geometry(const glm::mat4& light_view_proj); - void execute_shadow_passes(Scene& scene); + void execute_shadow_passes(Scene &scene, CameraComponent &camera); void execute_geometry_pass(const glm::mat4 &view, const glm::mat4 &proj); void execute_skybox_pass(Scene *scene, const glm::mat4 &view, const glm::mat4 &projection, CameraComponent *camera_comp) const; From 909df76ec22c2bbe091d6cf558be27a45999d3cc Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Thu, 27 Nov 2025 21:59:43 +0100 Subject: [PATCH 03/27] - Added a rendering settings panel on the editor project - Added an option to change the shadow bias settings in the renderer settings panel. --- .../assets/shaders/lighting/blinn_phong.glsl | 3 +- editor/assets/shaders/phong.frag | 4 +-- editor/src/EditorApplication.cpp | 20 ++++++----- editor/src/EditorApplication.h | 3 +- .../UI/Panels/Inspector/InspectorPanel.cpp | 33 +++++++------------ .../Renderer/RendererSettingsPanel.cpp | 22 +++++++++++++ .../Settings/Renderer/RendererSettingsPanel.h | 13 ++++++++ engine/src/hellfire/ecs/Entity.h | 2 -- engine/src/hellfire/ecs/LightComponent.h | 5 +-- .../hellfire/graphics/renderer/Renderer.cpp | 7 ++-- .../src/hellfire/graphics/renderer/Renderer.h | 11 +++++++ 11 files changed, 80 insertions(+), 43 deletions(-) create mode 100644 editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp create mode 100644 editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.h diff --git a/editor/assets/shaders/lighting/blinn_phong.glsl b/editor/assets/shaders/lighting/blinn_phong.glsl index 30dad84..a0dd073 100644 --- a/editor/assets/shaders/lighting/blinn_phong.glsl +++ b/editor/assets/shaders/lighting/blinn_phong.glsl @@ -53,8 +53,7 @@ vec3 calcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 diffuse, v } vec3 calculateBlinnPhongLighting(vec3 normal, vec3 baseColor, vec3 fragPos) { - vec3 result = uAmbientLight * baseColor; - + vec3 result = vec3(0.0); vec3 materialDiffuse = uDiffuseColor * baseColor; // Add directional lights for (int i = 0; i < min(numDirectionalLights, MAX_DIRECTIONAL_LIGHTS); i++) { diff --git a/editor/assets/shaders/phong.frag b/editor/assets/shaders/phong.frag index c198a18..93499e8 100644 --- a/editor/assets/shaders/phong.frag +++ b/editor/assets/shaders/phong.frag @@ -10,6 +10,7 @@ layout(location=0) out vec4 fragColor; layout(location=1) out uint objectID; uniform uint uObjectID; +uniform float uShadowBias; float calculate_shadow(int light_index, vec3 frag_pos, vec3 normal, vec3 light_dir) { vec4 frag_pos_light_space = uLightSpaceMatrix[light_index] * vec4(frag_pos, 1.0); @@ -19,7 +20,6 @@ float calculate_shadow(int light_index, vec3 frag_pos, vec3 normal, vec3 light_d if (proj_coords.z > 1.0) return 0.0; float current_depth = proj_coords.z; - float bias = 0.005; // PCF: sample a 3x3 area float shadow = 0.0; @@ -28,7 +28,7 @@ float calculate_shadow(int light_index, vec3 frag_pos, vec3 normal, vec3 light_d for (int x = -1; x <= 1; ++x) { for (int y = -1; y <= 1; ++y) { float pcf_depth = texture(uShadowMap[light_index], proj_coords.xy + vec2(x, y) * texel_size).r; - shadow += current_depth - bias > pcf_depth ? 1.0 : 0.0; + shadow += current_depth - uShadowBias > pcf_depth ? 1.0 : 0.0; } } diff --git a/editor/src/EditorApplication.cpp b/editor/src/EditorApplication.cpp index 9635515..b841eac 100644 --- a/editor/src/EditorApplication.cpp +++ b/editor/src/EditorApplication.cpp @@ -19,11 +19,11 @@ #include "UI/Panels/Viewport/ViewportPanel.h" #include "IconsFontAwesome6.h" #include "Scenes/DefaultScene.h" +#include "UI/Panels/Settings/Renderer/RendererSettingsPanel.h" namespace hellfire::editor { void EditorApplication::on_initialize(Application &app) { - app_ = &app; - app_->get_window_info().should_warp_cursor = false; + app.get_window_info().should_warp_cursor = false; auto *window = ServiceLocator::get_service(); if (!window) { @@ -51,11 +51,14 @@ namespace hellfire::editor { inspector_panel_ = std::make_unique(); inspector_panel_->set_context(&editor_context_); + renderer_settings_panel_ = std::make_unique(); + renderer_settings_panel_->set_context(&editor_context_); + // TEMP: const auto sm = ServiceLocator::get_service(); const auto new_scene = sm->create_scene("Test"); setup_default_scene_with_default_entities(new_scene); - sm->set_active_scene(new_scene, true); // Don't play in editor mode as default + sm->set_active_scene(new_scene, false); // Don't play in editor mode as default } void EditorApplication::initialize_imgui(IWindow *window) { @@ -159,6 +162,7 @@ namespace hellfire::editor { create_dockspace(); inspector_panel_->render(); + renderer_settings_panel_->render(); scene_viewport_->render(); scene_hierarchy_->render(); } @@ -220,14 +224,14 @@ namespace hellfire::editor { bool EditorApplication::on_mouse_move(float x, float y, float x_offset, float y_offset) { if (scene_viewport_->is_editor_camera_active()) { // Update camera with offset - scene_viewport_->get_editor_camera() - ->get_component() - ->handle_mouse_movement(x_offset, y_offset); - + if (const auto *camera = scene_viewport_->get_editor_camera()) { + if (auto *script = camera->get_component()) { + script->handle_mouse_movement(x_offset, y_offset); + } + } return true; // consumed } - ImGuiIO &io = ImGui::GetIO(); if (io.WantCaptureMouse) { return true; diff --git a/editor/src/EditorApplication.h b/editor/src/EditorApplication.h index d046974..171fc57 100644 --- a/editor/src/EditorApplication.h +++ b/editor/src/EditorApplication.h @@ -11,6 +11,7 @@ #include "hellfire/Interfaces/IApplicationPlugin.h" #include "hellfire/platform/IWindow.h" #include "UI/Panels/Inspector/InspectorPanel.h" +#include "UI/Panels/Settings/Renderer/RendererSettingsPanel.h" #include "UI/Panels/Viewport/ViewportPanel.h" namespace hellfire::editor { @@ -52,7 +53,6 @@ namespace hellfire::editor { private: EditorContext editor_context_; - Application* app_ = nullptr; bool imgui_initialized_ = false; // UI Components @@ -60,5 +60,6 @@ namespace hellfire::editor { std::unique_ptr scene_hierarchy_; std::unique_ptr scene_viewport_; std::unique_ptr inspector_panel_; + std::unique_ptr renderer_settings_panel_; }; } diff --git a/editor/src/UI/Panels/Inspector/InspectorPanel.cpp b/editor/src/UI/Panels/Inspector/InspectorPanel.cpp index 77fe301..b702fdb 100644 --- a/editor/src/UI/Panels/Inspector/InspectorPanel.cpp +++ b/editor/src/UI/Panels/Inspector/InspectorPanel.cpp @@ -19,17 +19,13 @@ namespace hellfire::editor { void InspectorPanel::render_add_component_context_menu(Entity *selected_entity) { if (ImGui::BeginPopupContextWindow("AddComponentPopup")) { - if (ImGui::MenuItem("Light")) { - if (selected_entity->has_component()) { - } else { - selected_entity->add_component(); - } + const bool has_light = selected_entity->has_component(); + if (ImGui::MenuItem("Light", nullptr, false, !has_light)) { + selected_entity->add_component(); } - if (ImGui::MenuItem("Renderable")) { - if (selected_entity->has_component()) { - } else { - selected_entity->add_component(); - } + const bool has_renderable = selected_entity->has_component(); + if (ImGui::MenuItem("Renderable", nullptr, false, !has_renderable)) { + selected_entity->add_component(); } if (ImGui::BeginMenu("Script")) { if (ImGui::MenuItem("Rotate Script")) { @@ -40,17 +36,13 @@ namespace hellfire::editor { } ImGui::EndMenu(); } - if (ImGui::MenuItem("Mesh")) { - if (selected_entity->has_component()) { - } else { - selected_entity->add_component(); - } + bool has_mesh = selected_entity->has_component(); + if (ImGui::MenuItem("Mesh", nullptr, false, !has_mesh)) { + selected_entity->add_component(); } - if (ImGui::MenuItem("Camera")) { - if (selected_entity->has_component()) { - } else { - selected_entity->add_component(); - } + bool has_camera = selected_entity->has_component(); + if (ImGui::MenuItem("Camera", nullptr, false, !has_camera)) { + selected_entity->add_component(); } ImGui::EndPopup(); } @@ -174,7 +166,6 @@ namespace hellfire::editor { } // Show material name and type - ImGui::Text("Material: %s", material->get_name().c_str()); if (material->has_custom_shader()) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "[Custom]"); diff --git a/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp b/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp new file mode 100644 index 0000000..c326d89 --- /dev/null +++ b/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp @@ -0,0 +1,22 @@ +// +// Created by denzel on 27/11/2025. +// + +#include "RendererSettingsPanel.h" + +#include "imgui.h" +#include "hellfire/graphics/renderer/Renderer.h" +#include "UI/ui.h" + +namespace hellfire::editor { +void RendererSettingsPanel::render() { + if (ImGui::Begin("Renderer Settings")) { + if (auto renderer = ServiceLocator::get_service()) { + ui::float_input("Shadow Bias", &renderer->get_shadow_settings().bias, 0.001); + } + ImGui::End(); + } + +} +} + diff --git a/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.h b/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.h new file mode 100644 index 0000000..c90cdd8 --- /dev/null +++ b/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.h @@ -0,0 +1,13 @@ +// +// Created by denzel on 27/11/2025. +// + +#pragma once +#include "UI/Panels/EditorPanel.h" + +namespace hellfire::editor { +class RendererSettingsPanel : public EditorPanel { +public: + void render() override; +}; +} diff --git a/engine/src/hellfire/ecs/Entity.h b/engine/src/hellfire/ecs/Entity.h index 23aa122..afc97fd 100644 --- a/engine/src/hellfire/ecs/Entity.h +++ b/engine/src/hellfire/ecs/Entity.h @@ -47,8 +47,6 @@ namespace hellfire { void set_name(const std::string &name) { name_ = name; } // Component management - - template T *add_component(Args &&... args); diff --git a/engine/src/hellfire/ecs/LightComponent.h b/engine/src/hellfire/ecs/LightComponent.h index 97162e4..5294546 100644 --- a/engine/src/hellfire/ecs/LightComponent.h +++ b/engine/src/hellfire/ecs/LightComponent.h @@ -14,10 +14,7 @@ #include "hellfire/graphics/backends/opengl/Framebuffer.h" namespace hellfire { - struct ShadowMapData { - std::unique_ptr framebuffer; - glm::mat4 light_view_proj; - }; + class LightComponent : public Component { public: diff --git a/engine/src/hellfire/graphics/renderer/Renderer.cpp b/engine/src/hellfire/graphics/renderer/Renderer.cpp index 03b8b8f..b5d3354 100644 --- a/engine/src/hellfire/graphics/renderer/Renderer.cpp +++ b/engine/src/hellfire/graphics/renderer/Renderer.cpp @@ -247,6 +247,7 @@ namespace hellfire { // Set shader uniforms shader.set_int("uShadowMap[" + std::to_string(i) + "]", texture_unit); shader.set_mat4("uLightSpaceMatrix[" + std::to_string(i) + "]", shadow_data.light_view_proj); + shader.set_float("uShadowBias", shadow_settings_.bias); } } } @@ -382,15 +383,15 @@ namespace hellfire { // Use light's view-projection matrix shadow_data.light_view_proj = calculate_light_view_proj(light_entity, light, camera); // Store for main pass - glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(1.5f, 2.0f); + // glEnable(GL_POLYGON_OFFSET_FILL); + // glPolygonOffset(1.5f, 2.0f); // Render geometry to depth texture draw_shadow_geometry(shadow_data.light_view_proj); shadow_data.framebuffer->unbind(); } - glDisable(GL_POLYGON_OFFSET_FILL); + // glDisable(GL_POLYGON_OFFSET_FILL); glCullFace(GL_BACK); glViewport(0, 0, framebuffer_width_, framebuffer_height_); } diff --git a/engine/src/hellfire/graphics/renderer/Renderer.h b/engine/src/hellfire/graphics/renderer/Renderer.h index a7e344b..903efac 100644 --- a/engine/src/hellfire/graphics/renderer/Renderer.h +++ b/engine/src/hellfire/graphics/renderer/Renderer.h @@ -49,6 +49,15 @@ namespace hellfire { } }; + struct ShadowMapData { + std::unique_ptr framebuffer; + glm::mat4 light_view_proj; + }; + + struct ShadowSettings { + float bias = 0.005f; + }; + class Renderer { public: Renderer(); @@ -92,6 +101,7 @@ namespace hellfire { ShaderManager &get_shader_manager() { return shader_manager_; } ShaderRegistry &get_shader_registry() { return shader_registry_; } + ShadowSettings &get_shadow_settings() { return shadow_settings_; } private: enum RendererFboId : uint32_t { @@ -121,6 +131,7 @@ namespace hellfire { std::vector opaque_instanced_objects_; std::vector transparent_instanced_objects_; std::unordered_map shadow_maps_; + ShadowSettings shadow_settings_; SkyboxRenderer skybox_renderer_; std::shared_ptr shadow_material_; From 85922306681a374780696f1bd377f5b40e6dfb97 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Tue, 2 Dec 2025 06:47:09 +0100 Subject: [PATCH 04/27] - Enabled Compile warnings as errors for cmake configuration --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d0c3db..7b88b08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(Hellfire VERSION 1.1) # Specify the C++ standard set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_COMPILE_WARNING_AS_ERROR ON) # Build the documentation only option option(BUILD_DOCS_ONLY "Only build documentation" OFF) From dceb1e8319da3b930a34a2e7554cc922771002ce Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Tue, 2 Dec 2025 06:47:42 +0100 Subject: [PATCH 05/27] W.I.P. Component Serialization --- .../hellfire/ecs/ComponentRegistration.cpp | 57 +++++++++++++++ .../src/hellfire/ecs/ComponentRegistration.h | 9 +++ engine/src/hellfire/ecs/ComponentRegistry.h | 73 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 engine/src/hellfire/ecs/ComponentRegistration.cpp create mode 100644 engine/src/hellfire/ecs/ComponentRegistration.h create mode 100644 engine/src/hellfire/ecs/ComponentRegistry.h diff --git a/engine/src/hellfire/ecs/ComponentRegistration.cpp b/engine/src/hellfire/ecs/ComponentRegistration.cpp new file mode 100644 index 0000000..0046226 --- /dev/null +++ b/engine/src/hellfire/ecs/ComponentRegistration.cpp @@ -0,0 +1,57 @@ +// +// Created by denzel on 30/11/2025. +// + +#include "ComponentRegistration.h" + +#include "ComponentRegistry.h" +#include "Entity.h" +#include "TransformComponent.h" + +namespace hellfire { + void register_all_components() { + auto ® = ComponentRegistry::instance(); + + reg.register_component("TransformComponent", + [](const TransformComponent &t) { + return nlohmann::json{ + { + "position", + { + t.get_position().x, t.get_position().y, + t.get_position().z + } + }, + { + "scale", + {t.get_scale().x, t.get_scale().y, t.get_scale().z} + }, + { + "rotation", + { + t.get_rotation().x, t.get_rotation().y, + t.get_rotation().z + } + } + }; + }, + [](Entity *e, const nlohmann::json &j) { + const auto t = e->transform(); + + if (j.contains("position")) { + auto &pos = j.at("position"); + t->set_position({pos[0], pos[1], pos[2]}); + } + + if (j.contains("scale")) { + auto &scale = j.at("scale"); + t->set_scale({scale[0], scale[1], scale[2]}); + } + + if (j.contains("rotation")) { + auto &rot = j.at("rotation"); + t->set_rotation({rot[0], rot[1], rot[2]}); + } + }); + } +} diff --git a/engine/src/hellfire/ecs/ComponentRegistration.h b/engine/src/hellfire/ecs/ComponentRegistration.h new file mode 100644 index 0000000..fd6e3a3 --- /dev/null +++ b/engine/src/hellfire/ecs/ComponentRegistration.h @@ -0,0 +1,9 @@ +// +// Created by denzel on 30/11/2025. +// + +#pragma once + +namespace hellfire { + void register_all_components(); +} \ No newline at end of file diff --git a/engine/src/hellfire/ecs/ComponentRegistry.h b/engine/src/hellfire/ecs/ComponentRegistry.h new file mode 100644 index 0000000..7c6c3fc --- /dev/null +++ b/engine/src/hellfire/ecs/ComponentRegistry.h @@ -0,0 +1,73 @@ +// +// Created by denzel on 30/11/2025. +// + +#pragma once +#include +#include + +#include "Component.h" +#include "nlohmann/json.hpp" + +namespace hellfire { + class Entity; + class Component; + + template + concept ComponentType = std::derived_from; + + class ComponentRegistry { + public: + using SerializeFn = std::function; + using DeserializeFn = std::function; + + static ComponentRegistry &instance() { + static ComponentRegistry registry; + return registry; + } + + template + void register_component(const std::string &type_name, std::function serialize, + std::function deserialize) { + const auto type = std::type_index(typeid(T)); + type_names_[type] = type_name; + name_to_type_.insert_or_assign(type_name, type); + + // Warp the typed function to match our storage type + serializers_[type] = [serialize](const Component *c) -> nlohmann::json { + return serialize(static_cast(*c)); + }; + + deserializers_[type_name] = std::move(deserialize); + } + + nlohmann::json serialize(const Component *component) const { + const auto type = std::type_index(typeid(*component)); + const auto it = serializers_.find(type); + if (it == serializers_.end()) return nullptr; + + nlohmann::json j = it->second(component); + j["_type"] = type_names_.at(type); + return j; + } + + void deserialize(Entity *entity, const nlohmann::json &j) const { + const std::string type_name = j.at("_type"); + const auto it = deserializers_.find(type_name); + if (it != deserializers_.end()) { + it->second(entity, j); + } + } + + // Prevent copying + ComponentRegistry(const ComponentRegistry&) = delete; + ComponentRegistry& operator=(const ComponentRegistry&) = delete; + private: + ComponentRegistry() = default; + + std::unordered_map type_names_; + std::unordered_map name_to_type_; + std::unordered_map serializers_; + std::unordered_map deserializers_; + }; +} From f35aa6ef3ce5eed3bcb8c15ab1db95b3e36a1fcf Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Tue, 2 Dec 2025 06:48:20 +0100 Subject: [PATCH 06/27] - Fixed a warning where the method wouldn't return if window is a nullptr --- .../platform/windows_linux/GLFWWindow.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/engine/src/hellfire/platform/windows_linux/GLFWWindow.cpp b/engine/src/hellfire/platform/windows_linux/GLFWWindow.cpp index 325d1ed..3fb4692 100644 --- a/engine/src/hellfire/platform/windows_linux/GLFWWindow.cpp +++ b/engine/src/hellfire/platform/windows_linux/GLFWWindow.cpp @@ -4,7 +4,7 @@ #include "GLFWWindow.h" namespace hellfire { - bool GLFWWindow::create(int width, int height, const std::string &title) { + bool GLFWWindow::create(const int width, const int height, const std::string &title) { if (!initialized_) { if (!glfwInit()) { return false; @@ -71,7 +71,7 @@ namespace hellfire { } } - void GLFWWindow::set_size(int width, int height) { + void GLFWWindow::set_size(const int width, const int height) { window_info_.width = width; window_info_.height = height; if (window_) { @@ -100,7 +100,7 @@ namespace hellfire { return {0, 0}; } - bool GLFWWindow::is_key_pressed(int keycode) const { + bool GLFWWindow::is_key_pressed(const int keycode) const { if (window_) { return glfwGetKey(window_, keycode) == GLFW_PRESS; } @@ -113,6 +113,7 @@ namespace hellfire { glfwGetCursorPos(window_, &x, &y); return {static_cast(x), static_cast(y)}; } + return {0, 0}; } void GLFWWindow::make_current() { @@ -121,7 +122,7 @@ namespace hellfire { } } - void GLFWWindow::key_callback(GLFWwindow *window, int key, int scancode, int action, int mods) { + void GLFWWindow::key_callback(GLFWwindow *window, const int key, int scancode, const int action, int mods) { if (const auto *instance = static_cast(glfwGetWindowUserPointer(window)); instance && instance->event_handler_) { if (action == GLFW_PRESS) { @@ -132,7 +133,7 @@ namespace hellfire { } } - void GLFWWindow::mouse_button_callback(GLFWwindow *window, int button, int action, int mods) { + void GLFWWindow::mouse_button_callback(GLFWwindow *window, const int button, const int action, int mods) { if (const auto *instance = static_cast(glfwGetWindowUserPointer(window)); instance && instance->event_handler_) { if (action == GLFW_PRESS) { @@ -143,14 +144,14 @@ namespace hellfire { } } - void GLFWWindow::cursor_position_callback(GLFWwindow *window, double x, double y) { + void GLFWWindow::cursor_position_callback(GLFWwindow *window, const double x, const double y) { if (const auto *instance = static_cast(glfwGetWindowUserPointer(window)); instance && instance->event_handler_) { instance->event_handler_->on_mouse_move(static_cast(x), static_cast(y)); } } - void GLFWWindow::window_size_callback(GLFWwindow *window, int width, int height) { + void GLFWWindow::window_size_callback(GLFWwindow *window, const int width, const int height) { if (auto *instance = static_cast(glfwGetWindowUserPointer(window))) { instance->window_info_.width = width; instance->window_info_.height = height; @@ -175,14 +176,14 @@ namespace hellfire { } float GLFWWindow::get_elapsed_time() { - return glfwGetTime(); + return static_cast(glfwGetTime()); } - void GLFWWindow::warp_cursor(double x, double y) { + void GLFWWindow::warp_cursor(const double x, const double y) { glfwSetCursorPos(window_, x, y); } - void GLFWWindow::set_cursor_mode(CursorMode mode) { + void GLFWWindow::set_cursor_mode(const CursorMode mode) { switch (mode) { case HIDDEN: glfwSetInputMode(window_, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); From 2e05b8bd5dd0f4daee5e9a4547151be6976bcf7e Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Tue, 2 Dec 2025 06:49:07 +0100 Subject: [PATCH 07/27] - added a comment for more context in phong shading shadow calculations --- editor/assets/shaders/phong.frag | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/assets/shaders/phong.frag b/editor/assets/shaders/phong.frag index 93499e8..6772924 100644 --- a/editor/assets/shaders/phong.frag +++ b/editor/assets/shaders/phong.frag @@ -15,6 +15,7 @@ uniform float uShadowBias; float calculate_shadow(int light_index, vec3 frag_pos, vec3 normal, vec3 light_dir) { vec4 frag_pos_light_space = uLightSpaceMatrix[light_index] * vec4(frag_pos, 1.0); vec3 proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; + // project coords into a [0, 1] space proj_coords = proj_coords * 0.5 + 0.5; if (proj_coords.z > 1.0) return 0.0; From e5064884cbc843fd3205b151be9d7dc6224fef13 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Tue, 2 Dec 2025 06:49:53 +0100 Subject: [PATCH 08/27] - const correctness fix --- engine/src/hellfire/ecs/TransformComponent.h | 3 ++- .../ecs/components/TransformComponent.cpp | 15 +++++++++++++++ engine/src/hellfire/graphics/Transform3D.cpp | 2 +- engine/src/hellfire/graphics/Transform3D.h | 11 +---------- 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 engine/src/hellfire/ecs/components/TransformComponent.cpp diff --git a/engine/src/hellfire/ecs/TransformComponent.h b/engine/src/hellfire/ecs/TransformComponent.h index 74ba926..d3de967 100644 --- a/engine/src/hellfire/ecs/TransformComponent.h +++ b/engine/src/hellfire/ecs/TransformComponent.h @@ -15,7 +15,8 @@ namespace hellfire { void set_position(float x, float y, float z) { transform_.set_position(x, y, z); } void set_position(const glm::vec3& position) { transform_.set_position(position); } - const glm::vec3& get_rotation() { return transform_.get_rotation(); } + + const glm::vec3& get_rotation() const { return transform_.get_rotation(); } void set_rotation(const glm::vec3& eulers) { transform_.set_rotation(eulers); } void set_rotation(float x, float y, float z) { transform_.set_rotation(glm::vec3(x, y, z)); } void set_rotation(const glm::quat& quaternion) { transform_.set_rotation_quaternion(quaternion); } diff --git a/engine/src/hellfire/ecs/components/TransformComponent.cpp b/engine/src/hellfire/ecs/components/TransformComponent.cpp new file mode 100644 index 0000000..08514bf --- /dev/null +++ b/engine/src/hellfire/ecs/components/TransformComponent.cpp @@ -0,0 +1,15 @@ +// +// Created by denzel on 30/11/2025. +// +#include "hellfire/ecs/TransformComponent.h" + +#include "hellfire/ecs/ComponentRegistry.h" + +namespace hellfire { + + static nlohmann::json serialize_transform(const hellfire::Component* c) { + + } + + +} diff --git a/engine/src/hellfire/graphics/Transform3D.cpp b/engine/src/hellfire/graphics/Transform3D.cpp index 8865c69..297b003 100644 --- a/engine/src/hellfire/graphics/Transform3D.cpp +++ b/engine/src/hellfire/graphics/Transform3D.cpp @@ -45,7 +45,7 @@ namespace hellfire { set_rotation(euler_degrees); } - glm::vec3& Transform3D::get_rotation() { + const glm::vec3& Transform3D::get_rotation() const { return rotation_in_degrees_; } diff --git a/engine/src/hellfire/graphics/Transform3D.h b/engine/src/hellfire/graphics/Transform3D.h index 7d5e7f8..049a509 100644 --- a/engine/src/hellfire/graphics/Transform3D.h +++ b/engine/src/hellfire/graphics/Transform3D.h @@ -152,16 +152,7 @@ namespace hellfire { use_scale_matrix_ = false; } - glm::vec3& get_rotation(); - - json to_json() { - json j; - j["position"] = {position_.x, position_.y, position_.z}; - j["rotation"] = {get_rotation().x, get_rotation().y, get_rotation().z}; - j["scale"] = {scale_.x, scale_.y, scale_.z}; - - return j; - } + const glm::vec3& get_rotation() const; private: glm::vec3 position_; From 3905ecb1701f882ccc6ff36f9d33996643c6ea76 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Thu, 4 Dec 2025 06:41:32 +0100 Subject: [PATCH 09/27] - Updated the license txt to include the name of the project and year - Added a menu item to the menubar for saving the project - Added Serializers for TransformComp and Scene --- CMakeLists.txt | 7 ++ LICENSE.txt | 2 +- .../UI/Panels/MenuBar/MenuBarComponent.cpp | 5 + .../hellfire/ecs/ComponentRegistration.cpp | 44 +-------- engine/src/hellfire/ecs/ComponentRegistry.h | 77 ++++++++++----- engine/src/hellfire/ecs/TransformComponent.h | 10 +- engine/src/hellfire/scene/SceneManager.cpp | 68 +++++-------- engine/src/hellfire/scene/SceneManager.h | 2 + .../hellfire/serialization/SceneSerializer.h | 95 +++++++++++++++++++ .../src/hellfire/serialization/Serializer.h | 59 ++++++++++++ 10 files changed, 248 insertions(+), 121 deletions(-) create mode 100644 engine/src/hellfire/serialization/SceneSerializer.h create mode 100644 engine/src/hellfire/serialization/Serializer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b88b08..1653d5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,13 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_COMPILE_WARNING_AS_ERROR ON) +# Maximize compiler warnings +#if(MSVC) +# add_compile_options(/W4 /WX) +#else() +# add_compile_options(-Wall -Wextra -Wpedantic -Werror) +#endif() + # Build the documentation only option option(BUILD_DOCS_ONLY "Only build documentation" OFF) diff --git a/LICENSE.txt b/LICENSE.txt index 8aa2645..c961d6a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2025 Hellfire Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp b/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp index 6573669..53c05f3 100644 --- a/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp +++ b/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp @@ -6,6 +6,7 @@ #include #include "imgui.h" #include "hellfire/core/Project.h" +#include "hellfire/ecs/ComponentRegistration.h" #include "hellfire/scene/CameraFactory.h" #include "hellfire/scene/Scene.h" #include "hellfire/scene/SceneManager.h" @@ -28,6 +29,10 @@ namespace hellfire::editor { Project::create("Test", "G:\\Dev\\Games"); std::cout << "Clicked on button New Project" << std::endl; } + if (ImGui::MenuItem("Save Project")) { + register_all_components(); + ServiceLocator::get_service()->save_scene("G:\\Dev\\Games\\Test\\assets\\cool_scene.hfscene", context_->active_scene); + } ImGui::EndMenu(); } } diff --git a/engine/src/hellfire/ecs/ComponentRegistration.cpp b/engine/src/hellfire/ecs/ComponentRegistration.cpp index 0046226..f612aeb 100644 --- a/engine/src/hellfire/ecs/ComponentRegistration.cpp +++ b/engine/src/hellfire/ecs/ComponentRegistration.cpp @@ -5,53 +5,11 @@ #include "ComponentRegistration.h" #include "ComponentRegistry.h" -#include "Entity.h" #include "TransformComponent.h" namespace hellfire { void register_all_components() { auto ® = ComponentRegistry::instance(); - - reg.register_component("TransformComponent", - [](const TransformComponent &t) { - return nlohmann::json{ - { - "position", - { - t.get_position().x, t.get_position().y, - t.get_position().z - } - }, - { - "scale", - {t.get_scale().x, t.get_scale().y, t.get_scale().z} - }, - { - "rotation", - { - t.get_rotation().x, t.get_rotation().y, - t.get_rotation().z - } - } - }; - }, - [](Entity *e, const nlohmann::json &j) { - const auto t = e->transform(); - - if (j.contains("position")) { - auto &pos = j.at("position"); - t->set_position({pos[0], pos[1], pos[2]}); - } - - if (j.contains("scale")) { - auto &scale = j.at("scale"); - t->set_scale({scale[0], scale[1], scale[2]}); - } - - if (j.contains("rotation")) { - auto &rot = j.at("rotation"); - t->set_rotation({rot[0], rot[1], rot[2]}); - } - }); + reg.register_component("TransformComponent"); } } diff --git a/engine/src/hellfire/ecs/ComponentRegistry.h b/engine/src/hellfire/ecs/ComponentRegistry.h index 7c6c3fc..9c3ee15 100644 --- a/engine/src/hellfire/ecs/ComponentRegistry.h +++ b/engine/src/hellfire/ecs/ComponentRegistry.h @@ -6,67 +6,92 @@ #include #include -#include "Component.h" +#include "Entity.h" +#include "hellfire/serialization/Serializer.h" #include "nlohmann/json.hpp" namespace hellfire { class Entity; class Component; - template - concept ComponentType = std::derived_from; - class ComponentRegistry { public: - using SerializeFn = std::function; - using DeserializeFn = std::function; + using SerializeFn = std::function; + using DeserializeFn = std::function; + using HasComponentFn = std::function; static ComponentRegistry &instance() { static ComponentRegistry registry; return registry; } + // Prevent copying + ComponentRegistry(const ComponentRegistry&) = delete; + ComponentRegistry& operator=(const ComponentRegistry&) = delete; + + template - void register_component(const std::string &type_name, std::function serialize, - std::function deserialize) { + void register_component(const std::string &type_name) { const auto type = std::type_index(typeid(T)); type_names_[type] = type_name; name_to_type_.insert_or_assign(type_name, type); - // Warp the typed function to match our storage type - serializers_[type] = [serialize](const Component *c) -> nlohmann::json { - return serialize(static_cast(*c)); + has_component_[type] = [](const Entity& e) { + return e.has_component(); + }; + + // Lambda method for returning the serialization method + serializers_[type] = [](const Entity& e) { + std::ostringstream stream; + Serializer::serialize(stream, e.get_component()); + nlohmann::json j = nlohmann::json::parse(stream.str()); + return j; }; - deserializers_[type_name] = std::move(deserialize); + deserializers_[type_name] = [](Entity& e, const nlohmann::json& j) { + auto comp = e.add_component(); + std::istringstream stream(j.dump()); + Serializer::deserialize(stream, comp); + }; } - nlohmann::json serialize(const Component *component) const { - const auto type = std::type_index(typeid(*component)); - const auto it = serializers_.find(type); - if (it == serializers_.end()) return nullptr; + nlohmann::json serialize_all_components(const Entity& entity) const { + nlohmann::ordered_json components = nlohmann::ordered_json::array(); - nlohmann::json j = it->second(component); - j["_type"] = type_names_.at(type); - return j; + for (const auto& [type, has_fn] : has_component_) { + if (has_fn(entity)) { + nlohmann::ordered_json comp = serializers_.at(type)(entity); + comp["_type"] = type_names_.at(type); + components.push_back(comp); + } + } + + return components; } - void deserialize(Entity *entity, const nlohmann::json &j) const { - const std::string type_name = j.at("_type"); + bool deserialize_component(Entity& entity, const nlohmann::json& comp_json) const { + if (!comp_json.contains("_type")) return false; + + std::string type_name = comp_json.at("_type"); const auto it = deserializers_.find(type_name); - if (it != deserializers_.end()) { - it->second(entity, j); + if (it == deserializers_.end()) return false; + + it->second(entity, comp_json); + return true; + } + + void deserialize_all_components(Entity& entity, const nlohmann::json& components) const { + for (const auto& comp_json : components) { + deserialize_component(entity, comp_json); } } - // Prevent copying - ComponentRegistry(const ComponentRegistry&) = delete; - ComponentRegistry& operator=(const ComponentRegistry&) = delete; private: ComponentRegistry() = default; std::unordered_map type_names_; std::unordered_map name_to_type_; + std::unordered_map has_component_; std::unordered_map serializers_; std::unordered_map deserializers_; }; diff --git a/engine/src/hellfire/ecs/TransformComponent.h b/engine/src/hellfire/ecs/TransformComponent.h index d3de967..1e80ac5 100644 --- a/engine/src/hellfire/ecs/TransformComponent.h +++ b/engine/src/hellfire/ecs/TransformComponent.h @@ -21,8 +21,8 @@ namespace hellfire { void set_rotation(float x, float y, float z) { transform_.set_rotation(glm::vec3(x, y, z)); } void set_rotation(const glm::quat& quaternion) { transform_.set_rotation_quaternion(quaternion); } - - const glm::vec3& get_scale() const { return transform_.get_scale(); } + + glm::vec3 get_scale() const { return transform_.get_scale(); } glm::vec3& get_scale() { return transform_.get_scale(); } void set_scale(float x, float y, float z) { transform_.set_scale(glm::vec3(x, y, z)); } void set_scale(float value) { transform_.set_scale(glm::vec3(value, value, value)); } @@ -35,9 +35,9 @@ namespace hellfire { const glm::mat4& get_local_matrix() const { return transform_.get_local_matrix(); } const glm::mat4& get_world_matrix() const { return transform_.get_world_matrix(); } - const glm::mat4 get_rotation_matrix() const { return transform_.get_rotation_matrix(); } - const glm::mat4 get_translation_matrix() const { return transform_.get_translation_matrix(); } - const glm::mat4 get_scale_matrix() const { return transform_.get_scale_matrix(); } + glm::mat4 get_rotation_matrix() const { return transform_.get_rotation_matrix(); } + glm::mat4 get_translation_matrix() const { return transform_.get_translation_matrix(); } + glm::mat4 get_scale_matrix() const { return transform_.get_scale_matrix(); } // Delegate all other methods to the internal Transform3D void look_at(const glm::vec3& target, const glm::vec3& up = glm::vec3(0,1,0)) { diff --git a/engine/src/hellfire/scene/SceneManager.cpp b/engine/src/hellfire/scene/SceneManager.cpp index b2da750..fa4facb 100644 --- a/engine/src/hellfire/scene/SceneManager.cpp +++ b/engine/src/hellfire/scene/SceneManager.cpp @@ -5,6 +5,8 @@ #include #include +#include "hellfire/serialization/SceneSerializer.h" + namespace hellfire { SceneManager::SceneManager() : active_scene_(nullptr) { } @@ -42,28 +44,14 @@ namespace hellfire { return nullptr; } - nlohmann::json scene_data; - try { - file >> scene_data; - } catch (const std::exception &e) { - std::cerr << "Failed to parse scene file: " << e.what() << std::endl; + Scene* new_scene = create_scene(); + if (!Serializer::deserialize(file, new_scene)) { + std::cerr << "Failed to deserialize scene: " << filename << std::endl; + destroy_scene(new_scene); return nullptr; } - // Validate scene data - if (!scene_data.contains("name") || !scene_data.contains("version")) { - std::cerr << "Invalid scene file format" << std::endl; - return nullptr; - } - - Scene *new_scene = create_scene(scene_data["name"]); new_scene->set_source_filename(filename); - - // TODO: Deserialize entities from scene_data["entities"] - if (scene_data.contains("entities") && !scene_data["entities"].empty()) { - // Deserialize entities here when implemented - std::cout << "TODO: Deserialize " << scene_data["entities"].size() << " entities\n"; - } return new_scene; } @@ -71,37 +59,19 @@ namespace hellfire { if (!scene) scene = get_active_scene(); if (!scene) return false; - try { - json scene_data; - scene_data["name"] = scene->get_name(); - scene_data["version"] = "1.0"; - - json entities_array = json::array(); - - // TODO: Iterate through scene entities - // for (EntityID id : scene->get_all_entity_ids()) { - // Entity* entity = scene->get_entity(id); - // json entity_data = serialize_entity(entity); - // entities_array.push_back(entity_data); - // } - - scene_data["entities"] = entities_array; - - std::ofstream file(filepath); - if (!file.is_open()) { - std::cerr << "Failed to open file for writing: " << filepath << std::endl; - return false; - } - - file << std::setw(4) << scene_data << std::endl; - - scene->set_source_filename(filepath); + std::ofstream file(filepath); + if (!file.is_open()) { + std::cerr << "Failed to open file for writing: " << filepath << std::endl; + return false; + } - return true; - } catch (const std::exception &e) { - std::cerr << "Error saving scene: " << e.what() << std::endl; + if (!Serializer::serialize(file, scene)) { + std::cerr << "Failed to serialize scene: " << filepath << std::endl; return false; } + + scene->set_source_filename(filepath); + return true; } void SceneManager::update(float delta_time) { @@ -118,6 +88,12 @@ namespace hellfire { active_scene_ = nullptr; } + void SceneManager::destroy_scene(Scene* scene) { + // TO + + } + + EntityID SceneManager::find_entity_by_name(const std::string &name) { if (active_scene_) { Entity *entity = active_scene_->find_entity_by_name(name); diff --git a/engine/src/hellfire/scene/SceneManager.h b/engine/src/hellfire/scene/SceneManager.h index 233e12e..b5d6825 100644 --- a/engine/src/hellfire/scene/SceneManager.h +++ b/engine/src/hellfire/scene/SceneManager.h @@ -46,6 +46,8 @@ namespace hellfire { void clear(); + void destroy_scene(Scene *scene); + // Object management Scene *create_scene(const std::string &name = "GameScene"); diff --git a/engine/src/hellfire/serialization/SceneSerializer.h b/engine/src/hellfire/serialization/SceneSerializer.h new file mode 100644 index 0000000..576bf35 --- /dev/null +++ b/engine/src/hellfire/serialization/SceneSerializer.h @@ -0,0 +1,95 @@ +// +// Created by denzel on 03/12/2025. +// + +#pragma once +#include "hellfire/ecs/ComponentRegistry.h" +#include "hellfire/scene/Scene.h" +#include "hellfire/serialization/Serializer.h" + +namespace hellfire { + template<> + struct Serializer { + using Remap = std::unordered_map; + + static bool serialize(std::ostream& output, const Scene* scene) { + if (scene == nullptr) return false; + + nlohmann::ordered_json j; + j["name"] = scene->get_name(); + j["default_camera"] = scene->get_default_camera_entity_id(); + j["entities"] = nlohmann::ordered_json::array(); + + for (const EntityID root_id : scene->get_root_entities()) { + serialize_entity_recursive(*scene, root_id, j["entities"]); + } + + output << j.dump(2); + return output.good(); + } + + static bool deserialize(std::istream& input, Scene* scene) { + try { + nlohmann::json j; + input >> j; + + scene->set_name(j.at("name")); + + Remap id_remap; + for (const auto& entity_json : j.at("entities")) { + create_entity_recursive(*scene, entity_json, INVALID_ENTITY, id_remap); + } + + if (j.contains("default_camera")) { + EntityID old_id = j.at("default_camera"); + if (id_remap.contains(old_id)) { + scene->set_default_camera(id_remap.at(old_id)); + } + } + + return true; + } catch (...) { + return false; + } + } + + private: + static void serialize_entity_recursive(const Scene& scene, EntityID id, nlohmann::ordered_json& out) { + const Entity* entity = scene.get_entity(id); + if (!entity) return; + + nlohmann::ordered_json entity_json; + entity_json["id"] = id; + entity_json["name"] = entity->get_name(); + entity_json["components"] = ComponentRegistry::instance().serialize_all_components(*entity); + entity_json["children"] = nlohmann::json::array(); + + for (EntityID child_id : scene.get_children(id)) { + serialize_entity_recursive(scene, child_id, entity_json["children"]); + } + + out.push_back(entity_json); + } + + static void create_entity_recursive(Scene& scene, const nlohmann::json& entity_json, EntityID parent_id, Remap& id_remap) { + EntityID old_id = entity_json.at("id"); + EntityID new_id = scene.create_entity(entity_json.at("name")); + id_remap[old_id] = new_id; + + if (parent_id != INVALID_ENTITY) { + scene.set_parent(new_id, parent_id); + } + + Entity* entity = scene.get_entity(new_id); + ComponentRegistry::instance().deserialize_all_components(*entity, entity_json); + + if (entity_json.contains("children")) { + for (const auto& child_json : entity_json.at("children")) { + create_entity_recursive(scene, child_json, new_id, id_remap); + } + } + + + } + }; +} diff --git a/engine/src/hellfire/serialization/Serializer.h b/engine/src/hellfire/serialization/Serializer.h new file mode 100644 index 0000000..f4a436a --- /dev/null +++ b/engine/src/hellfire/serialization/Serializer.h @@ -0,0 +1,59 @@ +// +// Created by denzel on 03/12/2025. +// +#pragma once + +#include + +#include "hellfire/ecs/TransformComponent.h" +#include "hellfire/scene/Scene.h" + +// GLM Helper extensions +namespace glm { + inline void to_json(nlohmann::json& j, const vec3& v) { + j = {v.x, v.y, v.z}; + } + + inline void from_json(const nlohmann::json& j, vec3& v) { + v = {j[0].get(), j[1].get(), j[2].get()}; + } +} + +namespace hellfire { + template + struct Serializer { + static bool serialize(std::ostream& output, const T* obj); + static bool deserialize(std::istream& input, T* obj); + }; + + template<> + struct Serializer { + static bool serialize(std::ostream& output, const TransformComponent* obj) { + if (obj == nullptr) return false; + + nlohmann::json j = { + {"position", obj->get_position()}, + {"rotation", obj->get_rotation()}, + {"scale", obj->get_scale()} + }; + + output << j.dump(4); + return output.good(); + } + + static bool deserialize(std::istream& input, TransformComponent* obj) { + try { + if (obj == nullptr) return false; + + nlohmann::json j; + input >> j; + obj->set_position(j.at("position").get()); + obj->set_rotation(j.at("rotation").get()); + obj->set_scale(j.at("scale").get()); + return true; + } catch (...) { + return false; + } + } + }; +} From 9cdb407f384fd5e79ad38a5294b8fc44a2a57fae Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:00:58 +0100 Subject: [PATCH 10/27] - Removed the registering of the scenemanager service from the application, will be registered by Project or manual from application that uses the engine. - Also won't render the scene without the scenemanager --- engine/src/hellfire/core/Application.cpp | 51 +++++++++++++----------- engine/src/hellfire/core/Application.h | 1 - 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/engine/src/hellfire/core/Application.cpp b/engine/src/hellfire/core/Application.cpp index ad9fb6a..ad20087 100644 --- a/engine/src/hellfire/core/Application.cpp +++ b/engine/src/hellfire/core/Application.cpp @@ -88,8 +88,6 @@ namespace hellfire { ServiceLocator::register_service(input_manager_.get()); ServiceLocator::register_service(&shader_manager_); ServiceLocator::register_service(window_.get()); - ServiceLocator::register_service(&scene_manager_); - // Initialize engine systems Time::init(); @@ -101,7 +99,7 @@ namespace hellfire { call_plugins([this](IApplicationPlugin &plugin) { plugin.on_initialize(*this); - }); + }); } void Application::run() { @@ -115,11 +113,13 @@ namespace hellfire { window_->poll_events(); // Make sure the timer is updated Time::update(); - + input_manager_->update(); // Update scene - scene_manager_.update(Time::delta_time); + if (auto sm = ServiceLocator::get_service()) { + sm->update(Time::delta_time); + } on_render(); } @@ -132,17 +132,19 @@ namespace hellfire { plugin.on_begin_frame(); }); renderer_.begin_frame(); - - if (auto *active_scene = scene_manager_.get_active_scene()) { - Entity *camera_override = nullptr; - - call_plugins([&camera_override](IApplicationPlugin &plugin) { - if (!camera_override) { - camera_override = plugin.get_render_camera_override(); - } - }); - - renderer_.render(*active_scene, camera_override); + + if (auto sm = ServiceLocator::get_service()) { + if (auto *active_scene = sm->get_active_scene()) { + Entity *camera_override = nullptr; + + call_plugins([&camera_override](IApplicationPlugin &plugin) { + if (!camera_override) { + camera_override = plugin.get_render_camera_override(); + } + }); + + renderer_.render(*active_scene, camera_override); + } } // Plugin render @@ -150,8 +152,8 @@ namespace hellfire { plugin.on_render(); }); - renderer_.end_frame(); - + + renderer_.end_frame(); // Plugin end_frame call_plugins([](IApplicationPlugin &plugin) { plugin.on_end_frame(); @@ -240,11 +242,14 @@ namespace hellfire { glViewport(0, 0, width, height); // Update cameras - if (Scene *active_scene = scene_manager_.get_active_scene()) { - for (const EntityID camera_id: scene_manager_.get_camera_entities()) { - if (const Entity *camera_entity = active_scene->get_entity(camera_id)) { - if (auto *camera_comp = camera_entity->get_component()) { - camera_comp->set_aspect_ratio(window_info_.aspect_ratio); } + if (auto sm = ServiceLocator::get_service()) { + if (Scene *active_scene = sm->get_active_scene()) { + for (const EntityID camera_id: sm->get_camera_entities()) { + if (const Entity *camera_entity = active_scene->get_entity(camera_id)) { + if (auto *camera_comp = camera_entity->get_component()) { + camera_comp->set_aspect_ratio(window_info_.aspect_ratio); + } + } } } } diff --git a/engine/src/hellfire/core/Application.h b/engine/src/hellfire/core/Application.h index 8b49f70..2e13f2e 100644 --- a/engine/src/hellfire/core/Application.h +++ b/engine/src/hellfire/core/Application.h @@ -90,7 +90,6 @@ namespace hellfire { ShaderManager shader_manager_; ShaderRegistry shader_registry_; - SceneManager scene_manager_; Renderer renderer_; // Window info tracking From a36d3f1dfd629e9f51e3c7b5ea5a187e899619d2 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:01:53 +0100 Subject: [PATCH 11/27] - Updated time with get_current_time() helper method --- engine/src/hellfire/core/Time.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/engine/src/hellfire/core/Time.h b/engine/src/hellfire/core/Time.h index 0fa2ea6..9ad1f50 100644 --- a/engine/src/hellfire/core/Time.h +++ b/engine/src/hellfire/core/Time.h @@ -25,5 +25,14 @@ namespace hellfire { delta_time = current_time - last_frame_time; last_frame_time = current_time; } + + + static std::string get_current_timestamp() { + auto now = std::chrono::system_clock::now(); + auto time_t_now = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::gmtime(&time_t_now), "%Y-%m-%dT%H:%M:%SZ"); + return ss.str(); + } }; } \ No newline at end of file From b01ea75ca6ee229dd0b4ab64fb0f0d91a921101e Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:02:21 +0100 Subject: [PATCH 12/27] - Added RAII wrappers around ImGui for automatic resource cleanup --- editor/src/UI/ui.h | 54 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/editor/src/UI/ui.h b/editor/src/UI/ui.h index a396490..c99c414 100644 --- a/editor/src/UI/ui.h +++ b/editor/src/UI/ui.h @@ -13,6 +13,7 @@ #include "glm/detail/type_vec4.hpp" #include "hellfire/utilities/FileDialog.h" +/// Inputs namespace hellfire::editor::ui { /** * @brief Method for generating UI Id's @@ -41,8 +42,8 @@ namespace hellfire::editor::ui { * @param max Maximum allowed value (default: FLT_MAX) * @return True if the value was modified */ - inline bool float_input(const std::string &label, float *value, float speed = 1.0f, float min = 0.0f, - float max = FLT_MAX) { + inline bool float_input(const std::string &label, float *value, const float speed = 1.0f, const float min = 0.0f, + const float max = FLT_MAX) { return Property(label, value, [&](const char *id, float *val) { return ImGui::DragFloat(id, val, speed, min, max); }); @@ -57,8 +58,8 @@ namespace hellfire::editor::ui { * @param max Maximum allowed value (default: FLT_MAX) * @return True if the value was modified */ - inline bool vec2_input(const std::string &label, glm::vec2 *value, float speed = 1.0f, float min = 0.0f, - float max = FLT_MAX) { + inline bool vec2_input(const std::string &label, glm::vec2 *value, const float speed = 1.0f, const float min = 0.0f, + const float max = FLT_MAX) { return Property(label, value, [&](const char *id, glm::vec2 *val) { return ImGui::DragFloat2(id, &(*val)[0], speed, min, max); }); @@ -74,8 +75,8 @@ namespace hellfire::editor::ui { * @param max Maximum allowed value (default: FLT_MAX) * @return True if the value was modified */ - inline bool vec3_input(const std::string &label, glm::vec3 *value, float speed = 1.0f, float min = 0.0f, - float max = FLT_MAX) { + inline bool vec3_input(const std::string &label, glm::vec3 *value, const float speed = 1.0f, const float min = 0.0f, + const float max = FLT_MAX) { return Property(label, value, [&](const char *id, glm::vec3 *val) { return ImGui::DragFloat3(id, &(*val)[0], speed, min, max); }); @@ -216,3 +217,44 @@ namespace hellfire::editor::ui { return changed; } } + +/// Windows +namespace hellfire::editor::ui { + class Window { + public: + Window(const char* name, bool* p_open = nullptr, const ImGuiWindowFlags flags = 0) : visible_(ImGui::Begin(name, p_open, flags)) {} + Window(const std::string& name, bool* p_open = nullptr, const ImGuiWindowFlags flags = 0) : visible_(ImGui::Begin(name.c_str(), p_open, flags)) {} + + ~Window() { ImGui::End(); } + + // Returns true if window content should be rendered + explicit operator bool() const { return visible_; } + + // Disable move and copying + Window(const Window&) = delete; + Window& operator=(const Window&) = delete; + Window(Window&&) = delete; + Window& operator=(Window&&) = delete; + private: + bool visible_; + }; + + + class ChildWindow { + public: + ChildWindow(const char* name, const ImVec2 size = {}, const ImGuiWindowFlags child_flags = 0, const ImGuiWindowFlags window_flags = 0) : visible_(ImGui::BeginChild(name, size, child_flags, window_flags)) {} + + ~ChildWindow() { ImGui::EndChild(); } + + // Returns true if window content should be rendered + explicit operator bool() const { return visible_; } + + // Disable move and copying + ChildWindow(const ChildWindow&) = delete; + ChildWindow& operator=(const ChildWindow&) = delete; + ChildWindow(ChildWindow&&) = delete; + ChildWindow& operator=(ChildWindow&&) = delete; + private: + bool visible_; + }; +} From b84b09175d4cf12258a75ca52dbadd0d35ba7d34 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:04:29 +0100 Subject: [PATCH 13/27] - Implemented a method for selecting folders in the filedialog utility. --- engine/src/hellfire/utilities/FileDialog.cpp | 69 ++++++++++++++++++++ engine/src/hellfire/utilities/FileDialog.h | 10 ++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/engine/src/hellfire/utilities/FileDialog.cpp b/engine/src/hellfire/utilities/FileDialog.cpp index 2270458..d32c8fe 100644 --- a/engine/src/hellfire/utilities/FileDialog.cpp +++ b/engine/src/hellfire/utilities/FileDialog.cpp @@ -8,6 +8,8 @@ #ifdef _WIN32 #include #include +#include +#include #endif namespace hellfire::Utility { @@ -168,6 +170,65 @@ namespace hellfire::Utility { return filepath; } + std::string FileDialog::win32_select_folder(const std::string& title) { + std::string folder_path; + + // Initialize COM (required for IFileDialog) + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (FAILED(hr)) { + return folder_path; + } + + IFileDialog* pfd = nullptr; + hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd)); + + if (SUCCEEDED(hr)) { + // Get current options and add folder picker flag + DWORD dwOptions; + hr = pfd->GetOptions(&dwOptions); + if (SUCCEEDED(hr)) { + // FOS_PICKFOLDERS makes it a folder picker instead of file picker + hr = pfd->SetOptions(dwOptions | FOS_PICKFOLDERS); + } + + // Set the dialog title if provided + if (SUCCEEDED(hr) && !title.empty()) { + std::wstring wideTitle(title.begin(), title.end()); + pfd->SetTitle(wideTitle.c_str()); + } + + // Show the dialog + if (SUCCEEDED(hr)) { + hr = pfd->Show(GetActiveWindow()); + } + + // Get the result + if (SUCCEEDED(hr)) { + IShellItem* psi = nullptr; + hr = pfd->GetResult(&psi); + if (SUCCEEDED(hr)) { + PWSTR pszPath = nullptr; + hr = psi->GetDisplayName(SIGDN_FILESYSPATH, &pszPath); + if (SUCCEEDED(hr) && pszPath) { + // Convert wide string to narrow string + const int size = WideCharToMultiByte(CP_UTF8, 0, pszPath, -1, nullptr, 0, nullptr, nullptr); + if (size > 0) { + folder_path.resize(size - 1); + WideCharToMultiByte(CP_UTF8, 0, pszPath, -1, &folder_path[0], size, nullptr, nullptr); + } + CoTaskMemFree(pszPath); + } + psi->Release(); + } + } + pfd->Release(); + } + + CoUninitialize(); + return folder_path; + + } + std::string FileDialog::imgui_save_file(const std::string &default_filename, const std::vector &filters) { static std::string filepath; @@ -240,6 +301,14 @@ namespace hellfire::Utility { #endif } + std::string FileDialog::select_folder(const std::string& title) { +#ifdef _WIN32 + return win32_select_folder(title); +#else + return imgui_select_folder(title); // TODO: Filepicker for other platforms using imgui +#endif + } + std::string FileDialog::save_file(std::string &save_name_to, const std::string &default_filename, const std::vector &filters) { #ifdef _WIN32 diff --git a/engine/src/hellfire/utilities/FileDialog.h b/engine/src/hellfire/utilities/FileDialog.h index 5f58612..be492d2 100644 --- a/engine/src/hellfire/utilities/FileDialog.h +++ b/engine/src/hellfire/utilities/FileDialog.h @@ -15,8 +15,11 @@ namespace hellfire::Utility { class FileDialog { public: static std::string open_file(const std::vector& filters = {}); + + static std::string select_folder(const std::string &title); + static std::string save_file(std::string& save_name_to, - const std::string& default_filename = "", + const std::string& default_filename = "", const std::vector& filters = {}); private: @@ -25,8 +28,11 @@ namespace hellfire::Utility { static std::string win32_save_file(const std::string &default_filename, const std::vector &filters, std::string &save_name_to); static std::string imgui_open_file(const std::vector& filters); + + static std::string win32_select_folder(const std::string &title); + static std::string imgui_save_file(const std::string& default_filename, - const std::vector& filters); + const std::vector& filters); }; } // namespace hellfire::Utility From 9f77c15d1b9366ebf97cd7cb33df6f3beaf3c796 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:09:25 +0100 Subject: [PATCH 14/27] - Changed the editor application to a statemachine, states are essentially screens. - ProjectHub State -> Can select, create and open projects - ProjectLoad State -> Shows information about creating/loading a project - Editor State -> Main Editor - Added serializers for the ProjectManager (Editor) --- editor/src/Events/StateEvents.h | 50 +++++ editor/src/Project/ProjectManager.cpp | 164 +++++++++++++++++ editor/src/Project/ProjectManager.h | 77 ++++++++ editor/src/States/Editor/EditorState.cpp | 100 ++++++++++ editor/src/States/Editor/EditorState.h | 36 ++++ .../ProjectCreator/ProjectCreatorState.cpp | 121 +++++++++++++ .../ProjectCreator/ProjectCreatorState.h | 24 +++ .../src/States/ProjectHub/ProjectHubState.cpp | 104 +++++++++++ .../src/States/ProjectHub/ProjectHubState.h | 26 +++ .../ProjectLoading/ProjectLoadingState.cpp | 49 +++++ .../ProjectLoading/ProjectLoadingState.h | 20 ++ editor/src/UI/EventBus.h | 70 +++++++ editor/src/UI/PanelManager.h | 41 +++++ editor/src/UI/Panels/EditorPanel.h | 34 +++- .../UI/Panels/Inspector/InspectorPanel.cpp | 5 +- .../UI/Panels/Projects/NewProjectPanel.cpp | 27 +++ .../src/UI/Panels/Projects/NewProjectPanel.h | 17 ++ editor/src/Utils/MoveOnlyFunction.h | 48 +++++ editor/src/core/ApplicationState.h | 34 ++++ editor/src/{ => core}/EditorApplication.cpp | 171 ++++++------------ editor/src/{ => core}/EditorApplication.h | 21 +-- editor/src/core/StateManager.h | 74 ++++++++ editor/src/main.cpp | 2 +- .../graphics/backends/opengl/Framebuffer.cpp | 3 + .../hellfire/serialization/SceneSerializer.h | 4 +- .../src/hellfire/serialization/Serializer.h | 30 ++- .../src/hellfire/utilities/SerializerUtils.h | 39 ++++ .../src/hellfire/utilities/ServiceLocator.h | 11 +- sandbox/src/main.cpp | 2 +- 29 files changed, 1248 insertions(+), 156 deletions(-) create mode 100644 editor/src/Events/StateEvents.h create mode 100644 editor/src/Project/ProjectManager.cpp create mode 100644 editor/src/Project/ProjectManager.h create mode 100644 editor/src/States/Editor/EditorState.cpp create mode 100644 editor/src/States/Editor/EditorState.h create mode 100644 editor/src/States/ProjectCreator/ProjectCreatorState.cpp create mode 100644 editor/src/States/ProjectCreator/ProjectCreatorState.h create mode 100644 editor/src/States/ProjectHub/ProjectHubState.cpp create mode 100644 editor/src/States/ProjectHub/ProjectHubState.h create mode 100644 editor/src/States/ProjectLoading/ProjectLoadingState.cpp create mode 100644 editor/src/States/ProjectLoading/ProjectLoadingState.h create mode 100644 editor/src/UI/EventBus.h create mode 100644 editor/src/UI/PanelManager.h create mode 100644 editor/src/UI/Panels/Projects/NewProjectPanel.cpp create mode 100644 editor/src/UI/Panels/Projects/NewProjectPanel.h create mode 100644 editor/src/Utils/MoveOnlyFunction.h create mode 100644 editor/src/core/ApplicationState.h rename editor/src/{ => core}/EditorApplication.cpp (51%) rename editor/src/{ => core}/EditorApplication.h (62%) create mode 100644 editor/src/core/StateManager.h create mode 100644 engine/src/hellfire/utilities/SerializerUtils.h diff --git a/editor/src/Events/StateEvents.h b/editor/src/Events/StateEvents.h new file mode 100644 index 0000000..10257e2 --- /dev/null +++ b/editor/src/Events/StateEvents.h @@ -0,0 +1,50 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once + +#include + +#include "UI/EventBus.h" + +namespace hellfire::editor { + + struct OpenProjectCreatorEvent { + using is_event_tag = void; + }; + + struct CancelProjectCreatorEvent { + using is_event_tag = void; + }; + + struct CreateProjectEvent { + using is_event_tag = void; + + std::string name; + std::filesystem::path location; + std::string template_id; + }; + + struct OpenProjectEvent { + using is_event_tag = void; + + std::filesystem::path path; + }; + + struct CloseProjectEvent { + using is_event_tag = void; + }; + + struct ProjectLoadProgressEvent { + using is_event_tag = void; + + std::string message; + float progress; // 0.0 to 1.0 + }; + + struct ProjectLoadCompleteEvent { + using is_event_tag = void; + + }; +} diff --git a/editor/src/Project/ProjectManager.cpp b/editor/src/Project/ProjectManager.cpp new file mode 100644 index 0000000..a38f6e7 --- /dev/null +++ b/editor/src/Project/ProjectManager.cpp @@ -0,0 +1,164 @@ +// +// Created by denzel on 05/12/2025. +// + +#include "ProjectManager.h" + +#include +#include + +#include "Events/StateEvents.h" +#include "hellfire/core/Time.h" +#include "Serializers/ProjectManagerSerializer.h" +#include "UI/Panels/EditorPanel.h" + +namespace hellfire::editor { + ProjectManager::ProjectManager(EventBus &event_bus, EditorContext &context) : event_bus_(event_bus), context_(context) { + + // Platform-specific config path +#ifdef _WIN32 + config_path_ = std::filesystem::path(getenv("APPDATA")) / "Hellfire"; +#else + config_path_ = std::filesystem::path(getenv("HOME")) / ".config" / "hellfire"; +#endif + + std::filesystem::create_directories(config_path_); + load_recent_projects(); + } + + void ProjectManager::create_project_async(const std::string &name, const std::filesystem::path &location, + const std::string &template_id) { + std::thread([this, name, location, template_id]() { + emit_progress("[INFO] Creating project folder: " + location.string(), 0.1f); + auto project = Project::create(name, location); + + if (!project) { + emit_progress("[ERROR] Failed to create project", 1.0f); + return; + } + + + emit_progress("[INFO] Generated " + name + ".hfproj", 0.3f); + emit_progress("[INFO] Created Assets/ folders", 0.5f); + if (template_id != "empty") { + emit_progress("[INFO] Applying template: " + template_id, 0.6f); + // Copy template scenes, scripts, etc. + } + emit_progress("[INFO] Created Scenes/SampleScene.hfscene", 0.8f); + emit_progress("[INFO] Initializing editor...", 0.9f); + // Switch to main thread for final setup + // add_to_recent(name, current_project_ ); + + context_.queue_main_thread([this, p = std::move(project)]() mutable { + current_project_ = std::move(p); + add_to_recent(current_project_->get_name(), current_project_->get_project_root() / "project.hfproj"); + emit_progress("[INFO] Done!", 1.0f); + event_bus_.dispatch(); + }); + + + }).detach(); + } + + void ProjectManager::open_project_async(const std::filesystem::path &project_file) { + std::thread([this, project_file]() { + emit_progress("[INFO] Loading project: " + project_file.string(), 0.2f); + + auto project = Project::load_data(project_file); + if (!project) { + emit_progress("[ERROR] Failed to load project", 1.0f); + return; + } + + emit_progress("[INFO] Done!", 1.0f); + + context_.queue_main_thread([this, p = std::move(project), project_file]() mutable { + current_project_ = std::move(p); + current_project_->initialize_managers(); + current_project_->get_metadata().last_opened = Time::get_current_timestamp(); + add_to_recent(current_project_->get_name(), project_file); + event_bus_.dispatch(); + }); + }).detach(); + } + + void ProjectManager::close_project() { + if (current_project_) { + current_project_->save(); + current_project_.reset(); + } + } + + std::vector ProjectManager::get_recent_projects() const { + std::vector result; + result.reserve(recent_projects_.size()); + + for (const auto& [path, project] : recent_projects_) { + result.push_back(project); + } + + // Sort by last_opened (most recent first) + std::sort(result.begin(), result.end(), [](const RecentProject& a, const RecentProject& b) { + return a.last_opened > b.last_opened; + }); + + return result; + } + + void ProjectManager::clear_recent_projects() { + recent_projects_.clear(); + } + + void ProjectManager::remove_from_recent(const std::filesystem::path &path) { + recent_projects_.erase(path); + save_recent_projects(); + } + + std::vector ProjectManager::get_templates() const { + return { + {"empty", "Empty", "A blank project with no assets"}, + {"3d_prototype", "3D Prototype Starter", "Basic 3D scene with camera and lighting"}, + {"3d_fps", "3D FPS Starter", "First person controller with basic movement"} + }; + } + + void ProjectManager::add_to_recent(const std::string &name, const std::filesystem::path &path) { + recent_projects_[path] = RecentProject{ + .name = name, + .path = path, + .last_opened = current_project_->get_metadata().last_opened, + .exists = true + }; + + // Keep max 10 - remove oldest if needed + if (recent_projects_.size() > 10) { + auto oldest = recent_projects_.begin(); + for (auto it = recent_projects_.begin(); it != recent_projects_.end(); ++it) { + if (it->second.last_opened < oldest->second.last_opened) { + oldest = it; + } + } + recent_projects_.erase(oldest); + } + + save_recent_projects(); + } + + void ProjectManager::load_recent_projects() { + std::ifstream file(get_recent_projects_path()); + if (!file.is_open()) return; + + Serializer>::deserialize(file, &recent_projects_); + } + + void ProjectManager::save_recent_projects() const { + std::ofstream file(get_recent_projects_path()); + if (!file.is_open()) return; + + Serializer>::serialize(file, &recent_projects_); + } + + void ProjectManager::emit_progress(const std::string &message, float progress) { + event_bus_.dispatch(message, progress); + } +} diff --git a/editor/src/Project/ProjectManager.h b/editor/src/Project/ProjectManager.h new file mode 100644 index 0000000..771ef08 --- /dev/null +++ b/editor/src/Project/ProjectManager.h @@ -0,0 +1,77 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include +#include + +#include "hellfire/core/Project.h" +#include "UI/EventBus.h" + +namespace hellfire::editor { + class EditorContext; + + struct RecentProject { + std::string name; + std::filesystem::path path; + std::string last_opened; + bool exists = true; + }; + + struct ProjectTemplate { + std::string id; + std::string name; + std::string description; + std::filesystem::path source_path; + }; + + struct PathHash { + std::size_t operator()(const std::filesystem::path& p) const { + return std::filesystem::hash_value(p); + } + }; + + // TODO: Add doxy gen comments + class ProjectManager { + public: + explicit ProjectManager(EventBus &event_bus, EditorContext &context); + + // Current project + Project *get_current_project() { return current_project_.get(); } + bool has_project() { return current_project_ != nullptr; } + + // Project create/open/close + void create_project_async(const std::string &name, + const std::filesystem::path &location, + const std::string &template_id); + + void open_project_async(const std::filesystem::path &project_file); + + void close_project(); + + // Recent projects + std::vector get_recent_projects() const; + void clear_recent_projects(); + void remove_from_recent(const std::filesystem::path& path); + std::filesystem::path get_recent_projects_path() const { + return config_path_ / "recent_projects.json"; + } + + // Templates + std::vector get_templates() const; + + private: + void add_to_recent(const std::string& name, const std::filesystem::path& path); + void load_recent_projects(); + void save_recent_projects() const; + + void emit_progress(const std::string& message, float progress); + + EventBus &event_bus_; + EditorContext &context_; + std::unique_ptr current_project_; + std::unordered_map recent_projects_; + std::filesystem::path config_path_; + }; +}; diff --git a/editor/src/States/Editor/EditorState.cpp b/editor/src/States/Editor/EditorState.cpp new file mode 100644 index 0000000..9a6c7c9 --- /dev/null +++ b/editor/src/States/Editor/EditorState.cpp @@ -0,0 +1,100 @@ +// +// Created by denzel on 05/12/2025. +// + +#include "EditorState.h" + +#include "hellfire/graphics/renderer/Renderer.h" +#include "hellfire/scene/SceneManager.h" +#include "Scenes/DefaultScene.h" +#include "UI/Panels/Inspector/InspectorPanel.h" +#include "UI/Panels/MenuBar/MenuBarComponent.h" +#include "UI/Panels/SceneHierarchy/SceneHierarchyPanel.h" +#include "UI/Panels/Settings/Renderer/RendererSettingsPanel.h" +#include "UI/Panels/Viewport/SceneCameraScript.h" +#include "UI/Panels/Viewport/ViewportPanel.h" + +namespace hellfire::editor { + void EditorState::on_enter() { + // Make sure the renderer render's the scene to a framebuffer + ServiceLocator::get_service()->set_render_to_framebuffer(true); + + // Initialize and set context for UI components + menu_bar_ = std::make_unique(context_); + panel_manager_.add_panel(); + panel_manager_.add_panel(); + panel_manager_.add_panel(); + viewport_panel_ = panel_manager_.add_panel(); + + panel_manager_.set_context(context_); + } + + void EditorState::on_exit() { + ApplicationState::on_exit(); + } + + void EditorState::render() { + // Create main dockspace + create_dockspace(); + + + + panel_manager_.render_all(); + } + + void EditorState::create_dockspace() { + const ImGuiViewport *viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::SetNextWindowViewport(viewport->ID); + + const ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + + ImGui::Begin("DockSpace", nullptr, window_flags); + ImGui::PopStyleVar(3); + + menu_bar_->render(); + // Create dockspace + const ImGuiID dockspace_id = ImGui::GetID("MainDockSpace"); + ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + + ImGui::End(); + } + + bool EditorState::on_mouse_move(float x, float y, float x_offset, float y_offset) { + if (viewport_panel_ && viewport_panel_->is_editor_camera_active()) { + if (const auto* camera = viewport_panel_->get_editor_camera()) { + if (auto* script = camera->get_component()) { + script->handle_mouse_movement(x_offset, y_offset); + } + } + return true; + } + + if (ImGui::GetIO().WantCaptureMouse) { + return true; + } + + return false; + } + + bool EditorState::on_mouse_wheel(float delta) { + if (ImGui::GetIO().WantCaptureMouse) { + return true; + } + + return false; + } + + Entity* EditorState::get_render_camera_override() { + return viewport_panel_ ? viewport_panel_->get_editor_camera() : nullptr; + } +} diff --git a/editor/src/States/Editor/EditorState.h b/editor/src/States/Editor/EditorState.h new file mode 100644 index 0000000..603c661 --- /dev/null +++ b/editor/src/States/Editor/EditorState.h @@ -0,0 +1,36 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include "core/ApplicationState.h" +#include "UI/PanelManager.h" +#include "UI/Panels/MenuBar/MenuBarComponent.h" +#include "UI/Panels/Projects/NewProjectPanel.h" +#include "UI/Panels/Viewport/ViewportPanel.h" + +namespace hellfire::editor { + class EditorState : public ApplicationState { + public: + void on_enter() override; + void on_exit() override; + void render() override; + + private: + void create_dockspace(); + + public: + bool on_mouse_move(float x, float y, float x_offset, float y_offset) override; + + bool on_mouse_wheel(float delta) override; + + Entity * get_render_camera_override() override; + + private: + PanelManager panel_manager_; + ViewportPanel* viewport_panel_ = nullptr; // Raw pointer for quick access + + // UI Components + std::unique_ptr menu_bar_; + }; +} diff --git a/editor/src/States/ProjectCreator/ProjectCreatorState.cpp b/editor/src/States/ProjectCreator/ProjectCreatorState.cpp new file mode 100644 index 0000000..7ba485b --- /dev/null +++ b/editor/src/States/ProjectCreator/ProjectCreatorState.cpp @@ -0,0 +1,121 @@ +// +// Created by denzel on 05/12/2025. +// + +#include "ProjectCreatorState.h" + +#include "IconsFontAwesome6.h" +#include "imgui.h" +#include "Events/StateEvents.h" +#include "UI/ui.h" +#include "UI/Panels/EditorPanel.h" + +namespace hellfire::editor { + void ProjectCreatorState::on_enter() { + // Reset form + memset(project_name_, 0, sizeof(project_name_)); + memset(project_location_, 0, sizeof(project_location_)); + selected_template_ = 0; + + templates_ = context_->project_manager->get_templates(); + } + + void ProjectCreatorState::render() { + const ImGuiViewport *viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + + if (ui::Window window{ + "Hellfire - Project Hub", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize + }) { + // Center the form + float form_width = 500.0f; + float offset_x = (ImGui::GetContentRegionAvail().x - form_width) * 0.5f; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offset_x); + + if (ui::ChildWindow child{"CreatorForm", ImVec2(form_width, 0)}) { + ImGui::Dummy(ImVec2(0, 50)); // Top padding + + render_form(); + + ImGui::Dummy(ImVec2(0, 20)); + render_buttons(); + } + } + } + + void ProjectCreatorState::render_form() { + float label_width = 120.0f; + + // Project Name + ImGui::Text("Project Name"); + ImGui::SameLine(label_width); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##name", project_name_, sizeof(project_name_)); + + ImGui::Dummy(ImVec2(0, 5)); + + // Project Location + ImGui::Text("Project Location"); + ImGui::SameLine(label_width); + ImGui::SetNextItemWidth(-30); + ImGui::InputText("##location", project_location_, sizeof(project_location_)); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER)) { + std::string folder = Utility::FileDialog::select_folder("Select Project Folder"); + if (!folder.empty()) { + strncpy_s(project_location_, 256, folder.c_str(), 255); + } + } + + ImGui::Dummy(ImVec2(0, 5)); + + if (strlen(project_name_) > 0 && strlen(project_location_) > 0) { + ImGui::TextDisabled("Will create:"); + ImGui::SameLine(label_width); + ImGui::TextDisabled("%s\\%s", project_location_, project_name_); + } + + ImGui::Dummy(ImVec2(0, 5)); + + // Template + ImGui::Text("Project Template"); + ImGui::SameLine(label_width); + ImGui::SetNextItemWidth(-1); + + if (ImGui::BeginListBox("##templates", ImVec2(-1, 80))) { + for (int i = 0; i < templates_.size(); i++) { + bool selected = (selected_template_ == i); + if (ImGui::Selectable(templates_[i].name.c_str(), selected)) { + selected_template_ = i; + } + } + ImGui::EndListBox(); + } + } + + void ProjectCreatorState::render_buttons() { + float button_width = 100.0f; + float total_width = button_width * 2 + ImGui::GetStyle().ItemSpacing.x; + + ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - total_width); + + bool can_create = strlen(project_name_) > 0 && strlen(project_location_) > 0; + + ImGui::BeginDisabled(!can_create); + if (ImGui::Button("Create", ImVec2(button_width, 0))) { + context_->event_bus.dispatch( + project_name_, + std::filesystem::path(project_location_), + templates_[selected_template_].id + ); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(button_width, 0))) { + context_->event_bus.dispatch(); + } + } +} diff --git a/editor/src/States/ProjectCreator/ProjectCreatorState.h b/editor/src/States/ProjectCreator/ProjectCreatorState.h new file mode 100644 index 0000000..54b8004 --- /dev/null +++ b/editor/src/States/ProjectCreator/ProjectCreatorState.h @@ -0,0 +1,24 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include "core/ApplicationState.h" +#include "Project/ProjectManager.h" + +namespace hellfire::editor { + class ProjectCreatorState : public ApplicationState { + public: + void on_enter() override; + void render() override; + + private: + void render_form(); + void render_buttons(); + + char project_name_[256] = ""; + char project_location_[512] = ""; + std::vector templates_; + int selected_template_ = 0; + }; +} diff --git a/editor/src/States/ProjectHub/ProjectHubState.cpp b/editor/src/States/ProjectHub/ProjectHubState.cpp new file mode 100644 index 0000000..b5f280e --- /dev/null +++ b/editor/src/States/ProjectHub/ProjectHubState.cpp @@ -0,0 +1,104 @@ +// +// Created by denzel on 05/12/2025. +// + +#include "ProjectHubState.h" + +#include "IconsFontAwesome6.h" +#include "imgui.h" +#include "Events/StateEvents.h" +#include "UI/ui.h" +#include "UI/Panels/EditorPanel.h" + +namespace hellfire::editor { + void ProjectHubState::on_enter() { + recent_projects_ = context_->project_manager->get_recent_projects(); + } + + void ProjectHubState::render() { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize; + + if (ui::Window window{"Hellfire - Project Hub", nullptr, flags}) { + if (ImGui::BeginTable("HubLayout", 2, ImGuiTableFlags_Resizable)) { + ImGui::TableSetupColumn("Sidebar", ImGuiTableColumnFlags_WidthFixed, 150.0f); + ImGui::TableSetupColumn("Content", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + + // Sidebar + ImGui::TableNextColumn(); + render_sidebar(); + + // Content + ImGui::TableNextColumn(); + render_buttons(); + ImGui::Separator(); + render_project_list(); + + ImGui::EndTable(); + } + } + } + + void ProjectHubState::render_sidebar() { + if (ImGui::Selectable("Projects", selected_tab_ == 0)) { + selected_tab_ = 0; + } + if (ImGui::Selectable("Documentation", selected_tab_ == 1)) { + selected_tab_ = 1; + } + } + + void ProjectHubState::render_project_list() { + if (ui::ChildWindow child{"ProjectList"}) { + for (const auto& project : recent_projects_) { + ImGui::PushID(&project); + + bool selected = false; + if (ImGui::Selectable("##project", &selected, ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, 30))) { + if (ImGui::IsMouseDoubleClicked(0)) { + context_->event_bus.dispatch(project.path); + } + } + + ImGui::SameLine(); + ImGui::Text("%s", project.name.c_str()); + ImGui::SameLine(120); + ImGui::Text("%s", project.path.string().c_str()); + + if (ImGui::BeginPopupContextWindow(("##" + project.path.string()).c_str())) { + if (ImGui::MenuItem("Remove from recent projects...")) { + context_->project_manager->remove_from_recent(project.path); + recent_projects_ = context_->project_manager->get_recent_projects(); + } + ImGui::EndPopup(); + } + + ImGui::PopID(); + } + } + } + + void ProjectHubState::render_buttons() { + float button_width = 120.0f; + float spacing = ImGui::GetContentRegionAvail().x - (button_width * 2) - ImGui::GetStyle().ItemSpacing.x; + + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + spacing); + + if (ImGui::Button("New Project", ImVec2(button_width, 0))) { + context_->event_bus.dispatch(); + } + ImGui::SameLine(); + if (ImGui::Button("Open Project", ImVec2(button_width, 0))) { + // Open file dialog, then emit OpenProjectEvent + hellfire::Utility::FileFilter project_ext_filter = {"Hellfire Project", "*.hfproj"}; + const std::string path_to_proj = Utility::FileDialog::open_file({project_ext_filter}); + + context_->event_bus.dispatch(path_to_proj); + } + } +} diff --git a/editor/src/States/ProjectHub/ProjectHubState.h b/editor/src/States/ProjectHub/ProjectHubState.h new file mode 100644 index 0000000..8aac3c8 --- /dev/null +++ b/editor/src/States/ProjectHub/ProjectHubState.h @@ -0,0 +1,26 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include + +#include "core/ApplicationState.h" +#include "Project/ProjectManager.h" + +namespace hellfire::editor { + + class ProjectHubState : public ApplicationState { + public: + void on_enter() override; + void render() override; + + private: + void render_sidebar(); + void render_project_list(); + void render_buttons(); + + std::vector recent_projects_; + int selected_tab_ = 0; + }; +} diff --git a/editor/src/States/ProjectLoading/ProjectLoadingState.cpp b/editor/src/States/ProjectLoading/ProjectLoadingState.cpp new file mode 100644 index 0000000..75c6373 --- /dev/null +++ b/editor/src/States/ProjectLoading/ProjectLoadingState.cpp @@ -0,0 +1,49 @@ +// +// Created by denzel on 05/12/2025. +// + +#include "ProjectLoadingState.h" + +#include "imgui.h" +#include "Events/StateEvents.h" +#include "UI/ui.h" +#include "UI/Panels/EditorPanel.h" + +namespace hellfire::editor { + void ProjectLoadingState::on_enter() { + log_messages_.clear(); + progress_ = 0.0f; + + context_->event_bus.subscribe([this](const ProjectLoadProgressEvent &e) { + log_messages_.push_back(e.message); + progress_ = e.progress; + }); + } + + void ProjectLoadingState::render() { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + + if (ui::Window window{"Creating Project", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize}) { + + // Progress bar + ImGui::ProgressBar(progress_, ImVec2(-1, 0)); + + ImGui::Dummy(ImVec2(0, 10)); + + // Log output + if (ui::ChildWindow child{"LogOutput", ImVec2(-1, -1), ImGuiChildFlags_Border}) { + for (const auto& msg : log_messages_) { + ImGui::TextUnformatted(msg.c_str()); + } + + // Auto-scroll + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { + ImGui::SetScrollHereY(1.0f); + } + } + } + } +} \ No newline at end of file diff --git a/editor/src/States/ProjectLoading/ProjectLoadingState.h b/editor/src/States/ProjectLoading/ProjectLoadingState.h new file mode 100644 index 0000000..ea227f3 --- /dev/null +++ b/editor/src/States/ProjectLoading/ProjectLoadingState.h @@ -0,0 +1,20 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include "core/ApplicationState.h" + +namespace hellfire::editor { + +class ProjectLoadingState : public ApplicationState { +public: + void on_enter() override; + void render() override; + +private: + std::vector log_messages_; + float progress_ = 0.0f; +}; + +} diff --git a/editor/src/UI/EventBus.h b/editor/src/UI/EventBus.h new file mode 100644 index 0000000..960c9b4 --- /dev/null +++ b/editor/src/UI/EventBus.h @@ -0,0 +1,70 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include +#include +#include +#include + + +namespace hellfire::editor { + template + concept IsEvent = requires + { + typename T::is_event_tag; + }; + + class EventBus { + public: + using EventSubscribers = std::unordered_map > >; + using EventQueue = std::vector > >; + + template + using Callback = std::function; + + template + void subscribe(Callback callback) { + auto &vec = subscribers_[typeid(T)]; + vec.push_back([cb = std::move(callback)](const void *event) { + cb(*static_cast(event)); + }); + } + + template + void publish(const T &event) { + const auto it = subscribers_.find(typeid(T)); + if (it == subscribers_.end()) return; + + for (auto &fn: it->second) { + fn(&event); + } + } + + template + void dispatch(Args &&... args) { + publish(T{std::forward(args)...}); + } + + template + void queue(const T &event) { + queue_.emplace_back(typeid(T), std::make_shared(event)); + } + + void dispatch_queued() { + for (auto &[type, event_ptr]: queue_) { + if (auto it = subscribers_.find(type); it != subscribers_.end()) { + for (auto &fn: it->second) { + fn(event_ptr.get()); + } + } + } + queue_.clear(); + } + + private: + EventSubscribers subscribers_; + EventQueue queue_; + }; +} diff --git a/editor/src/UI/PanelManager.h b/editor/src/UI/PanelManager.h new file mode 100644 index 0000000..d01bc5f --- /dev/null +++ b/editor/src/UI/PanelManager.h @@ -0,0 +1,41 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include +#include +#include + +#include "Panels/EditorPanel.h" + +namespace hellfire::editor { + template + concept IsPanel = std::derived_from; + + class PanelManager { + public: + template + T* add_panel(Args&&... args) { + auto panel = std::make_unique(std::forward(args)...); + T* ptr = panel.get(); + panels_.push_back(std::move(panel)); + return ptr; + } + + void set_context(EditorContext* ctx) { + for (auto& panel : panels_) { + panel->set_context(ctx); + } + } + + void render_all() const { + for (const auto& panel : panels_) { + panel->render(); + } + } + + private: + std::vector> panels_; + }; +} diff --git a/editor/src/UI/Panels/EditorPanel.h b/editor/src/UI/Panels/EditorPanel.h index e0d5f8a..ae08f03 100644 --- a/editor/src/UI/Panels/EditorPanel.h +++ b/editor/src/UI/Panels/EditorPanel.h @@ -3,20 +3,42 @@ // #pragma once +#include +#include + #include "hellfire/ecs/Entity.h" #include "hellfire/platform/IWindow.h" #include "hellfire/scene/Scene.h" #include "hellfire/utilities/ServiceLocator.h" +#include "Project/ProjectManager.h" +#include "UI/EventBus.h" +#include "Utils/MoveOnlyFunction.h" namespace hellfire::editor { class EditorContext { public: EntityID selected_entity_id = INVALID_ENTITY; - std::vector selected_entity_ids; Scene* active_scene = nullptr; + EventBus event_bus; + std::unique_ptr project_manager; - EntityID get_primary_selection() const { - return selected_entity_ids.empty() ? 0 : selected_entity_ids.front(); + // Thread-safe main thread queue + void queue_main_thread(utils::MoveOnlyFunction fn) { + std::lock_guard lock(main_thread_mutex_); + main_thread_queue_.push(std::move(fn)); + } + + void process_main_thread_queue() { + std::queue to_process; + + { + std::lock_guard lock(main_thread_mutex_); + std::swap(to_process, main_thread_queue_); + } + while (!to_process.empty()) { + to_process.front()(); + to_process.pop(); + } } void set_window_title(const std::string& title) { @@ -24,6 +46,10 @@ namespace hellfire::editor { window->set_title(title + " - Hellfire Editor"); } } + + private: + std::mutex main_thread_mutex_; + std::queue main_thread_queue_; }; class EditorPanel { @@ -31,9 +57,11 @@ namespace hellfire::editor { EditorContext* context_ = nullptr; public: virtual ~EditorPanel() = default; + EditorPanel(EditorContext* ctx = nullptr) : context_(ctx) {} virtual void set_context(EditorContext* ctx) { context_ = ctx; } virtual void render() = 0; virtual void on_entity_selected(Entity* entity) {} + }; } diff --git a/editor/src/UI/Panels/Inspector/InspectorPanel.cpp b/editor/src/UI/Panels/Inspector/InspectorPanel.cpp index b702fdb..82c3f2c 100644 --- a/editor/src/UI/Panels/Inspector/InspectorPanel.cpp +++ b/editor/src/UI/Panels/Inspector/InspectorPanel.cpp @@ -51,12 +51,11 @@ namespace hellfire::editor { void InspectorPanel::render() { if (!context_->active_scene) return; // Don't show if there's no active scene selected auto active_scene = context_->active_scene; - if (ImGui::Begin("Inspector")) { + if (ui::Window window{"Inspector"}) { auto *selected_entity = active_scene->get_entity(context_->selected_entity_id); if (!selected_entity) { ImGui::TextDisabled("No entity selected"); - ImGui::End(); return; } @@ -106,8 +105,6 @@ namespace hellfire::editor { ImGui::OpenPopup("AddComponentPopup"); } ImGui::EndGroup(); - - ImGui::End(); } } diff --git a/editor/src/UI/Panels/Projects/NewProjectPanel.cpp b/editor/src/UI/Panels/Projects/NewProjectPanel.cpp new file mode 100644 index 0000000..e4c2676 --- /dev/null +++ b/editor/src/UI/Panels/Projects/NewProjectPanel.cpp @@ -0,0 +1,27 @@ +// +// Created by denzel on 05/12/2025. +// + +#include "NewProjectPanel.h" + +#include "UI/ui.h" +#include "Events/WindowEvents.h" + +namespace hellfire::editor { + void NewProjectPanel::render() { + if (!should_open_) return; + + if (ui::Window window{"New Project"}) { + + ImGui::Text("Hello Test"); + } + } + + void NewProjectPanel::set_context(EditorContext *ctx) { + EditorPanel::set_context(ctx); + + context_->event_bus.subscribe([this](const OpenNewProjectWindowEvent) { + this->should_open_ = true; + }); + } +} diff --git a/editor/src/UI/Panels/Projects/NewProjectPanel.h b/editor/src/UI/Panels/Projects/NewProjectPanel.h new file mode 100644 index 0000000..16ca203 --- /dev/null +++ b/editor/src/UI/Panels/Projects/NewProjectPanel.h @@ -0,0 +1,17 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include "UI/Panels/EditorPanel.h" + +namespace hellfire::editor { + class NewProjectPanel : EditorPanel { + public: + void render() override; + + void set_context(EditorContext *ctx) override; + + bool should_open_ = false; + }; +} diff --git a/editor/src/Utils/MoveOnlyFunction.h b/editor/src/Utils/MoveOnlyFunction.h new file mode 100644 index 0000000..8212c7f --- /dev/null +++ b/editor/src/Utils/MoveOnlyFunction.h @@ -0,0 +1,48 @@ +// +// Created by denzel on 06/12/2025. +// + +#pragma once +#include +#include + +namespace hellfire::editor::utils { + + + class MoveOnlyFunction { + public: + MoveOnlyFunction() = default; + + template + MoveOnlyFunction(F&& f) + : impl_(std::make_unique>(std::forward(f))) {} + + MoveOnlyFunction(MoveOnlyFunction&&) = default; + MoveOnlyFunction& operator=(MoveOnlyFunction&&) = default; + + // Non-copyable + MoveOnlyFunction(const MoveOnlyFunction&) = delete; + MoveOnlyFunction& operator=(const MoveOnlyFunction&) = delete; + + void operator()() { + if (impl_) impl_->call(); + } + + explicit operator bool() const { return impl_ != nullptr; } + + private: + struct Concept { + virtual ~Concept() = default; + virtual void call() = 0; + }; + + template + struct Model : Concept { + F func; + Model(F&& f) : func(std::forward(f)) {} + void call() override { func(); } + }; + + std::unique_ptr impl_; + }; +} \ No newline at end of file diff --git a/editor/src/core/ApplicationState.h b/editor/src/core/ApplicationState.h new file mode 100644 index 0000000..0a8c540 --- /dev/null +++ b/editor/src/core/ApplicationState.h @@ -0,0 +1,34 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include "hellfire/ecs/Entity.h" + +namespace hellfire::editor { + class EditorContext; + + class ApplicationState { + public: + virtual ~ApplicationState() = default; + + virtual void on_enter() {} + virtual void on_exit() {} + virtual void render() = 0; + + // Input handling - return true if consumed + virtual bool on_mouse_move(float x, float y, float x_offset, float y_offset) { return false; } + virtual bool on_mouse_button(int button, bool pressed) { return false; } + virtual bool on_mouse_wheel(float delta) { return false; } + virtual bool on_key_down(int key) { return false; } + virtual bool on_key_up(int key) { return false; } + + // Optional overrides + virtual Entity* get_render_camera_override() { return nullptr; } + + void set_context(EditorContext* ctx) { context_ = ctx; } + + protected: + EditorContext* context_ = nullptr; + }; +} diff --git a/editor/src/EditorApplication.cpp b/editor/src/core/EditorApplication.cpp similarity index 51% rename from editor/src/EditorApplication.cpp rename to editor/src/core/EditorApplication.cpp index b841eac..e74c4c3 100644 --- a/editor/src/EditorApplication.cpp +++ b/editor/src/core/EditorApplication.cpp @@ -3,23 +3,23 @@ // #include "EditorApplication.h" -#include "EditorStyles.h" +#include "../EditorStyles.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include "imgui.h" #include "ImGuizmo.h" -#include "UI/Panels/EditorPanel.h" -#include "UI/Panels/MenuBar/MenuBarComponent.h" -#include "UI/Panels/SceneHierarchy/SceneHierarchyPanel.h" +#include "../UI/Panels/EditorPanel.h" #include "hellfire/core/Application.h" #include "hellfire/platform/IWindow.h" #include "hellfire/utilities/ServiceLocator.h" #include "hellfire/platform/windows_linux/GLFWWindow.h" -#include "UI/Panels/Viewport/SceneCameraScript.h" -#include "UI/Panels/Viewport/ViewportPanel.h" -#include "IconsFontAwesome6.h" -#include "Scenes/DefaultScene.h" -#include "UI/Panels/Settings/Renderer/RendererSettingsPanel.h" +#include "../IconsFontAwesome6.h" +#include "../Scenes/DefaultScene.h" +#include "Events/StateEvents.h" +#include "../States/Editor/EditorState.h" +#include "../States/ProjectHub/ProjectHubState.h" +#include "States/ProjectCreator/ProjectCreatorState.h" +#include "States/ProjectLoading/ProjectLoadingState.h" namespace hellfire::editor { void EditorApplication::on_initialize(Application &app) { @@ -35,30 +35,44 @@ namespace hellfire::editor { initialize_imgui(window); - // Make sure the renderer render's the scene to a framebuffer - ServiceLocator::get_service()->set_render_to_framebuffer(true); - - // Initialize and set context for UI components - menu_bar_ = std::make_unique(); - menu_bar_->set_context(&editor_context_); - - scene_hierarchy_ = std::make_unique(); - scene_hierarchy_->set_context(&editor_context_); - - scene_viewport_ = std::make_unique(); - scene_viewport_->set_context(&editor_context_); - - inspector_panel_ = std::make_unique(); - inspector_panel_->set_context(&editor_context_); - - renderer_settings_panel_ = std::make_unique(); - renderer_settings_panel_->set_context(&editor_context_); - - // TEMP: - const auto sm = ServiceLocator::get_service(); - const auto new_scene = sm->create_scene("Test"); - setup_default_scene_with_default_entities(new_scene); - sm->set_active_scene(new_scene, false); // Don't play in editor mode as default + state_manager_.register_state(); + state_manager_.register_state(); + state_manager_.register_state(); + state_manager_.register_state(); + state_manager_.set_context(&editor_context_); + editor_context_.project_manager = std::make_unique(editor_context_.event_bus, editor_context_); + + auto* pm = editor_context_.project_manager.get(); + // Subscribe to state transitions + editor_context_.event_bus.subscribe([this](const auto&) { + state_manager_.switch_to(); + }); + + editor_context_.event_bus.subscribe([this](const auto&) { + state_manager_.switch_to(); + }); + + editor_context_.event_bus.subscribe([this, pm](const CreateProjectEvent& e) { + state_manager_.switch_to(); + pm->create_project_async(e.name, e.location, e.template_id); + }); + + editor_context_.event_bus.subscribe([this, pm](const OpenProjectEvent& e) { + state_manager_.switch_to(); + pm->open_project_async(e.path); + }); + + editor_context_.event_bus.subscribe([this](const auto&) { + state_manager_.switch_to(); + }); + + editor_context_.event_bus.subscribe([this, pm](const auto&) { + pm->close_project(); + state_manager_.switch_to(); + }); + + // Start with project selection state + state_manager_.switch_to(); } void EditorApplication::initialize_imgui(IWindow *window) { @@ -69,7 +83,7 @@ namespace hellfire::editor { // Load default font io.Fonts->AddFontFromFileTTF("assets/fonts/Roboto-Regular.ttf", 16.0f); - static const ImWchar icons_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; + static constexpr ImWchar icons_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; ImFontConfig icons_config; icons_config.MergeMode = true; icons_config.PixelSnapH = true; @@ -86,6 +100,8 @@ namespace hellfire::editor { io.ConfigViewportsNoDecoration = false; io.ConfigViewportsNoTaskBarIcon = false; io.ConfigViewportsNoAutoMerge = false; + + io.ConfigErrorRecoveryEnableAssert = true; // Setup style styles::SetupDarkRedModeStyle(); @@ -103,7 +119,7 @@ namespace hellfire::editor { // Setup Platform/Renderer backends ImGui_ImplGlfw_InitForOpenGL(glfw_window, true); - ImGui_ImplOpenGL3_Init("#version 330"); + ImGui_ImplOpenGL3_Init("#version 440"); imgui_initialized_ = true; } @@ -138,7 +154,7 @@ namespace hellfire::editor { // Handle multi-viewport if (const ImGuiIO &io = ImGui::GetIO(); io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { - IWindow *window = ServiceLocator::get_service(); + auto *window = ServiceLocator::get_service(); ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); window->make_current(); @@ -154,99 +170,32 @@ namespace hellfire::editor { void EditorApplication::on_render() { if (!imgui_initialized_) return; + editor_context_.process_main_thread_queue(); // Sync editor context with scene manager sync_editor_context(); - // Create main dockspace - create_dockspace(); - - inspector_panel_->render(); - renderer_settings_panel_->render(); - scene_viewport_->render(); - scene_hierarchy_->render(); - } - - void EditorApplication::create_dockspace() { - ImGuiViewport *viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->Pos); - ImGui::SetNextWindowSize(viewport->Size); - ImGui::SetNextWindowViewport(viewport->ID); - - ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking | - ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNavFocus; - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - - ImGui::Begin("DockSpace", nullptr, window_flags); - ImGui::PopStyleVar(3); - - menu_bar_->render(); - // Create dockspace - ImGuiID dockspace_id = ImGui::GetID("MainDockSpace"); - ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); - - ImGui::End(); + state_manager_.render(); } bool EditorApplication::on_key_down(int key) { - ImGuiIO &io = ImGui::GetIO(); - if (io.WantCaptureKeyboard) { - return true; // ImGui consumed the input - } - - return false; + return state_manager_.on_key_down(key); } bool EditorApplication::on_key_up(int key) { - ImGuiIO &io = ImGui::GetIO(); - if (io.WantCaptureKeyboard) { - return true; - } - - return false; + return state_manager_.on_key_up(key); } bool EditorApplication::on_mouse_button(int button, bool pressed) { - ImGuiIO &io = ImGui::GetIO(); - if (io.WantCaptureMouse) { - return true; - } - - return false; + return state_manager_.on_mouse_button(button, pressed); } bool EditorApplication::on_mouse_move(float x, float y, float x_offset, float y_offset) { - if (scene_viewport_->is_editor_camera_active()) { - // Update camera with offset - if (const auto *camera = scene_viewport_->get_editor_camera()) { - if (auto *script = camera->get_component()) { - script->handle_mouse_movement(x_offset, y_offset); - } - } - return true; // consumed - } - - ImGuiIO &io = ImGui::GetIO(); - if (io.WantCaptureMouse) { - return true; - } - - return false; + return state_manager_.on_mouse_move(x, y, x_offset, y_offset); } bool EditorApplication::on_mouse_wheel(float delta) { - ImGuiIO &io = ImGui::GetIO(); - if (io.WantCaptureMouse) { - return true; - } - - return false; + return state_manager_.on_mouse_wheel(delta); } void EditorApplication::on_window_resize(int width, int height) { @@ -258,6 +207,6 @@ namespace hellfire::editor { } Entity *EditorApplication::get_render_camera_override() { - return scene_viewport_->get_editor_camera(); + return state_manager_.get_render_camera_override(); } } diff --git a/editor/src/EditorApplication.h b/editor/src/core/EditorApplication.h similarity index 62% rename from editor/src/EditorApplication.h rename to editor/src/core/EditorApplication.h index 171fc57..3598e87 100644 --- a/editor/src/EditorApplication.h +++ b/editor/src/core/EditorApplication.h @@ -3,16 +3,12 @@ // #pragma once -#include -#include "UI/Panels/EditorPanel.h" -#include "UI/Panels/MenuBar/MenuBarComponent.h" -#include "UI/Panels/SceneHierarchy/SceneHierarchyPanel.h" +#include "StateManager.h" +#include "../UI/Panels/EditorPanel.h" #include "hellfire/Interfaces/IApplicationPlugin.h" #include "hellfire/platform/IWindow.h" -#include "UI/Panels/Inspector/InspectorPanel.h" -#include "UI/Panels/Settings/Renderer/RendererSettingsPanel.h" -#include "UI/Panels/Viewport/ViewportPanel.h" +#include "../UI/Panels/Inspector/InspectorPanel.h" namespace hellfire::editor { class EditorApplication final : public IApplicationPlugin { @@ -33,8 +29,6 @@ namespace hellfire::editor { void on_render() override; - void create_dockspace(); - bool on_key_down(int key) override; bool on_key_up(int key) override; @@ -54,12 +48,7 @@ namespace hellfire::editor { private: EditorContext editor_context_; bool imgui_initialized_ = false; - - // UI Components - std::unique_ptr menu_bar_; - std::unique_ptr scene_hierarchy_; - std::unique_ptr scene_viewport_; - std::unique_ptr inspector_panel_; - std::unique_ptr renderer_settings_panel_; + + StateManager state_manager_; }; } diff --git a/editor/src/core/StateManager.h b/editor/src/core/StateManager.h new file mode 100644 index 0000000..bfc3702 --- /dev/null +++ b/editor/src/core/StateManager.h @@ -0,0 +1,74 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once +#include +#include +#include +#include + +#include "ApplicationState.h" + +namespace hellfire::editor { + class StateManager { + public: + template + void register_state(Args&&... args) { + auto state = std::make_unique(std::forward(args)...); + states_[std::type_index(typeid(T))] = std::move(state); + } + + template + void switch_to() { + auto it = states_.find(std::type_index(typeid(T))); + if (it == states_.end()) return; + + if (current_state_) { + current_state_->on_exit(); + } + current_state_ = it->second.get(); + current_state_->on_enter(); + } + + void set_context(EditorContext* ctx) { + for (const auto &state: states_ | std::views::values) { + state->set_context(ctx); + } + } + + void render() const { + if (current_state_) { + current_state_->render(); + } + } + + bool on_mouse_move(const float x, const float y, const float x_offset, const float y_offset) const { + return current_state_ ? current_state_->on_mouse_move(x, y, x_offset, y_offset) : false; + } + + bool on_mouse_button(const int button, const bool pressed) const { + return current_state_ ? current_state_->on_mouse_button(button, pressed) : false; + } + + bool on_mouse_wheel(const float delta) const { + return current_state_ ? current_state_->on_mouse_wheel(delta) : false; + } + + bool on_key_down(const int key) const { + return current_state_ ? current_state_->on_key_down(key) : false; + } + + bool on_key_up(const int key) const { + return current_state_ ? current_state_->on_key_up(key) : false; + } + + Entity* get_render_camera_override() const { + return current_state_ ? current_state_->get_render_camera_override() : nullptr; + } + + private: + std::unordered_map> states_; + ApplicationState* current_state_ = nullptr; + }; +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index ec3f612..22b1190 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -2,7 +2,7 @@ // Created by denzel on 17/09/2025. // -#include "EditorApplication.h" +#include "core/EditorApplication.h" #include "hellfire/EntryPoint.h" namespace { diff --git a/engine/src/hellfire/graphics/backends/opengl/Framebuffer.cpp b/engine/src/hellfire/graphics/backends/opengl/Framebuffer.cpp index 41863f8..bd292f8 100644 --- a/engine/src/hellfire/graphics/backends/opengl/Framebuffer.cpp +++ b/engine/src/hellfire/graphics/backends/opengl/Framebuffer.cpp @@ -185,6 +185,9 @@ namespace hellfire { } uint32_t Framebuffer::read_pixel_from_texture(const uint32_t texture_id, const int x, const int y) { + std::cout << "FBO ID: " << framebuffer_id_<< ", is valid: " << (glIsFramebuffer(framebuffer_id_) ? "yes" : "no") << std::endl; + std::cout << "Texture ID: " << texture_id << ", is valid: " << (glIsTexture(texture_id) ? "yes" : "no") << std::endl; + // Bind the framebuffer bind(); // Attach the texture to the framebuffer diff --git a/engine/src/hellfire/serialization/SceneSerializer.h b/engine/src/hellfire/serialization/SceneSerializer.h index 576bf35..4b71466 100644 --- a/engine/src/hellfire/serialization/SceneSerializer.h +++ b/engine/src/hellfire/serialization/SceneSerializer.h @@ -81,7 +81,9 @@ namespace hellfire { } Entity* entity = scene.get_entity(new_id); - ComponentRegistry::instance().deserialize_all_components(*entity, entity_json); + if (entity_json.contains("components")) { + ComponentRegistry::instance().deserialize_all_components(*entity, entity_json.at("components")); + } if (entity_json.contains("children")) { for (const auto& child_json : entity_json.at("children")) { diff --git a/engine/src/hellfire/serialization/Serializer.h b/engine/src/hellfire/serialization/Serializer.h index f4a436a..66d2911 100644 --- a/engine/src/hellfire/serialization/Serializer.h +++ b/engine/src/hellfire/serialization/Serializer.h @@ -7,17 +7,7 @@ #include "hellfire/ecs/TransformComponent.h" #include "hellfire/scene/Scene.h" - -// GLM Helper extensions -namespace glm { - inline void to_json(nlohmann::json& j, const vec3& v) { - j = {v.x, v.y, v.z}; - } - - inline void from_json(const nlohmann::json& j, vec3& v) { - v = {j[0].get(), j[1].get(), j[2].get()}; - } -} +#include "hellfire/utilities/SerializerUtils.h" namespace hellfire { template @@ -30,8 +20,8 @@ namespace hellfire { struct Serializer { static bool serialize(std::ostream& output, const TransformComponent* obj) { if (obj == nullptr) return false; - - nlohmann::json j = { + + const nlohmann::json j = { {"position", obj->get_position()}, {"rotation", obj->get_rotation()}, {"scale", obj->get_scale()} @@ -47,9 +37,17 @@ namespace hellfire { nlohmann::json j; input >> j; - obj->set_position(j.at("position").get()); - obj->set_rotation(j.at("rotation").get()); - obj->set_scale(j.at("scale").get()); + + const auto position = json_get_vec3(j, "position"); + const auto rotation = json_get_vec3(j, "rotation"); + const auto scale = json_get_vec3(j, "scale"); + + if (!position || !rotation || !scale) return false; + + obj->set_position(*position); + obj->set_rotation(*rotation); + obj->set_scale(*scale); + return true; } catch (...) { return false; diff --git a/engine/src/hellfire/utilities/SerializerUtils.h b/engine/src/hellfire/utilities/SerializerUtils.h new file mode 100644 index 0000000..cda3048 --- /dev/null +++ b/engine/src/hellfire/utilities/SerializerUtils.h @@ -0,0 +1,39 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include + +#include "glm/glm/detail/type_vec.hpp" +#include "nlohmann/json.hpp" + +namespace hellfire { + + inline std::optional json_to_vec3(const nlohmann::json& j) { + if (!j.is_array() || j.size() != 3) return std::nullopt; + + for (const auto& elem : j) { + if (!elem.is_number()) return std::nullopt; + } + + return glm::vec3(j[0].get(), j[1].get(), j[2].get()); + } + + inline std::optional json_get_vec3(const nlohmann::json& j, const std::string& key) { + if (!j.contains(key)) return std::nullopt; + return json_to_vec3(j.at(key)); + } + +} // namespace hellfire + +// GLM Helper extensions +namespace glm { + inline void to_json(nlohmann::json& j, const vec3& v) { + j = {v.x, v.y, v.z}; + } + + inline void from_json(const nlohmann::json& j, vec3& v) { + v = {j[0].get(), j[1].get(), j[2].get()}; + } +} // namespace glm diff --git a/engine/src/hellfire/utilities/ServiceLocator.h b/engine/src/hellfire/utilities/ServiceLocator.h index 0385a91..7c4c5d9 100644 --- a/engine/src/hellfire/utilities/ServiceLocator.h +++ b/engine/src/hellfire/utilities/ServiceLocator.h @@ -13,9 +13,6 @@ namespace hellfire { class InputManager; class ServiceLocator { - private: - static std::unordered_map services_; - public: template static void register_service(T *service) { @@ -30,5 +27,13 @@ namespace hellfire { } return nullptr; } + + template + static void unregister_service() { + services_.erase(typeid(T)); + } + + private: + static std::unordered_map services_; }; } diff --git a/sandbox/src/main.cpp b/sandbox/src/main.cpp index fb33cc7..066430d 100644 --- a/sandbox/src/main.cpp +++ b/sandbox/src/main.cpp @@ -3,7 +3,7 @@ #include "hellfire/scene/CameraFactory.h" #include "hellfire/graphics/Geometry/Cube.h" -void main() { +int main() { // Create the main application window // Parameters: width (pixels), height (pixels), window title hellfire::Application app(800, 600, "Custom Engine - Hellfire"); From da4014d7f865e1033adf87f6aae9c923efe321d7 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:10:08 +0100 Subject: [PATCH 15/27] - Removed testing stuff with registering assets --- engine/src/hellfire/assets/AssetRegistry.cpp | 11 +---------- engine/src/hellfire/assets/AssetRegistry.h | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/engine/src/hellfire/assets/AssetRegistry.cpp b/engine/src/hellfire/assets/AssetRegistry.cpp index 2683ea3..9f2c2e2 100644 --- a/engine/src/hellfire/assets/AssetRegistry.cpp +++ b/engine/src/hellfire/assets/AssetRegistry.cpp @@ -94,7 +94,7 @@ namespace hellfire { } } - std::optional AssetRegistry::get_asset(uint32_t uuid) const { + std::optional AssetRegistry::get_asset(AssetID uuid) const { const auto it = assets_.find(uuid); if (it != assets_.end()) { return it->second; @@ -175,15 +175,6 @@ namespace hellfire { j["version"] = "1.0"; j["assets"] = nlohmann::json::array(); - register_asset(project_root_ / "test_scene.hfscene"); - register_asset(project_root_ / "test_texture.png"); - register_asset(project_root_ / "test_texture.jpg"); - register_asset(project_root_ / "test_shader.vert"); - register_asset(project_root_ / "test_shader.frag"); - register_asset(project_root_ / "test_model.obj"); - register_asset(project_root_ / "test_model.fbx"); - register_asset(project_root_ / "test_model.gltf"); - for (const auto &metadata: assets_ | std::views::values) { nlohmann::json asset_json; asset_json["uuid"] = metadata.uuid; diff --git a/engine/src/hellfire/assets/AssetRegistry.h b/engine/src/hellfire/assets/AssetRegistry.h index 10121c4..6514253 100644 --- a/engine/src/hellfire/assets/AssetRegistry.h +++ b/engine/src/hellfire/assets/AssetRegistry.h @@ -46,7 +46,7 @@ namespace hellfire { void refresh_assets(); // Re-scan and update modified times // Lookup - std::optional get_asset(uint32_t uuid) const; + std::optional get_asset(AssetID uuid) const; std::optional get_uuid_by_path(const std::filesystem::path &filepath); std::vector get_assets_by_type(AssetType type); std::vector get_all_assets() const; From 5232eee511d865ec16fdd43883be2c5b716d9c50 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:10:41 +0100 Subject: [PATCH 16/27] - Updated the project class to work better with threads --- engine/src/hellfire/core/Project.cpp | 184 ++++++++++----------------- engine/src/hellfire/core/Project.h | 13 +- 2 files changed, 73 insertions(+), 124 deletions(-) diff --git a/engine/src/hellfire/core/Project.cpp b/engine/src/hellfire/core/Project.cpp index e2b14ec..fa799d9 100644 --- a/engine/src/hellfire/core/Project.cpp +++ b/engine/src/hellfire/core/Project.cpp @@ -8,7 +8,11 @@ #include #include +#include "imgui_internal.h" #include "json.hpp" +#include "hellfire/scene/Scene.h" +#include "hellfire/serialization/ProjectSerializer.h" +#include "hellfire/utilities/ServiceLocator.h" namespace hellfire { Project::Project(const std::string &name) { @@ -23,31 +27,35 @@ namespace hellfire { Project::Project(const ProjectMetadata &metadata) : metadata_(metadata) {} + Project::~Project() { + cleanup_managers(); + } + std::unique_ptr Project::create(const std::string &name, const std::filesystem::path &location) { - auto project = std::make_unique(name); - + ProjectMetadata metadata; + metadata.name = name; + metadata.version = "1.0.0"; + metadata.engine_version = "0.1.0"; + + // Get timestamp + metadata.created_at = get_current_timestamp(); + metadata.last_opened = metadata.created_at; + + auto project = std::make_unique(metadata); project->project_root_path_ = location / name; project->project_file_path_ = project->project_root_path_ / "project.hfproj"; - - // Create directory structure + project->create_directory_structure(); - - project->initialize_managers(); - - // Initialize default assets - project->initialize_default_assets(); - - // Save project file - if (!project->serialize()) { - std::cerr << "ERROR::PROJECT::CREATE:: Failed to save project file" << std::endl; + + if (!project->save()) { + std::cerr << "Failed to save new project" << std::endl; return nullptr; } - - std::cout << "Succesfully create project:" << name << std::endl; + return project; } - std::unique_ptr Project::load(const std::filesystem::path &project_file) { + std::unique_ptr Project::load_data(const std::filesystem::path &project_file) { if (!exists(project_file)) { std::cerr << "ERROR::PROJECT::LOAD:: Project file doesnt exist at " << project_file.string() << std::endl; return nullptr; @@ -59,50 +67,11 @@ namespace hellfire { std::cerr << "ERROR::PROJECT::LOAD:: Failed to open project file" << std::endl; return nullptr; } - - // Parse file content as json - nlohmann::json j; - file >> j; - - if (!j.contains("name")) { - std::cerr << "ERROR::PROJECT::LOAD:: Missing required 'name' field" << std::endl; - return nullptr; - } - - // Deserialize metadata - ProjectMetadata metadata; - - - if (j.contains("version")) { - metadata.version = j["version"].get(); - } - - if (j.contains("engine_version")) { - metadata.engine_version = j["engine_version"].get(); - } - - if (j.contains("created")) { - metadata.created_at = j["created"].get(); - } - if (j.contains("last_opened")) { - metadata.last_opened= j["last_opened"].get(); - } - - if (j.contains("last_scene")) { - metadata.last_scene = j["last_scene"].get(); - } - - if (j.contains("settings")) { - const auto& settings = j["settings"]; - - if (settings.contains("default_scene")) { - metadata.default_scene = settings["default_scene"].get(); - } - - if (settings.contains("renderer_settings")) { - metadata.renderer_settings = settings["renderer_settings"].get(); - } + ProjectMetadata metadata; + if (!Serializer::deserialize(file, &metadata)) { + std::cerr << "Failed to deserialize project metadata" << std::endl; + return nullptr; } // Create project with metadata @@ -110,18 +79,6 @@ namespace hellfire { project->project_file_path_ = project_file; project->project_root_path_ = project_file.parent_path(); - // Initialize managers - project->initialize_managers(); - - // Load the last opened scene if it exists - if (!metadata.last_scene.empty()) { - auto scene_path = project->project_root_path_ / metadata.last_scene; - if (std::filesystem::exists(scene_path)) { - project->scene_manager_->load_scene(scene_path); - } - } - - std::cout << "Successfully loaded project: " << metadata.name << std::endl; return project; } catch (const nlohmann::json::parse_error& e) { std::cerr << "ERROR::PROJECT::LOAD:: JSON parse error: " << e.what() << std::endl; @@ -136,12 +93,9 @@ namespace hellfire { bool Project::save() { // Update last_opened timestamp metadata_.last_opened = get_current_timestamp(); - - // Serialize project file - if (!serialize()) { - std::cerr << "ERROR::PROJECT::SAVE:: Failed to serialize project" << std::endl; - return false; - } + + std::ofstream file(project_file_path_); + if (!file.is_open()) return false; // Save asset registry if (asset_registry_) { @@ -151,9 +105,11 @@ namespace hellfire { // Save current scene if loaded if (scene_manager_ && scene_manager_->has_active_scene()) { scene_manager_->save_current_scene(); + metadata_.last_scene = scene_manager_->get_active_scene_asset_id(); } - return true; + + return Serializer::serialize(file, &metadata_); } void Project::close() { @@ -166,6 +122,8 @@ namespace hellfire { if (asset_registry_) { asset_registry_->clear(); } + + cleanup_managers(); } std::filesystem::path Project::get_project_root() const { @@ -184,47 +142,13 @@ namespace hellfire { return project_root_path_ / "settings"; } - bool Project::serialize() const { - try { - nlohmann::json j; - - // Serialize metadata - j["name"] = metadata_.name; - j["version"] = metadata_.version; - j["engine_version"] = metadata_.engine_version; - j["created"] = metadata_.created_at; - j["last_opened"] = metadata_.last_opened; - - if (!metadata_.last_scene.empty()) { - j["last_scene"] = metadata_.last_scene.string(); - } - - // Serialize settings - j["settings"] = { - {"default_scene", metadata_.default_scene.string()}, - {"renderer_settings", metadata_.renderer_settings.string()} - }; - - // Write to file - std::ofstream file(project_file_path_); - if (!file.is_open()) { - return false; - } - - file << j.dump(4); // Pretty print - return true; - - } catch (const std::exception& e) { - std::cerr << "ERROR::PROJECT::SERIALIZE:: " << e.what() << std::endl; - return false; - } - } - void Project::create_directory_structure() const { - create_directories(project_root_path_/ "settings"); - create_directories(project_root_path_/ "assets"); - create_directories(project_root_path_/ "shared"); - + std::filesystem::create_directories(project_root_path_ / "settings"); + std::filesystem::create_directories(project_root_path_ / "assets"); + std::filesystem::create_directories(project_root_path_ / "assets" / "scenes"); + std::filesystem::create_directories(project_root_path_ / "assets" / "textures"); + std::filesystem::create_directories(project_root_path_ / "assets" / "models"); + std::filesystem::create_directories(project_root_path_ / "assets" / "scripts"); } void Project::initialize_default_assets() { @@ -232,14 +156,36 @@ namespace hellfire { void Project::initialize_managers() { + ServiceLocator::unregister_service(); + ServiceLocator::unregister_service(); + // Initialize managers with project root context auto registry_path = project_root_path_ / "settings/assetregistry.json"; asset_registry_ = std::make_unique(registry_path, project_root_path_); + ServiceLocator::register_service(asset_registry_.get()); + asset_registry_->register_directory(get_assets_path(), true); scene_manager_ = std::make_unique(); + ServiceLocator::register_service(scene_manager_.get()); + + // Load last scene if exists + if (metadata_.last_scene) { + if (auto asset_info = asset_registry_->get_asset(metadata_.last_scene.value())) { + auto path = asset_registry_->get_absolute_path(asset_info->uuid); + auto scene = scene_manager_->load_scene(asset_info->uuid, path); + scene_manager_->set_active_scene(scene); + } + } + } + + void Project::cleanup_managers() { + ServiceLocator::unregister_service(); + ServiceLocator::unregister_service(); + scene_manager_.reset(); + asset_registry_.reset(); } - std::string Project::get_current_timestamp() const { + std::string Project::get_current_timestamp() { auto now = std::chrono::system_clock::now(); auto time_t_now = std::chrono::system_clock::to_time_t(now); std::stringstream ss; diff --git a/engine/src/hellfire/core/Project.h b/engine/src/hellfire/core/Project.h index 6405cc1..508e743 100644 --- a/engine/src/hellfire/core/Project.h +++ b/engine/src/hellfire/core/Project.h @@ -16,7 +16,7 @@ namespace hellfire { std::string engine_version; std::string created_at; std::string last_opened; - std::filesystem::path last_scene; + std::optional last_scene; std::filesystem::path default_scene; std::filesystem::path renderer_settings; }; @@ -25,9 +25,12 @@ namespace hellfire { public: explicit Project(const std::string &name); explicit Project(const ProjectMetadata &metadata); + ~Project(); static std::unique_ptr create(const std::string &name, const std::filesystem::path &location); - static std::unique_ptr load(const std::filesystem::path &project_file); + static std::unique_ptr load_data(const std::filesystem::path &project_file); + + void initialize_managers(); bool save(); void close(); @@ -57,11 +60,11 @@ namespace hellfire { std::vector recent_scenes_; private: - bool serialize() const; void create_directory_structure() const; void initialize_default_assets(); - void initialize_managers(); + void cleanup_managers(); + - std::string get_current_timestamp() const; + static std::string get_current_timestamp(); }; } From 52868c892fa6a985769e14aacbccd6aa098c7854 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:11:31 +0100 Subject: [PATCH 17/27] - Still W.I.P. projects saving/opening --- editor/src/Events/WindowEvents.h | 13 ++++ .../Serializers/ProjectManagerSerializer.h | 54 +++++++++++++++ .../UI/Panels/MenuBar/MenuBarComponent.cpp | 10 +-- .../src/UI/Panels/MenuBar/MenuBarComponent.h | 1 + .../Renderer/RendererSettingsPanel.cpp | 13 ++-- .../src/UI/Panels/Viewport/ViewportPanel.cpp | 22 ++++-- editor/src/UI/Panels/Viewport/ViewportPanel.h | 6 +- engine/src/hellfire/scene/SceneManager.cpp | 47 +++++++++++-- engine/src/hellfire/scene/SceneManager.h | 12 +++- .../serialization/ProjectSerializer.h | 67 +++++++++++++++++++ 10 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 editor/src/Events/WindowEvents.h create mode 100644 editor/src/Serializers/ProjectManagerSerializer.h create mode 100644 engine/src/hellfire/serialization/ProjectSerializer.h diff --git a/editor/src/Events/WindowEvents.h b/editor/src/Events/WindowEvents.h new file mode 100644 index 0000000..d6bc070 --- /dev/null +++ b/editor/src/Events/WindowEvents.h @@ -0,0 +1,13 @@ +// +// Created by denzel on 05/12/2025. +// + +#pragma once + +#include "UI/EventBus.h" + +namespace hellfire::editor { + struct OpenNewProjectWindowEvent { + using is_event_tag = void; + }; +} \ No newline at end of file diff --git a/editor/src/Serializers/ProjectManagerSerializer.h b/editor/src/Serializers/ProjectManagerSerializer.h new file mode 100644 index 0000000..fda6ffb --- /dev/null +++ b/editor/src/Serializers/ProjectManagerSerializer.h @@ -0,0 +1,54 @@ +// +// Created by denzel on 06/12/2025. +// + +#pragma once +#include "hellfire/serialization/Serializer.h" +#include "Project/ProjectManager.h" + +namespace hellfire { + template<> + struct Serializer> { + using MapType = std::unordered_map; + + static bool serialize(std::ostream& output, const MapType* recent_projects) { + if (recent_projects == nullptr) return false; + + nlohmann::ordered_json j; + + for (const auto &project: *recent_projects | std::views::values) { + nlohmann::ordered_json p; + p["name"] = project.name; + p["path"] = project.path.string(); + p["last_opened"] = project.last_opened; + j.push_back(p); + } + + output << j.dump(2); + return output.good(); + } + + static bool deserialize(std::istream& input, MapType* recent_projects) { + try { + nlohmann::json j; + input >> j; + + recent_projects->clear(); + + for (const auto& project : j) { + editor::RecentProject recent_project; + recent_project.name = project.at("name"); + recent_project.path = std::filesystem::path(project.at("path").get()); + recent_project.last_opened = project.at("last_opened"); + recent_project.exists = std::filesystem::exists(recent_project.path); + + (*recent_projects)[recent_project.path] = recent_project; + } + + return true; + } catch (...) { + return false; + } + } + }; +} diff --git a/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp b/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp index 53c05f3..79287c2 100644 --- a/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp +++ b/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp @@ -13,6 +13,7 @@ #include "hellfire/utilities/FileDialog.h" #include "hellfire/utilities/ServiceLocator.h" #include "Scenes/DefaultScene.h" +#include "Events/WindowEvents.h" namespace hellfire::editor { void MenuBarComponent::render() { @@ -26,12 +27,10 @@ namespace hellfire::editor { void MenuBarComponent::render_file_menu() { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("New Project")) { - Project::create("Test", "G:\\Dev\\Games"); - std::cout << "Clicked on button New Project" << std::endl; + context_->event_bus.dispatch(); } if (ImGui::MenuItem("Save Project")) { - register_all_components(); - ServiceLocator::get_service()->save_scene("G:\\Dev\\Games\\Test\\assets\\cool_scene.hfscene", context_->active_scene); + context_->project_manager->get_current_project()->save(); } ImGui::EndMenu(); } @@ -62,9 +61,6 @@ namespace hellfire::editor { const std::string filepath = Utility::FileDialog::open_file({scene_ext_filter}); if (!filepath.empty()) { const auto scene = sm->load_scene(filepath); - - setup_default_scene_with_default_entities(scene); - sm->set_active_scene(scene); if (context_) { diff --git a/editor/src/UI/Panels/MenuBar/MenuBarComponent.h b/editor/src/UI/Panels/MenuBar/MenuBarComponent.h index 8b90e31..c3379b4 100644 --- a/editor/src/UI/Panels/MenuBar/MenuBarComponent.h +++ b/editor/src/UI/Panels/MenuBar/MenuBarComponent.h @@ -8,6 +8,7 @@ namespace hellfire::editor { class MenuBarComponent : public EditorPanel { public: + MenuBarComponent(EditorContext* ctx) : EditorPanel(ctx) {} void render() override; private: void render_file_menu(); diff --git a/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp b/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp index c326d89..62a3bb8 100644 --- a/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp +++ b/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp @@ -9,14 +9,11 @@ #include "UI/ui.h" namespace hellfire::editor { -void RendererSettingsPanel::render() { - if (ImGui::Begin("Renderer Settings")) { - if (auto renderer = ServiceLocator::get_service()) { - ui::float_input("Shadow Bias", &renderer->get_shadow_settings().bias, 0.001); + void RendererSettingsPanel::render() { + if (ui::Window window{"Renderer Settings"}) { + if (const auto renderer = ServiceLocator::get_service()) { + ui::float_input("Shadow Bias", &renderer->get_shadow_settings().bias, 0.001); + } } - ImGui::End(); } - } -} - diff --git a/editor/src/UI/Panels/Viewport/ViewportPanel.cpp b/editor/src/UI/Panels/Viewport/ViewportPanel.cpp index 6e62e57..6ad3700 100644 --- a/editor/src/UI/Panels/Viewport/ViewportPanel.cpp +++ b/editor/src/UI/Panels/Viewport/ViewportPanel.cpp @@ -12,14 +12,20 @@ #include "SceneCameraScript.h" #include "hellfire/core/Time.h" #include "hellfire/platform/windows_linux/GLFWWindow.h" +#include "UI/ui.h" namespace hellfire::editor { ViewportPanel::ViewportPanel() { engine_renderer_ = ServiceLocator::get_service(); assert(engine_renderer_ != nullptr); create_editor_camera(); + } - picking_fbo_ = std::make_unique(); + Framebuffer* ViewportPanel::get_picking_fbo() { + if (!picking_fbo_) { + picking_fbo_ = std::make_unique(); + } + return picking_fbo_.get(); } ViewportPanel::~ViewportPanel() { @@ -227,8 +233,10 @@ namespace hellfire::editor { } } - void ViewportPanel::handle_object_picking() const { + void ViewportPanel::handle_object_picking() { if (!context_->active_scene) return; + if (!engine_renderer_) return; + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !is_using_gizmo_) { const ImVec2 mouse_pos = ImGui::GetMousePos(); const ImVec2 viewport_pos = ImGui::GetItemRectMin(); @@ -258,7 +266,8 @@ namespace hellfire::editor { ? ICON_FA_EYE " " + context_->active_scene->get_name() : "Viewport"; - if (ImGui::Begin(window_name.c_str())) { + if (ui::Window window{window_name}) { + if (!context_->active_scene) return; // Store the viewport bound for outside usage viewport_pos_ = ImGui::GetWindowPos(); viewport_size_ = ImGui::GetWindowSize(); @@ -274,7 +283,6 @@ namespace hellfire::editor { render_viewport_stats_overlay(); } } - ImGui::End(); } @@ -310,7 +318,9 @@ namespace hellfire::editor { ImGui::End(); } - uint32_t ViewportPanel::pick_object_at_mouse(const int mouse_x, const int mouse_y) const { + uint32_t ViewportPanel::pick_object_at_mouse(const int mouse_x, const int mouse_y) { + if (!engine_renderer_) return 0; + const uint32_t object_id_texture = engine_renderer_->get_object_id_texture(); if (object_id_texture == 0) return 0; @@ -326,7 +336,7 @@ namespace hellfire::editor { return 0; } - const uint32_t pixel_data = picking_fbo_->read_pixel_from_texture(object_id_texture, tex_x, tex_y); + const uint32_t pixel_data = get_picking_fbo()->read_pixel_from_texture(object_id_texture, tex_x, tex_y); return pixel_data; } diff --git a/editor/src/UI/Panels/Viewport/ViewportPanel.h b/editor/src/UI/Panels/Viewport/ViewportPanel.h index fdaa98f..452cb73 100644 --- a/editor/src/UI/Panels/Viewport/ViewportPanel.h +++ b/editor/src/UI/Panels/Viewport/ViewportPanel.h @@ -17,6 +17,8 @@ namespace hellfire::editor { public: ViewportPanel(); + Framebuffer *get_picking_fbo(); + ~ViewportPanel() override; /** @@ -36,12 +38,12 @@ namespace hellfire::editor { * @param mouse_y Y coordinate in screen space * @return EntityID at the given postion, or 0 if none */ - uint32_t pick_object_at_mouse(int mouse_x, int mouse_y) const; + uint32_t pick_object_at_mouse(int mouse_x, int mouse_y); /** * @brief Handles mouse-based entity selection using the picking framebuffer */ - void handle_object_picking() const; + void handle_object_picking(); Entity *get_editor_camera() const { return editor_camera_; } bool is_editor_camera_active() const { return camera_active_; } diff --git a/engine/src/hellfire/scene/SceneManager.cpp b/engine/src/hellfire/scene/SceneManager.cpp index fa4facb..6bee3bd 100644 --- a/engine/src/hellfire/scene/SceneManager.cpp +++ b/engine/src/hellfire/scene/SceneManager.cpp @@ -5,10 +5,13 @@ #include #include +#include "hellfire/ecs/ComponentRegistration.h" #include "hellfire/serialization/SceneSerializer.h" namespace hellfire { SceneManager::SceneManager() : active_scene_(nullptr) { + // Serialization of scene components + register_all_components(); } SceneManager::~SceneManager() { @@ -20,7 +23,7 @@ namespace hellfire { } void SceneManager::save_current_scene() const { - active_scene_->save(); + save_scene(active_scene_->get_source_filename().string(), active_scene_); } Scene *SceneManager::create_scene(const std::string &name) { @@ -55,6 +58,26 @@ namespace hellfire { return new_scene; } + Scene* SceneManager::load_scene(AssetID asset_id, const std::filesystem::path& filename) { + Scene* scene = load_scene(filename); + if (scene) { + scene_asset_ids_[scene] = asset_id; + } + return scene; + } + + std::optional SceneManager::get_scene_asset_id(Scene* scene) const { + auto it = scene_asset_ids_.find(scene); + if (it != scene_asset_ids_.end()) { + return it->second; + } + return std::nullopt; + } + + std::optional SceneManager::get_active_scene_asset_id() const { + return get_scene_asset_id(active_scene_); + } + bool SceneManager::save_scene(const std::string &filepath, Scene *scene) const { if (!scene) scene = get_active_scene(); if (!scene) return false; @@ -81,16 +104,32 @@ namespace hellfire { } void SceneManager::clear() { - for (Scene *scene: scenes_) { + for (Scene* scene : scenes_) { delete scene; } scenes_.clear(); + scene_asset_ids_.clear(); active_scene_ = nullptr; } void SceneManager::destroy_scene(Scene* scene) { - // TO - + if (!scene) return; + + // Remove from asset ID map + scene_asset_ids_.erase(scene); + + // Remove from scenes vector + auto it = std::find(scenes_.begin(), scenes_.end(), scene); + if (it != scenes_.end()) { + scenes_.erase(it); + } + + // Clear active if this was it + if (active_scene_ == scene) { + active_scene_ = nullptr; + } + + delete scene; } diff --git a/engine/src/hellfire/scene/SceneManager.h b/engine/src/hellfire/scene/SceneManager.h index b5d6825..79007b4 100644 --- a/engine/src/hellfire/scene/SceneManager.h +++ b/engine/src/hellfire/scene/SceneManager.h @@ -2,10 +2,11 @@ #include #include -#include #include #include +#include "hellfire/assets/AssetRegistry.h" + // Forward declarations namespace hellfire { @@ -39,6 +40,12 @@ namespace hellfire { Scene *load_scene(const std::filesystem::path &filename); + Scene *load_scene(AssetID asset_id, const std::filesystem::path &filename); + + std::optional get_scene_asset_id(Scene* scene) const; + std::optional get_active_scene_asset_id() const; + void set_scene_asset_id(Scene* scene, AssetID id); + bool save_scene(const std::string &filename, Scene *scene) const; // Scene management @@ -60,8 +67,6 @@ namespace hellfire { void set_active_scene(Scene *scene, bool should_play = true); - void set_active_scene(const std::shared_ptr &scene); - std::vector get_camera_entities() const; Scene *get_active_scene() const { @@ -77,6 +82,7 @@ namespace hellfire { private: std::vector scenes_; Scene *active_scene_; + std::unordered_map scene_asset_ids_; // Helper methods SceneActivatedCallback scene_activated_callback_; diff --git a/engine/src/hellfire/serialization/ProjectSerializer.h b/engine/src/hellfire/serialization/ProjectSerializer.h new file mode 100644 index 0000000..cacb08e --- /dev/null +++ b/engine/src/hellfire/serialization/ProjectSerializer.h @@ -0,0 +1,67 @@ +// +// Created by denzel on 06/12/2025. +// + +#pragma once +#include "hellfire/serialization/Serializer.h" +#include "hellfire/core/Project.h" + +namespace hellfire { + template<> + struct Serializer { + static bool serialize(std::ostream &output, const ProjectMetadata *obj) { + if (!obj) return false; + + nlohmann::json j = { + {"name", obj->name}, + {"version", obj->version}, + {"engine_version", obj->engine_version}, + {"created", obj->created_at}, + {"last_opened", obj->last_opened}, + { + "settings", { + {"default_scene", obj->default_scene.string()}, + {"renderer_settings", obj->renderer_settings.string()} + } + } + }; + + if (obj->last_scene.has_value()) { + j["last_scene"] = obj->last_scene.value(); + } + + output << j.dump(2); + return output.good(); + } + + static bool deserialize(std::istream &input, ProjectMetadata *obj) { + try { + if (!obj) return false; + + nlohmann::json j; + input >> j; + + obj->name = j.at("name").get(); + obj->version = j.value("version", "1.0.0"); + obj->engine_version = j.value("engine_version", ""); + obj->created_at = j.value("created", ""); + obj->last_opened = j.value("last_opened", ""); + + if (j.contains("last_scene")) { + obj->last_scene = j["last_scene"].get(); + } + + if (j.contains("settings")) { + const auto& settings = j["settings"]; + obj->default_scene = settings.value("default_scene", ""); + obj->renderer_settings = settings.value("renderer_settings", ""); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "ProjectMetadata deserialize error: " << e.what() << std::endl; + return false; + } + } + }; +} From cc0964a6c4ddec76dcbfbf2b280c6613d199a952 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 19:11:52 +0100 Subject: [PATCH 18/27] - Added testing for transform serializer --- tests/{ => unit}/scene/test_scene.cpp | 0 .../serializers/test_transform_serializer.cpp | 251 ++++++++++++++++++ 2 files changed, 251 insertions(+) rename tests/{ => unit}/scene/test_scene.cpp (100%) create mode 100644 tests/unit/serializers/test_transform_serializer.cpp diff --git a/tests/scene/test_scene.cpp b/tests/unit/scene/test_scene.cpp similarity index 100% rename from tests/scene/test_scene.cpp rename to tests/unit/scene/test_scene.cpp diff --git a/tests/unit/serializers/test_transform_serializer.cpp b/tests/unit/serializers/test_transform_serializer.cpp new file mode 100644 index 0000000..39e13af --- /dev/null +++ b/tests/unit/serializers/test_transform_serializer.cpp @@ -0,0 +1,251 @@ +// +// Created by denzel on 08/12/2025. +// + + +#include +#include "hellfire/serialization/Serializer.h" +#include +#include + +#include "catch2/matchers/catch_matchers_floating_point.hpp" + +using namespace hellfire; +using Catch::Matchers::WithinAbs; + +// Helper to compare vec3 with tolerance +bool vec3_equal(const glm::vec3 &a, const glm::vec3 &b, float epsilon = 0.0001f) { + return glm::all(glm::epsilonEqual(a, b, epsilon)); +} + +TEST_CASE("TransformComponent serialization", "[serialization][transform]") { + SECTION("Round-trip preserves default values") { + TransformComponent original; + + std::stringstream ss; + REQUIRE(Serializer::serialize(ss, &original)); + + TransformComponent loaded; + REQUIRE(Serializer::deserialize(ss, &loaded)); + + CHECK(vec3_equal(loaded.get_position(), glm::vec3(0, 0, 0))); + CHECK(vec3_equal(loaded.get_rotation(), glm::vec3(0, 0, 0))); + CHECK(vec3_equal(loaded.get_scale(), glm::vec3(1, 1, 1))); + } + + SECTION("Round-trip preserves custom values") { + TransformComponent original; + original.set_position(1.5f, -25.6f, 100.0f); + original.set_rotation(45.0f, 90.0f, 180.0f); + original.set_scale(2.0f, 0.5f, 1.0f); + + std::stringstream ss; + REQUIRE(Serializer::serialize(ss, &original)); + + TransformComponent loaded; + REQUIRE(Serializer::deserialize(ss, &loaded)); + + CHECK(vec3_equal(loaded.get_position(), original.get_position())); + CHECK(vec3_equal(loaded.get_rotation(), original.get_rotation())); + CHECK(vec3_equal(loaded.get_scale(), original.get_scale())); + } + + SECTION("Handles extreme values") { + TransformComponent original; + original.set_position(FLT_MAX, FLT_MIN, -FLT_MAX); + original.set_rotation(360.0f, -360.0f, 720.0f); + original.set_scale(0.000001f, 10000.0f, 1.0f); + + std::stringstream ss; + REQUIRE(Serializer::serialize(ss, &original)); + + TransformComponent loaded; + REQUIRE(Serializer::deserialize(ss, &loaded)); + + CHECK(vec3_equal(loaded.get_position(), original.get_position())); + CHECK(vec3_equal(loaded.get_rotation(), original.get_rotation())); + CHECK(vec3_equal(loaded.get_scale(), original.get_scale())); + } + + SECTION("Handles negative values") { + TransformComponent original; + original.set_position(-10.0f, -20.0f, -30.0f); + original.set_rotation(-45.0f, -90.0f, -180.0f); + original.set_scale(-1.0f, -1.0f, -1.0f); // Negative scale for mirroring + + std::stringstream ss; + REQUIRE(Serializer::serialize(ss, &original)); + + TransformComponent loaded; + REQUIRE(Serializer::deserialize(ss, &loaded)); + + CHECK(vec3_equal(loaded.get_position(), original.get_position())); + CHECK(vec3_equal(loaded.get_rotation(), original.get_rotation())); + CHECK(vec3_equal(loaded.get_scale(), original.get_scale())); + } + + SECTION("Preserves floating point precision") { + TransformComponent original; + original.set_position(0.123456789f, 3.14159265f, 0.987654321f); + + std::stringstream ss; + REQUIRE(Serializer::serialize(ss, &original)); + + TransformComponent loaded; + REQUIRE(Serializer::deserialize(ss, &loaded)); + + CHECK(vec3_equal(loaded.get_position(), original.get_position(), 0.00001f)); + } +} + +TEST_CASE("TransformComponent serialization error handling", "[serialization][transform]") { + SECTION("Serialize returns false for nullptr") { + std::stringstream ss; + REQUIRE_FALSE(Serializer::serialize(ss, nullptr)); + } + + SECTION("Deserialize returns false for nullptr") { + std::stringstream ss; + ss << R"({"position":[0,0,0],"rotation":[0,0,0],"scale":[1,1,1]})"; + REQUIRE_FALSE(Serializer::deserialize(ss, nullptr)); + } + + SECTION("Deserialize returns false for empty input") { + std::stringstream ss; + TransformComponent comp; + REQUIRE_FALSE(Serializer::deserialize(ss, &comp)); + } + + SECTION("Deserialize returns false for invalid JSON") { + std::stringstream ss; + ss << "not valid json {{{"; + + TransformComponent comp; + REQUIRE_FALSE(Serializer::deserialize(ss, &comp)); + } + + SECTION("Deserialize returns false for missing position field") { + std::stringstream ss; + ss << R"({"rotation":[0,0,0],"scale":[1,1,1]})"; + + TransformComponent comp; + REQUIRE_FALSE(Serializer::deserialize(ss, &comp)); + } + + SECTION("Deserialize returns false for missing rotation field") { + std::stringstream ss; + ss << R"({"position":[0,0,0],"scale":[1,1,1]})"; + + TransformComponent comp; + REQUIRE_FALSE(Serializer::deserialize(ss, &comp)); + } + + SECTION("Deserialize returns false for missing scale field") { + std::stringstream ss; + ss << R"({"position":[0,0,0],"rotation":[0,0,0]})"; + + TransformComponent comp; + REQUIRE_FALSE(Serializer::deserialize(ss, &comp)); + } + + SECTION("Deserialize returns false for wrong type in position") { + std::stringstream ss; + ss << R"({"position":"not an array","rotation":[0,0,0],"scale":[1,1,1]})"; + + TransformComponent comp; + REQUIRE_FALSE(Serializer::deserialize(ss, &comp)); + } + + SECTION("Deserialize returns false for wrong array size") { + std::stringstream ss; + ss << R"({"position":[0,0],"rotation":[0,0,0],"scale":[1,1,1]})"; // Only 2 elements + + TransformComponent comp; + REQUIRE_FALSE(Serializer::deserialize(ss, &comp)); + } +} + +TEST_CASE("TransformComponent JSON output format", "[serialization][transform]") { + SECTION("Output is valid JSON") { + TransformComponent comp; + comp.set_rotation(1, 2, 3); + + std::stringstream ss; + REQUIRE(Serializer::serialize(ss, &comp)); + + // Verify it parses as valid json + nlohmann::json j; + REQUIRE_NOTHROW(j = nlohmann::json::parse(ss.str())); + } + + SECTION("Output contains expected keys") { + TransformComponent comp; + + std::stringstream ss; + Serializer::serialize(ss, &comp); + + nlohmann::json j = nlohmann::json::parse(ss.str()); + + CHECK(j.contains("position")); + CHECK(j.contains("rotation")); + CHECK(j.contains("scale")); + } + + SECTION("Position is serialized as array") { + TransformComponent comp; + comp.set_position(1, 2, 3); + + std::stringstream ss; + Serializer::serialize(ss, &comp); + + nlohmann::json j = nlohmann::json::parse(ss.str()); + + REQUIRE(j["position"].is_array()); + REQUIRE(j["position"].size() == 3); + + REQUIRE(j["position"][0] == 1.0f); + REQUIRE(j["position"][1] == 2.0f); + REQUIRE(j["position"][2] == 3.0f); + } +} + +TEST_CASE("TransformComponent deserialization from manual JSON", "[serialization][transform]") { + SECTION("Can load hand-written JSON") { + std::stringstream ss; + ss << R"({ + "position": [10.0, 20.0, 30.0], + "rotation": [0.0, 90.0, 180.0], + "scale": [1.0, 1.0, 1.0] + })"; + + TransformComponent comp; + REQUIRE(Serializer::deserialize(ss, &comp)); + + CHECK(vec3_equal(comp.get_position(), glm::vec3(10, 20, 30))); + CHECK(vec3_equal(comp.get_rotation(), glm::vec3(0, 90, 180))); + CHECK(vec3_equal(comp.get_scale(), glm::vec3(1, 1, 1))); + } + + SECTION("Ignore extra fields") { + std::stringstream ss; + ss << R"({ + "position": [1.0, 2.0, 3.0], + "rotation": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0], + "some_random_field": "should be ignored" + })"; + + TransformComponent comp; + REQUIRE(Serializer::deserialize(ss, &comp)); + CHECK(vec3_equal(comp.get_position(), glm::vec3(1, 2, 3))); + } + + SECTION("Handles integer values in JSON") { + std::stringstream ss; + ss << R"({"position":[1.0,2.0,3.0],"rotation":[0.0,0.0,0.0],"scale":[1.0,1.0,1.0]})"; + + TransformComponent comp; + REQUIRE(Serializer::deserialize(ss, &comp)); + CHECK(vec3_equal(comp.get_position(), glm::vec3(1, 2, 3))); + } +} From 7423463302103552823f9cdd2babed9a2ffc206f Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 22:55:23 +0100 Subject: [PATCH 19/27] - changed folder names to lowercase --- editor/src/core/EditorApplication.cpp | 14 +- editor/src/core/EditorApplication.h | 4 +- editor/src/{Events => events}/StateEvents.h | 2 +- editor/src/{Events => events}/WindowEvents.h | 2 +- .../{Project => project}/ProjectManager.cpp | 6 +- .../src/{Project => project}/ProjectManager.h | 2 +- editor/src/{Scenes => scenes}/DefaultScene.h | 0 editor/src/{Scenes => scenes}/RotateScript.h | 0 .../ProjectManagerSerializer.h | 4 +- .../{States => states}/Editor/EditorState.cpp | 14 +- .../{States => states}/Editor/EditorState.h | 8 +- .../ProjectCreator/ProjectCreatorState.cpp | 6 +- .../ProjectCreator/ProjectCreatorState.h | 2 +- .../ProjectHub/ProjectHubState.cpp | 6 +- .../ProjectHub/ProjectHubState.h | 2 +- .../ProjectLoading/ProjectLoadingState.cpp | 6 +- .../ProjectLoading/ProjectLoadingState.h | 0 editor/src/{UI => ui}/EventBus.h | 0 editor/src/{UI => ui}/PanelManager.h | 0 editor/src/{UI => ui}/Panels/EditorPanel.h | 6 +- .../Panels/Inspector/InspectorPanel.cpp | 4 +- .../Panels/Inspector/InspectorPanel.h | 2 +- .../Panels/Inspector/TextureSlotManager.h | 0 .../Panels/MenuBar/MenuBarComponent.cpp | 4 +- .../Panels/MenuBar/MenuBarComponent.h | 0 .../Panels/Projects/NewProjectPanel.cpp | 4 +- .../Panels/Projects/NewProjectPanel.h | 2 +- .../SceneHierarchy/SceneHierarchyPanel.cpp | 0 .../SceneHierarchy/SceneHierarchyPanel.h | 0 .../Renderer/RendererSettingsPanel.cpp | 2 +- .../Settings/Renderer/RendererSettingsPanel.h | 2 +- .../Panels/Viewport/SceneCameraScript.cpp | 2 +- .../Panels/Viewport/SceneCameraScript.h | 0 .../Panels/Viewport/ViewportPanel.cpp | 2 +- .../Panels/Viewport/ViewportPanel.h | 2 +- editor/src/{UI => ui}/ui.h | 0 .../{Utils => utilities}/MoveOnlyFunction.h | 0 .../src/hellfire/assets/AnimationSystem.cpp | 1 - engine/src/hellfire/assets/AnimationSystem.h | 189 -------------- .../src/hellfire/assets/models/ImportResult.h | 52 ++++ engine/src/hellfire/core/Project.cpp | 2 +- engine/src/hellfire/ecs/ComponentRegistry.h | 2 +- engine/src/hellfire/graphics/Vertex.h | 3 +- .../hellfire/graphics/material/MaterialData.h | 44 ++++ .../hellfire/graphics/renderer/Renderer.cpp | 24 +- .../graphics/renderer/RenderingUtils.h | 15 +- engine/src/hellfire/scene/Scene.cpp | 1 - engine/src/hellfire/scene/SceneManager.cpp | 2 +- .../serializers/MaterialSerializer.cpp | 246 ++++++++++++++++++ .../hellfire/serializers/MaterialSerializer.h | 29 +++ .../ProjectSerializer.h | 2 +- .../SceneSerializer.h | 2 +- .../Serializer.h | 0 .../hellfire/utilities/MaterialSerializer.cpp | 139 ---------- .../hellfire/utilities/MaterialSerializer.h | 21 -- .../hellfire/utilities/ObjectDeserializer.cpp | 9 - .../hellfire/utilities/ObjectDeserializer.h | 28 -- .../hellfire/utilities/ObjectSerializer.cpp | 8 - .../src/hellfire/utilities/ObjectSerializer.h | 17 -- .../src/hellfire/utilities/TextureUtils.cpp | 40 --- engine/src/hellfire/utilities/TextureUtils.h | 10 - .../serializers/test_transform_serializer.cpp | 2 +- 62 files changed, 451 insertions(+), 547 deletions(-) rename editor/src/{Events => events}/StateEvents.h (97%) rename editor/src/{Events => events}/WindowEvents.h (87%) rename editor/src/{Project => project}/ProjectManager.cpp (98%) rename editor/src/{Project => project}/ProjectManager.h (98%) rename editor/src/{Scenes => scenes}/DefaultScene.h (100%) rename editor/src/{Scenes => scenes}/RotateScript.h (100%) rename editor/src/{Serializers => serializers}/ProjectManagerSerializer.h (95%) rename editor/src/{States => states}/Editor/EditorState.cpp (90%) rename editor/src/{States => states}/Editor/EditorState.h (81%) rename editor/src/{States => states}/ProjectCreator/ProjectCreatorState.cpp (97%) rename editor/src/{States => states}/ProjectCreator/ProjectCreatorState.h (93%) rename editor/src/{States => states}/ProjectHub/ProjectHubState.cpp (97%) rename editor/src/{States => states}/ProjectHub/ProjectHubState.h (93%) rename editor/src/{States => states}/ProjectLoading/ProjectLoadingState.cpp (94%) rename editor/src/{States => states}/ProjectLoading/ProjectLoadingState.h (100%) rename editor/src/{UI => ui}/EventBus.h (100%) rename editor/src/{UI => ui}/PanelManager.h (100%) rename editor/src/{UI => ui}/Panels/EditorPanel.h (94%) rename editor/src/{UI => ui}/Panels/Inspector/InspectorPanel.cpp (99%) rename editor/src/{UI => ui}/Panels/Inspector/InspectorPanel.h (97%) rename editor/src/{UI => ui}/Panels/Inspector/TextureSlotManager.h (100%) rename editor/src/{UI => ui}/Panels/MenuBar/MenuBarComponent.cpp (97%) rename editor/src/{UI => ui}/Panels/MenuBar/MenuBarComponent.h (100%) rename editor/src/{UI => ui}/Panels/Projects/NewProjectPanel.cpp (91%) rename editor/src/{UI => ui}/Panels/Projects/NewProjectPanel.h (88%) rename editor/src/{UI => ui}/Panels/SceneHierarchy/SceneHierarchyPanel.cpp (100%) rename editor/src/{UI => ui}/Panels/SceneHierarchy/SceneHierarchyPanel.h (100%) rename editor/src/{UI => ui}/Panels/Settings/Renderer/RendererSettingsPanel.cpp (96%) rename editor/src/{UI => ui}/Panels/Settings/Renderer/RendererSettingsPanel.h (83%) rename editor/src/{UI => ui}/Panels/Viewport/SceneCameraScript.cpp (98%) rename editor/src/{UI => ui}/Panels/Viewport/SceneCameraScript.h (100%) rename editor/src/{UI => ui}/Panels/Viewport/ViewportPanel.cpp (99%) rename editor/src/{UI => ui}/Panels/Viewport/ViewportPanel.h (98%) rename editor/src/{UI => ui}/ui.h (100%) rename editor/src/{Utils => utilities}/MoveOnlyFunction.h (100%) delete mode 100644 engine/src/hellfire/assets/AnimationSystem.cpp delete mode 100644 engine/src/hellfire/assets/AnimationSystem.h create mode 100644 engine/src/hellfire/assets/models/ImportResult.h create mode 100644 engine/src/hellfire/graphics/material/MaterialData.h create mode 100644 engine/src/hellfire/serializers/MaterialSerializer.cpp create mode 100644 engine/src/hellfire/serializers/MaterialSerializer.h rename engine/src/hellfire/{serialization => serializers}/ProjectSerializer.h (97%) rename engine/src/hellfire/{serialization => serializers}/SceneSerializer.h (98%) rename engine/src/hellfire/{serialization => serializers}/Serializer.h (100%) delete mode 100644 engine/src/hellfire/utilities/MaterialSerializer.cpp delete mode 100644 engine/src/hellfire/utilities/MaterialSerializer.h delete mode 100644 engine/src/hellfire/utilities/ObjectDeserializer.cpp delete mode 100644 engine/src/hellfire/utilities/ObjectDeserializer.h delete mode 100644 engine/src/hellfire/utilities/ObjectSerializer.cpp delete mode 100644 engine/src/hellfire/utilities/ObjectSerializer.h delete mode 100644 engine/src/hellfire/utilities/TextureUtils.cpp delete mode 100644 engine/src/hellfire/utilities/TextureUtils.h diff --git a/editor/src/core/EditorApplication.cpp b/editor/src/core/EditorApplication.cpp index e74c4c3..b654e3c 100644 --- a/editor/src/core/EditorApplication.cpp +++ b/editor/src/core/EditorApplication.cpp @@ -8,18 +8,18 @@ #include "imgui_impl_opengl3.h" #include "imgui.h" #include "ImGuizmo.h" -#include "../UI/Panels/EditorPanel.h" +#include "../ui/Panels/EditorPanel.h" #include "hellfire/core/Application.h" #include "hellfire/platform/IWindow.h" #include "hellfire/utilities/ServiceLocator.h" #include "hellfire/platform/windows_linux/GLFWWindow.h" #include "../IconsFontAwesome6.h" -#include "../Scenes/DefaultScene.h" -#include "Events/StateEvents.h" -#include "../States/Editor/EditorState.h" -#include "../States/ProjectHub/ProjectHubState.h" -#include "States/ProjectCreator/ProjectCreatorState.h" -#include "States/ProjectLoading/ProjectLoadingState.h" +#include "../scenes/DefaultScene.h" +#include "events/StateEvents.h" +#include "../states/Editor/EditorState.h" +#include "../states/ProjectHub/ProjectHubState.h" +#include "states/ProjectCreator/ProjectCreatorState.h" +#include "states/ProjectLoading/ProjectLoadingState.h" namespace hellfire::editor { void EditorApplication::on_initialize(Application &app) { diff --git a/editor/src/core/EditorApplication.h b/editor/src/core/EditorApplication.h index 3598e87..b560d13 100644 --- a/editor/src/core/EditorApplication.h +++ b/editor/src/core/EditorApplication.h @@ -5,10 +5,10 @@ #pragma once #include "StateManager.h" -#include "../UI/Panels/EditorPanel.h" +#include "../ui/Panels/EditorPanel.h" #include "hellfire/Interfaces/IApplicationPlugin.h" #include "hellfire/platform/IWindow.h" -#include "../UI/Panels/Inspector/InspectorPanel.h" +#include "../ui/Panels/Inspector/InspectorPanel.h" namespace hellfire::editor { class EditorApplication final : public IApplicationPlugin { diff --git a/editor/src/Events/StateEvents.h b/editor/src/events/StateEvents.h similarity index 97% rename from editor/src/Events/StateEvents.h rename to editor/src/events/StateEvents.h index 10257e2..4a31d11 100644 --- a/editor/src/Events/StateEvents.h +++ b/editor/src/events/StateEvents.h @@ -6,7 +6,7 @@ #include -#include "UI/EventBus.h" +#include "ui/EventBus.h" namespace hellfire::editor { diff --git a/editor/src/Events/WindowEvents.h b/editor/src/events/WindowEvents.h similarity index 87% rename from editor/src/Events/WindowEvents.h rename to editor/src/events/WindowEvents.h index d6bc070..7af3209 100644 --- a/editor/src/Events/WindowEvents.h +++ b/editor/src/events/WindowEvents.h @@ -4,7 +4,7 @@ #pragma once -#include "UI/EventBus.h" +#include "ui/EventBus.h" namespace hellfire::editor { struct OpenNewProjectWindowEvent { diff --git a/editor/src/Project/ProjectManager.cpp b/editor/src/project/ProjectManager.cpp similarity index 98% rename from editor/src/Project/ProjectManager.cpp rename to editor/src/project/ProjectManager.cpp index a38f6e7..745c9a6 100644 --- a/editor/src/Project/ProjectManager.cpp +++ b/editor/src/project/ProjectManager.cpp @@ -7,10 +7,10 @@ #include #include -#include "Events/StateEvents.h" +#include "events/StateEvents.h" #include "hellfire/core/Time.h" -#include "Serializers/ProjectManagerSerializer.h" -#include "UI/Panels/EditorPanel.h" +#include "serializers/ProjectManagerSerializer.h" +#include "ui/Panels/EditorPanel.h" namespace hellfire::editor { ProjectManager::ProjectManager(EventBus &event_bus, EditorContext &context) : event_bus_(event_bus), context_(context) { diff --git a/editor/src/Project/ProjectManager.h b/editor/src/project/ProjectManager.h similarity index 98% rename from editor/src/Project/ProjectManager.h rename to editor/src/project/ProjectManager.h index 771ef08..af16a56 100644 --- a/editor/src/Project/ProjectManager.h +++ b/editor/src/project/ProjectManager.h @@ -7,7 +7,7 @@ #include #include "hellfire/core/Project.h" -#include "UI/EventBus.h" +#include "ui/EventBus.h" namespace hellfire::editor { class EditorContext; diff --git a/editor/src/Scenes/DefaultScene.h b/editor/src/scenes/DefaultScene.h similarity index 100% rename from editor/src/Scenes/DefaultScene.h rename to editor/src/scenes/DefaultScene.h diff --git a/editor/src/Scenes/RotateScript.h b/editor/src/scenes/RotateScript.h similarity index 100% rename from editor/src/Scenes/RotateScript.h rename to editor/src/scenes/RotateScript.h diff --git a/editor/src/Serializers/ProjectManagerSerializer.h b/editor/src/serializers/ProjectManagerSerializer.h similarity index 95% rename from editor/src/Serializers/ProjectManagerSerializer.h rename to editor/src/serializers/ProjectManagerSerializer.h index fda6ffb..610b379 100644 --- a/editor/src/Serializers/ProjectManagerSerializer.h +++ b/editor/src/serializers/ProjectManagerSerializer.h @@ -3,8 +3,8 @@ // #pragma once -#include "hellfire/serialization/Serializer.h" -#include "Project/ProjectManager.h" +#include "hellfire/serializers/Serializer.h" +#include "project/ProjectManager.h" namespace hellfire { template<> diff --git a/editor/src/States/Editor/EditorState.cpp b/editor/src/states/Editor/EditorState.cpp similarity index 90% rename from editor/src/States/Editor/EditorState.cpp rename to editor/src/states/Editor/EditorState.cpp index 9a6c7c9..7664cf9 100644 --- a/editor/src/States/Editor/EditorState.cpp +++ b/editor/src/states/Editor/EditorState.cpp @@ -6,13 +6,13 @@ #include "hellfire/graphics/renderer/Renderer.h" #include "hellfire/scene/SceneManager.h" -#include "Scenes/DefaultScene.h" -#include "UI/Panels/Inspector/InspectorPanel.h" -#include "UI/Panels/MenuBar/MenuBarComponent.h" -#include "UI/Panels/SceneHierarchy/SceneHierarchyPanel.h" -#include "UI/Panels/Settings/Renderer/RendererSettingsPanel.h" -#include "UI/Panels/Viewport/SceneCameraScript.h" -#include "UI/Panels/Viewport/ViewportPanel.h" +#include "scenes/DefaultScene.h" +#include "ui/Panels/Inspector/InspectorPanel.h" +#include "ui/Panels/MenuBar/MenuBarComponent.h" +#include "ui/Panels/SceneHierarchy/SceneHierarchyPanel.h" +#include "ui/Panels/Settings/Renderer/RendererSettingsPanel.h" +#include "ui/Panels/Viewport/SceneCameraScript.h" +#include "ui/Panels/Viewport/ViewportPanel.h" namespace hellfire::editor { void EditorState::on_enter() { diff --git a/editor/src/States/Editor/EditorState.h b/editor/src/states/Editor/EditorState.h similarity index 81% rename from editor/src/States/Editor/EditorState.h rename to editor/src/states/Editor/EditorState.h index 603c661..0162622 100644 --- a/editor/src/States/Editor/EditorState.h +++ b/editor/src/states/Editor/EditorState.h @@ -4,10 +4,10 @@ #pragma once #include "core/ApplicationState.h" -#include "UI/PanelManager.h" -#include "UI/Panels/MenuBar/MenuBarComponent.h" -#include "UI/Panels/Projects/NewProjectPanel.h" -#include "UI/Panels/Viewport/ViewportPanel.h" +#include "ui/PanelManager.h" +#include "ui/Panels/MenuBar/MenuBarComponent.h" +#include "ui/Panels/Projects/NewProjectPanel.h" +#include "ui/Panels/Viewport/ViewportPanel.h" namespace hellfire::editor { class EditorState : public ApplicationState { diff --git a/editor/src/States/ProjectCreator/ProjectCreatorState.cpp b/editor/src/states/ProjectCreator/ProjectCreatorState.cpp similarity index 97% rename from editor/src/States/ProjectCreator/ProjectCreatorState.cpp rename to editor/src/states/ProjectCreator/ProjectCreatorState.cpp index 7ba485b..96354ce 100644 --- a/editor/src/States/ProjectCreator/ProjectCreatorState.cpp +++ b/editor/src/states/ProjectCreator/ProjectCreatorState.cpp @@ -6,9 +6,9 @@ #include "IconsFontAwesome6.h" #include "imgui.h" -#include "Events/StateEvents.h" -#include "UI/ui.h" -#include "UI/Panels/EditorPanel.h" +#include "events/StateEvents.h" +#include "ui/ui.h" +#include "ui/Panels/EditorPanel.h" namespace hellfire::editor { void ProjectCreatorState::on_enter() { diff --git a/editor/src/States/ProjectCreator/ProjectCreatorState.h b/editor/src/states/ProjectCreator/ProjectCreatorState.h similarity index 93% rename from editor/src/States/ProjectCreator/ProjectCreatorState.h rename to editor/src/states/ProjectCreator/ProjectCreatorState.h index 54b8004..8df8409 100644 --- a/editor/src/States/ProjectCreator/ProjectCreatorState.h +++ b/editor/src/states/ProjectCreator/ProjectCreatorState.h @@ -4,7 +4,7 @@ #pragma once #include "core/ApplicationState.h" -#include "Project/ProjectManager.h" +#include "project/ProjectManager.h" namespace hellfire::editor { class ProjectCreatorState : public ApplicationState { diff --git a/editor/src/States/ProjectHub/ProjectHubState.cpp b/editor/src/states/ProjectHub/ProjectHubState.cpp similarity index 97% rename from editor/src/States/ProjectHub/ProjectHubState.cpp rename to editor/src/states/ProjectHub/ProjectHubState.cpp index b5f280e..7920d64 100644 --- a/editor/src/States/ProjectHub/ProjectHubState.cpp +++ b/editor/src/states/ProjectHub/ProjectHubState.cpp @@ -6,9 +6,9 @@ #include "IconsFontAwesome6.h" #include "imgui.h" -#include "Events/StateEvents.h" -#include "UI/ui.h" -#include "UI/Panels/EditorPanel.h" +#include "events/StateEvents.h" +#include "ui/ui.h" +#include "ui/Panels/EditorPanel.h" namespace hellfire::editor { void ProjectHubState::on_enter() { diff --git a/editor/src/States/ProjectHub/ProjectHubState.h b/editor/src/states/ProjectHub/ProjectHubState.h similarity index 93% rename from editor/src/States/ProjectHub/ProjectHubState.h rename to editor/src/states/ProjectHub/ProjectHubState.h index 8aac3c8..bece9a1 100644 --- a/editor/src/States/ProjectHub/ProjectHubState.h +++ b/editor/src/states/ProjectHub/ProjectHubState.h @@ -6,7 +6,7 @@ #include #include "core/ApplicationState.h" -#include "Project/ProjectManager.h" +#include "project/ProjectManager.h" namespace hellfire::editor { diff --git a/editor/src/States/ProjectLoading/ProjectLoadingState.cpp b/editor/src/states/ProjectLoading/ProjectLoadingState.cpp similarity index 94% rename from editor/src/States/ProjectLoading/ProjectLoadingState.cpp rename to editor/src/states/ProjectLoading/ProjectLoadingState.cpp index 75c6373..dc3dbaa 100644 --- a/editor/src/States/ProjectLoading/ProjectLoadingState.cpp +++ b/editor/src/states/ProjectLoading/ProjectLoadingState.cpp @@ -5,9 +5,9 @@ #include "ProjectLoadingState.h" #include "imgui.h" -#include "Events/StateEvents.h" -#include "UI/ui.h" -#include "UI/Panels/EditorPanel.h" +#include "events/StateEvents.h" +#include "ui/ui.h" +#include "ui/Panels/EditorPanel.h" namespace hellfire::editor { void ProjectLoadingState::on_enter() { diff --git a/editor/src/States/ProjectLoading/ProjectLoadingState.h b/editor/src/states/ProjectLoading/ProjectLoadingState.h similarity index 100% rename from editor/src/States/ProjectLoading/ProjectLoadingState.h rename to editor/src/states/ProjectLoading/ProjectLoadingState.h diff --git a/editor/src/UI/EventBus.h b/editor/src/ui/EventBus.h similarity index 100% rename from editor/src/UI/EventBus.h rename to editor/src/ui/EventBus.h diff --git a/editor/src/UI/PanelManager.h b/editor/src/ui/PanelManager.h similarity index 100% rename from editor/src/UI/PanelManager.h rename to editor/src/ui/PanelManager.h diff --git a/editor/src/UI/Panels/EditorPanel.h b/editor/src/ui/Panels/EditorPanel.h similarity index 94% rename from editor/src/UI/Panels/EditorPanel.h rename to editor/src/ui/Panels/EditorPanel.h index ae08f03..012f9c3 100644 --- a/editor/src/UI/Panels/EditorPanel.h +++ b/editor/src/ui/Panels/EditorPanel.h @@ -10,9 +10,9 @@ #include "hellfire/platform/IWindow.h" #include "hellfire/scene/Scene.h" #include "hellfire/utilities/ServiceLocator.h" -#include "Project/ProjectManager.h" -#include "UI/EventBus.h" -#include "Utils/MoveOnlyFunction.h" +#include "project/ProjectManager.h" +#include "ui/EventBus.h" +#include "utilities/MoveOnlyFunction.h" namespace hellfire::editor { class EditorContext { diff --git a/editor/src/UI/Panels/Inspector/InspectorPanel.cpp b/editor/src/ui/Panels/Inspector/InspectorPanel.cpp similarity index 99% rename from editor/src/UI/Panels/Inspector/InspectorPanel.cpp rename to editor/src/ui/Panels/Inspector/InspectorPanel.cpp index 82c3f2c..e80b70f 100644 --- a/editor/src/UI/Panels/Inspector/InspectorPanel.cpp +++ b/editor/src/ui/Panels/Inspector/InspectorPanel.cpp @@ -13,8 +13,8 @@ #include "hellfire/ecs/components/MeshComponent.h" #include "hellfire/graphics/geometry/Cube.h" #include "hellfire/graphics/geometry/Sphere.h" -#include "Scenes/RotateScript.h" -#include "UI/ui.h" +#include "scenes/RotateScript.h" +#include "ui/ui.h" namespace hellfire::editor { void InspectorPanel::render_add_component_context_menu(Entity *selected_entity) { diff --git a/editor/src/UI/Panels/Inspector/InspectorPanel.h b/editor/src/ui/Panels/Inspector/InspectorPanel.h similarity index 97% rename from editor/src/UI/Panels/Inspector/InspectorPanel.h rename to editor/src/ui/Panels/Inspector/InspectorPanel.h index 3027efd..7e3d9d9 100644 --- a/editor/src/UI/Panels/Inspector/InspectorPanel.h +++ b/editor/src/ui/Panels/Inspector/InspectorPanel.h @@ -4,7 +4,7 @@ #pragma once #include "hellfire/ecs/components/MeshComponent.h" -#include "UI/Panels/EditorPanel.h" +#include "ui/Panels/EditorPanel.h" namespace hellfire { class LightComponent; diff --git a/editor/src/UI/Panels/Inspector/TextureSlotManager.h b/editor/src/ui/Panels/Inspector/TextureSlotManager.h similarity index 100% rename from editor/src/UI/Panels/Inspector/TextureSlotManager.h rename to editor/src/ui/Panels/Inspector/TextureSlotManager.h diff --git a/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp b/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp similarity index 97% rename from editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp rename to editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp index 79287c2..88c4bf8 100644 --- a/editor/src/UI/Panels/MenuBar/MenuBarComponent.cpp +++ b/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp @@ -12,8 +12,8 @@ #include "hellfire/scene/SceneManager.h" #include "hellfire/utilities/FileDialog.h" #include "hellfire/utilities/ServiceLocator.h" -#include "Scenes/DefaultScene.h" -#include "Events/WindowEvents.h" +#include "scenes/DefaultScene.h" +#include "events/WindowEvents.h" namespace hellfire::editor { void MenuBarComponent::render() { diff --git a/editor/src/UI/Panels/MenuBar/MenuBarComponent.h b/editor/src/ui/Panels/MenuBar/MenuBarComponent.h similarity index 100% rename from editor/src/UI/Panels/MenuBar/MenuBarComponent.h rename to editor/src/ui/Panels/MenuBar/MenuBarComponent.h diff --git a/editor/src/UI/Panels/Projects/NewProjectPanel.cpp b/editor/src/ui/Panels/Projects/NewProjectPanel.cpp similarity index 91% rename from editor/src/UI/Panels/Projects/NewProjectPanel.cpp rename to editor/src/ui/Panels/Projects/NewProjectPanel.cpp index e4c2676..a69d667 100644 --- a/editor/src/UI/Panels/Projects/NewProjectPanel.cpp +++ b/editor/src/ui/Panels/Projects/NewProjectPanel.cpp @@ -4,8 +4,8 @@ #include "NewProjectPanel.h" -#include "UI/ui.h" -#include "Events/WindowEvents.h" +#include "ui/ui.h" +#include "events/WindowEvents.h" namespace hellfire::editor { void NewProjectPanel::render() { diff --git a/editor/src/UI/Panels/Projects/NewProjectPanel.h b/editor/src/ui/Panels/Projects/NewProjectPanel.h similarity index 88% rename from editor/src/UI/Panels/Projects/NewProjectPanel.h rename to editor/src/ui/Panels/Projects/NewProjectPanel.h index 16ca203..774a693 100644 --- a/editor/src/UI/Panels/Projects/NewProjectPanel.h +++ b/editor/src/ui/Panels/Projects/NewProjectPanel.h @@ -3,7 +3,7 @@ // #pragma once -#include "UI/Panels/EditorPanel.h" +#include "ui/Panels/EditorPanel.h" namespace hellfire::editor { class NewProjectPanel : EditorPanel { diff --git a/editor/src/UI/Panels/SceneHierarchy/SceneHierarchyPanel.cpp b/editor/src/ui/Panels/SceneHierarchy/SceneHierarchyPanel.cpp similarity index 100% rename from editor/src/UI/Panels/SceneHierarchy/SceneHierarchyPanel.cpp rename to editor/src/ui/Panels/SceneHierarchy/SceneHierarchyPanel.cpp diff --git a/editor/src/UI/Panels/SceneHierarchy/SceneHierarchyPanel.h b/editor/src/ui/Panels/SceneHierarchy/SceneHierarchyPanel.h similarity index 100% rename from editor/src/UI/Panels/SceneHierarchy/SceneHierarchyPanel.h rename to editor/src/ui/Panels/SceneHierarchy/SceneHierarchyPanel.h diff --git a/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp b/editor/src/ui/Panels/Settings/Renderer/RendererSettingsPanel.cpp similarity index 96% rename from editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp rename to editor/src/ui/Panels/Settings/Renderer/RendererSettingsPanel.cpp index 62a3bb8..a764b05 100644 --- a/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.cpp +++ b/editor/src/ui/Panels/Settings/Renderer/RendererSettingsPanel.cpp @@ -6,7 +6,7 @@ #include "imgui.h" #include "hellfire/graphics/renderer/Renderer.h" -#include "UI/ui.h" +#include "ui/ui.h" namespace hellfire::editor { void RendererSettingsPanel::render() { diff --git a/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.h b/editor/src/ui/Panels/Settings/Renderer/RendererSettingsPanel.h similarity index 83% rename from editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.h rename to editor/src/ui/Panels/Settings/Renderer/RendererSettingsPanel.h index c90cdd8..91e8343 100644 --- a/editor/src/UI/Panels/Settings/Renderer/RendererSettingsPanel.h +++ b/editor/src/ui/Panels/Settings/Renderer/RendererSettingsPanel.h @@ -3,7 +3,7 @@ // #pragma once -#include "UI/Panels/EditorPanel.h" +#include "ui/Panels/EditorPanel.h" namespace hellfire::editor { class RendererSettingsPanel : public EditorPanel { diff --git a/editor/src/UI/Panels/Viewport/SceneCameraScript.cpp b/editor/src/ui/Panels/Viewport/SceneCameraScript.cpp similarity index 98% rename from editor/src/UI/Panels/Viewport/SceneCameraScript.cpp rename to editor/src/ui/Panels/Viewport/SceneCameraScript.cpp index 455a71d..2e7f1ce 100644 --- a/editor/src/UI/Panels/Viewport/SceneCameraScript.cpp +++ b/editor/src/ui/Panels/Viewport/SceneCameraScript.cpp @@ -2,7 +2,7 @@ // Created by denzel on 14/08/2025. // -#include "UI/Panels/Viewport/SceneCameraScript.h" +#include "ui/Panels/Viewport/SceneCameraScript.h" #include "hellfire/core/Application.h" #include "hellfire/ecs/CameraComponent.h" diff --git a/editor/src/UI/Panels/Viewport/SceneCameraScript.h b/editor/src/ui/Panels/Viewport/SceneCameraScript.h similarity index 100% rename from editor/src/UI/Panels/Viewport/SceneCameraScript.h rename to editor/src/ui/Panels/Viewport/SceneCameraScript.h diff --git a/editor/src/UI/Panels/Viewport/ViewportPanel.cpp b/editor/src/ui/Panels/Viewport/ViewportPanel.cpp similarity index 99% rename from editor/src/UI/Panels/Viewport/ViewportPanel.cpp rename to editor/src/ui/Panels/Viewport/ViewportPanel.cpp index 6ad3700..c933d25 100644 --- a/editor/src/UI/Panels/Viewport/ViewportPanel.cpp +++ b/editor/src/ui/Panels/Viewport/ViewportPanel.cpp @@ -12,7 +12,7 @@ #include "SceneCameraScript.h" #include "hellfire/core/Time.h" #include "hellfire/platform/windows_linux/GLFWWindow.h" -#include "UI/ui.h" +#include "ui/ui.h" namespace hellfire::editor { ViewportPanel::ViewportPanel() { diff --git a/editor/src/UI/Panels/Viewport/ViewportPanel.h b/editor/src/ui/Panels/Viewport/ViewportPanel.h similarity index 98% rename from editor/src/UI/Panels/Viewport/ViewportPanel.h rename to editor/src/ui/Panels/Viewport/ViewportPanel.h index 452cb73..ea383f8 100644 --- a/editor/src/UI/Panels/Viewport/ViewportPanel.h +++ b/editor/src/ui/Panels/Viewport/ViewportPanel.h @@ -7,7 +7,7 @@ #include "ImGuizmo.h" #include "hellfire/graphics/backends/opengl/Framebuffer.h" -#include "UI/Panels/EditorPanel.h" +#include "ui/Panels/EditorPanel.h" namespace hellfire::editor { /** diff --git a/editor/src/UI/ui.h b/editor/src/ui/ui.h similarity index 100% rename from editor/src/UI/ui.h rename to editor/src/ui/ui.h diff --git a/editor/src/Utils/MoveOnlyFunction.h b/editor/src/utilities/MoveOnlyFunction.h similarity index 100% rename from editor/src/Utils/MoveOnlyFunction.h rename to editor/src/utilities/MoveOnlyFunction.h diff --git a/engine/src/hellfire/assets/AnimationSystem.cpp b/engine/src/hellfire/assets/AnimationSystem.cpp deleted file mode 100644 index 753e2d4..0000000 --- a/engine/src/hellfire/assets/AnimationSystem.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "hellfire/assets/AnimationSystem.h" diff --git a/engine/src/hellfire/assets/AnimationSystem.h b/engine/src/hellfire/assets/AnimationSystem.h deleted file mode 100644 index 2b4a23d..0000000 --- a/engine/src/hellfire/assets/AnimationSystem.h +++ /dev/null @@ -1,189 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "hellfire/ecs/LightComponent.h" -#include "../ecs/Entity.h" - -namespace hellfire { - class AnimationSystem { - public: - // Animation callback type - using AnimationCallback = std::function; - - // Animation data structure - struct Animation { - std::string name; // Name for identification - Entity*target; // Target object - float duration; // Duration in seconds - float elapsed_time = 0.0f; // Current elapsed time - bool loop; // Whether to loop - bool active = true; // Whether animation is active - AnimationCallback callback; // The animation function - }; - - // Add an animation - void add_animation(const std::string &name, Entity*target, float duration, - AnimationCallback callback, bool loop = false) { - animations_.push_back({name, target, duration, 0.0f, loop, true, callback}); - } - - // Update all animations - void update(float dt) { - for (auto it = animations_.begin(); it != animations_.end();) { - auto &anim = *it; - - if (!anim.active) { - it = animations_.erase(it); - continue; - } - - anim.elapsed_time += dt; - - if (anim.elapsed_time >= anim.duration) { - if (anim.loop) { - // Loop back - anim.elapsed_time = fmodf(anim.elapsed_time, anim.duration); - } else { - // Ensure we get the final frame - anim.callback(anim.target, 1.0f, dt); - it = animations_.erase(it); - continue; - } - } - - float progress = anim.elapsed_time / anim.duration; - anim.callback(anim.target, progress, dt); - - ++it; - } - } - - // Pause/resume an animation by name - void set_animation_active(const std::string &name, bool active) { - for (auto &anim: animations_) { - if (anim.name == name) { - anim.active = active; - } - } - } - - // Stop and remove an animation by name - void remove_animation(const std::string &name) { - animations_.erase( - std::remove_if(animations_.begin(), animations_.end(), - [&name](const Animation &anim) { return anim.name == name; }), - animations_.end()); - } - - // Stop all animations for a specific object - void remove_animations_for_object(Entity*target) { - animations_.erase( - std::remove_if(animations_.begin(), animations_.end(), - [target](const Animation &anim) { return anim.target == target; }), - animations_.end()); - } - - // Helper methods for common animations - - void create_rotation(const std::string &name, Entity*target, - float x_speed, float y_speed, float z_speed) { - add_animation(name, target, 1.0f, - [x_speed, y_speed, z_speed](Entity* entity, float progress, float dt) { - glm::vec3 current_rotation = entity->transform()->get_rotation(); - - // Apply rotation based on speeds and delta time - current_rotation.x += x_speed * dt; - current_rotation.y += y_speed * dt; - current_rotation.z += z_speed * dt; - - // Keep rotations in range 0-360 - current_rotation.x = fmodf(current_rotation.x, 360.0f); - current_rotation.y = fmodf(current_rotation.y, 360.0f); - current_rotation.z = fmodf(current_rotation.z, 360.0f); - - entity->transform()->set_rotation(current_rotation); - }, true); - } - - // Pulsing scale animation - void create_pulsing_scale(const std::string &name, Entity*target, - float duration, float min_scale, float max_scale) { - add_animation(name, target, duration, - [min_scale, max_scale](Entity*entity, float progress, float dt) { - // Sine wave pulsing - float scale = min_scale + (max_scale - min_scale) * - (0.5f + 0.5f * sinf(progress * 6.28318f)); - - entity->transform()->set_scale(scale); - }, true); - } - - // Moving in a circular path - void create_circular_path(const std::string &name, Entity*target, - float duration, float radius, bool face_direction = true) { - // Store the original position - glm::vec3 center = target->transform()->get_position(); - - add_animation(name, target, duration, - [center, radius, face_direction](Entity*entity, float progress, float dt) { - // Calculate angle based on progress - float angle = progress * 6.28318f; // 2*PI - - // Calculate new position - glm::vec3 new_pos = center + glm::vec3( - radius * cosf(angle), - 0.0f, - radius * sinf(angle) - ); - - entity->transform()->set_position(new_pos.x, new_pos.y, new_pos.z); - - // Make object face movement direction - if (face_direction) { - // Direction tangent to circle - glm::vec3 direction = glm::vec3(-sinf(angle), 0.0f, cosf(angle)); - glm::vec3 target_pos = new_pos + direction; - entity->transform()->look_at(target_pos, glm::vec3(0.0f, 1.0f, 0.0f)); - } - }, true); - } - - // Oscillating movement (up and down, or side to side) - void create_oscillating_movement(const std::string &name, Entity* target, - float duration, const glm::vec3 &axis, float amplitude) { - // Store the original position - glm::vec3 start_pos = target->transform()->get_position(); - - add_animation(name, target, duration, - [start_pos, axis, amplitude](Entity* obj, float progress, float dt) { - // Sine wave oscillation - float offset = amplitude * sinf(progress * 6.28318f); - - // Calculate new position - glm::vec3 new_pos = start_pos + (axis * offset); - obj->transform()->set_position(new_pos.x, new_pos.y, new_pos.z); - }, true); - } - - void create_intensity_pulse(const std::string &name, Entity* target, - float duration, float min_intensity, float max_intensity) { - add_animation(name, target, duration, - [min_intensity, max_intensity](Entity*entity, float progress, float dt) { - // Cast back to Light* since we know this is a light - - // Sine wave pulsing - float intensity = min_intensity + (max_intensity - min_intensity) * - (0.5f + 0.5f * sinf(progress * 6.28318f)); - - entity->get_component()->set_intensity(intensity); - }, true); - } - - private: - std::vector animations_; - }; -} // namespace hellfire diff --git a/engine/src/hellfire/assets/models/ImportResult.h b/engine/src/hellfire/assets/models/ImportResult.h new file mode 100644 index 0000000..e0391bb --- /dev/null +++ b/engine/src/hellfire/assets/models/ImportResult.h @@ -0,0 +1,52 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once + +#include "glm/detail/type_vec.hpp" +#include "glm/detail/type_vec3.hpp" +#include "hellfire/assets/AssetRegistry.h" + +namespace hellfire { + /** + * @brief Represents a node in the imported model hierarchy + */ + struct ImportedNode { + std::string name; + glm::vec3 position{0.0f}; + glm::vec3 rotation{0.0f}; + glm::vec3 scale{1.0f}; + + /// Indices into ImportResult::meshes + std::vector mesh_indices; + /// Indices into ImportResult::nodes + std::vector child_indices; + }; + + /** + * @brief Represents an imported mesh with its material binding + */ + struct ImportedMesh { + AssetID mesh_asset; // Reference to .hfmesh file + AssetID material_asset; // Reference to .hfmat file + std::string name; + }; + + /** + * @brief Complete result of importing a model file + */ + struct ImportResult { + bool success = false; + std::string error_message; + + std::vector nodes; + std::vector meshes; + size_t root_node_index = 0; + + // All assets created during import + std::vector created_mesh_assets; + std::vector created_material_assets; + std::vector created_texture_assets; + }; +} diff --git a/engine/src/hellfire/core/Project.cpp b/engine/src/hellfire/core/Project.cpp index fa799d9..b564e16 100644 --- a/engine/src/hellfire/core/Project.cpp +++ b/engine/src/hellfire/core/Project.cpp @@ -11,7 +11,7 @@ #include "imgui_internal.h" #include "json.hpp" #include "hellfire/scene/Scene.h" -#include "hellfire/serialization/ProjectSerializer.h" +#include "hellfire/serializers/ProjectSerializer.h" #include "hellfire/utilities/ServiceLocator.h" namespace hellfire { diff --git a/engine/src/hellfire/ecs/ComponentRegistry.h b/engine/src/hellfire/ecs/ComponentRegistry.h index 9c3ee15..a497d98 100644 --- a/engine/src/hellfire/ecs/ComponentRegistry.h +++ b/engine/src/hellfire/ecs/ComponentRegistry.h @@ -7,7 +7,7 @@ #include #include "Entity.h" -#include "hellfire/serialization/Serializer.h" +#include "hellfire/serializers/Serializer.h" #include "nlohmann/json.hpp" namespace hellfire { diff --git a/engine/src/hellfire/graphics/Vertex.h b/engine/src/hellfire/graphics/Vertex.h index d8039a2..5e5972d 100644 --- a/engine/src/hellfire/graphics/Vertex.h +++ b/engine/src/hellfire/graphics/Vertex.h @@ -1,6 +1,5 @@ #pragma once -#include -#include +#include struct Vertex { diff --git a/engine/src/hellfire/graphics/material/MaterialData.h b/engine/src/hellfire/graphics/material/MaterialData.h new file mode 100644 index 0000000..7720138 --- /dev/null +++ b/engine/src/hellfire/graphics/material/MaterialData.h @@ -0,0 +1,44 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include +#include +#include + +#include "hellfire/assets/AssetRegistry.h" +#include "hellfire/graphics/texture/Texture.h" + +namespace hellfire { + /** + * @brief Serializable material data (separate from runtime Material class) + */ + struct MaterialData { + std::string name = "Material"; + + // Colors + glm::vec3 diffuse_color{1.0f}; + glm::vec3 ambient_color{0.1f}; + glm::vec3 specular_color{1.0f}; + glm::vec3 emissive_color{0.0f}; + + // Scalars + float opacity = 1.0f; + float shininess = 32.0f; + float metallic = 0.0f; + float roughness = 0.5f; + + // Texture references (by AssetID) + std::unordered_map texture_assets; + + // UV transforms (optional, per texture) + glm::vec2 uv_scale{1.0f}; + glm::vec2 uv_offset{0.0f}; + + // Rendering flags + bool double_sided = false; + bool alpha_blend = false; + float alpha_cutoff = 0.5f; + }; +} diff --git a/engine/src/hellfire/graphics/renderer/Renderer.cpp b/engine/src/hellfire/graphics/renderer/Renderer.cpp index b5d3354..59c472a 100644 --- a/engine/src/hellfire/graphics/renderer/Renderer.cpp +++ b/engine/src/hellfire/graphics/renderer/Renderer.cpp @@ -211,7 +211,7 @@ namespace hellfire { shadow_map->attach_depth_texture(settings); glBindTexture(GL_TEXTURE_2D, shadow_map->get_depth_attachment()); - float border_color[] = { 1.0f, 1.0f, 1.0f, 1.0f }; + const float border_color[] = { 1.0f, 1.0f, 1.0f, 1.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color); glBindTexture(GL_TEXTURE_2D, 0); shadow_maps_[light_entity] = {std::move(shadow_map), glm::mat4(1.0f)}; @@ -240,7 +240,7 @@ namespace hellfire { auto& shadow_data = shadow_maps_[light_entity]; // Bind depth texture to texture unit - int texture_unit = 10 + i; // Start at unit 10 to avoid conflicts + const int texture_unit = 10 + i; // Start at unit 10 to avoid conflicts glActiveTexture(GL_TEXTURE0 + texture_unit); glBindTexture(GL_TEXTURE_2D, shadow_data.framebuffer->get_depth_attachment()); @@ -397,7 +397,7 @@ namespace hellfire { } void Renderer::draw_shadow_geometry(const glm::mat4 &light_view_proj) { - Shader& shadow_shader = get_shader_for_material(shadow_material_); + const Shader& shadow_shader = get_shader_for_material(shadow_material_); shadow_shader.use(); shadow_shader.set_mat4("uLightViewProjMatrix", light_view_proj); @@ -420,32 +420,32 @@ namespace hellfire { } glm::mat4 Renderer::calculate_light_view_proj(Entity *light_entity, LightComponent *light, const CameraComponent &camera) { - glm::vec3 light_dir = glm::normalize(light->get_direction()); + const glm::vec3 light_dir = glm::normalize(light->get_direction()); // Center shadow map on camera position - glm::vec3 camera_pos = camera.get_owner().transform()->get_position(); - - float ortho_size = 100.0f; - float texel_size = (ortho_size * 2.0f) / 4096.0f; + const glm::vec3 camera_pos = camera.get_owner().transform()->get_position(); + + const float ortho_size = 100.0f; + const float texel_size = (ortho_size * 2.0f) / 4096.0f; glm::vec3 look_at; look_at.x = floor(camera_pos.x / texel_size) * texel_size; look_at.y = 0.0f; look_at.z = float(camera_pos.z / texel_size) * texel_size; - - glm::vec3 light_pos = look_at - light_dir * 30.0f; + + const glm::vec3 light_pos = look_at - light_dir * 30.0f; glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); if (glm::abs(glm::dot(light_dir, up)) > 0.99f) { up = glm::vec3(1.0f, 0.0f, 0.0f); } - glm::mat4 light_projection = glm::ortho( + const glm::mat4 light_projection = glm::ortho( -ortho_size, ortho_size, -ortho_size, ortho_size, 1.0f, 60.0f); - glm::mat4 light_view = glm::lookAt(light_pos, look_at, up); + const glm::mat4 light_view = glm::lookAt(light_pos, look_at, up); return light_projection * light_view; } diff --git a/engine/src/hellfire/graphics/renderer/RenderingUtils.h b/engine/src/hellfire/graphics/renderer/RenderingUtils.h index 4736536..8e6dd20 100644 --- a/engine/src/hellfire/graphics/renderer/RenderingUtils.h +++ b/engine/src/hellfire/graphics/renderer/RenderingUtils.h @@ -3,13 +3,12 @@ // #pragma once -#include #include #include "RendererContext.h" -#include "hellfire/assets/AnimationSystem.h" #include "hellfire/ecs/CameraComponent.h" #include "../shader/Shader.h" +#include "hellfire/ecs/LightComponent.h" namespace hellfire { /// Utility class containing shared rendering functionality. @@ -38,10 +37,8 @@ namespace hellfire { // Upload directional lights for (int i = 0; i < renderer_context.num_directional_lights && i < 4; i++) { - Entity* light_entity = renderer_context.directional_light_entities[i]; - if (light_entity) { - auto* light_component = light_entity->get_component(); - if (light_component) { + if (const Entity* light_entity = renderer_context.directional_light_entities[i]) { + if (const auto* light_component = light_entity->get_component()) { light_component->upload_to_shader(shader, i); } } @@ -49,9 +46,9 @@ namespace hellfire { // Upload point lights for (int i = 0; i < renderer_context.num_point_lights && i < 8; i++) { - Entity* light_entity = renderer_context.point_light_entities[i]; + const Entity* light_entity = renderer_context.point_light_entities[i]; if (light_entity) { - auto* light_component = light_entity->get_component(); + const auto* light_component = light_entity->get_component(); if (light_component) { light_component->upload_to_shader(shader, i); } @@ -60,7 +57,7 @@ namespace hellfire { // Upload camera position if (renderer_context.camera_component) { - auto* camera_transform = renderer_context.camera_component->get_owner().get_component(); + const auto* camera_transform = renderer_context.camera_component->get_owner().get_component(); if (camera_transform) { shader.set_camera_position(camera_transform->get_world_position()); } diff --git a/engine/src/hellfire/scene/Scene.cpp b/engine/src/hellfire/scene/Scene.cpp index e71f06c..49fef3f 100644 --- a/engine/src/hellfire/scene/Scene.cpp +++ b/engine/src/hellfire/scene/Scene.cpp @@ -3,7 +3,6 @@ #include #include "../graphics/Skybox.h" -#include "hellfire/assets/AnimationSystem.h" #include "hellfire/ecs/CameraComponent.h" namespace hellfire { diff --git a/engine/src/hellfire/scene/SceneManager.cpp b/engine/src/hellfire/scene/SceneManager.cpp index 6bee3bd..39f1f9f 100644 --- a/engine/src/hellfire/scene/SceneManager.cpp +++ b/engine/src/hellfire/scene/SceneManager.cpp @@ -6,7 +6,7 @@ #include #include "hellfire/ecs/ComponentRegistration.h" -#include "hellfire/serialization/SceneSerializer.h" +#include "hellfire/serializers/SceneSerializer.h" namespace hellfire { SceneManager::SceneManager() : active_scene_(nullptr) { diff --git a/engine/src/hellfire/serializers/MaterialSerializer.cpp b/engine/src/hellfire/serializers/MaterialSerializer.cpp new file mode 100644 index 0000000..cde59bd --- /dev/null +++ b/engine/src/hellfire/serializers/MaterialSerializer.cpp @@ -0,0 +1,246 @@ +// +// Created by denzel on 08/12/2025. +// + +#include "MaterialSerializer.h" + +#include +#include + +#include "hellfire/utilities/SerializerUtils.h" + +namespace hellfire { + bool MaterialSerializer::save(const std::filesystem::path &filepath, const MaterialData &mat) { + std::ofstream file(filepath, std::ios::binary); + if (!file) { + std::cerr << "MaterialSerializer: Cannot open file for writing: " << filepath << std::endl; + return false; + } + + // Header + write_header(file, MAGIC, VERSION); + + // Name + write_binary_string(file, mat.name); + + // Colors (write as raw floats) + write_binary(file, mat.diffuse_color); + write_binary(file, mat.ambient_color); + write_binary(file, mat.specular_color); + write_binary(file, mat.emissive_color); + + // Scalars + write_binary(file, mat.opacity); + write_binary(file, mat.shininess); + write_binary(file, mat.metallic); + write_binary(file, mat.roughness); + + // UV transforms + write_binary(file, mat.uv_scale); + write_binary(file, mat.uv_offset); + + // Flags + write_binary(file, mat.double_sided); + write_binary(file, mat.alpha_blend); + write_binary(file, mat.alpha_cutoff); + + // Texture references + const uint32_t texture_count = static_cast(mat.texture_assets.size()); + write_binary(file, texture_count); + + for (const auto &[type, asset_id]: mat.texture_assets) { + write_binary(file, static_cast(type)); + write_binary(file, asset_id); + } + + return file.good(); + } + + std::optional MaterialSerializer::load(const std::filesystem::path &filepath) { + std::ifstream file(filepath, std::ios::binary); + if (!file) { + std::cerr << "MaterialSerializer: Cannot open file: " << filepath << std::endl; + return std::nullopt; + } + + // Validate header + uint32_t version; + if (!read_and_validate_header(file, MAGIC, VERSION, version)) { + std::cerr << "MaterialSerializer: Invalid file header: " << filepath << std::endl; + return std::nullopt; + } + + MaterialData mat; + + // Name + if (!read_binary_string(file, mat.name)) return std::nullopt; + + // Colors + if (!read_binary(file, mat.diffuse_color)) return std::nullopt; + if (!read_binary(file, mat.ambient_color)) return std::nullopt; + if (!read_binary(file, mat.specular_color)) return std::nullopt; + if (!read_binary(file, mat.emissive_color)) return std::nullopt; + + // Scalars + if (!read_binary(file, mat.opacity)) return std::nullopt; + if (!read_binary(file, mat.shininess)) return std::nullopt; + if (!read_binary(file, mat.metallic)) return std::nullopt; + if (!read_binary(file, mat.roughness)) return std::nullopt; + + // UV transforms + if (!read_binary(file, mat.uv_scale)) return std::nullopt; + if (!read_binary(file, mat.uv_offset)) return std::nullopt; + + // Flags + if (!read_binary(file, mat.double_sided)) return std::nullopt; + if (!read_binary(file, mat.alpha_blend)) return std::nullopt; + if (!read_binary(file, mat.alpha_cutoff)) return std::nullopt; + + // Texture references + uint32_t texture_count; + if (!read_binary(file, texture_count)) return std::nullopt; + + for (uint32_t i = 0; i < texture_count; i++) { + uint32_t type_raw; + AssetID asset_id; + + if (!read_binary(file, type_raw)) return std::nullopt; + if (!read_binary(file, asset_id)) return std::nullopt; + + mat.texture_assets[static_cast(type_raw)] = asset_id; + } + + return mat; + } + + // Helper for TextureType to string conversion + static const char *texture_type_to_string(TextureType type) { + switch (type) { + case TextureType::DIFFUSE: return "diffuse"; + case TextureType::NORMAL: return "normal"; + case TextureType::SPECULAR: return "specular"; + case TextureType::METALNESS: return "metalness"; + case TextureType::ROUGHNESS: return "roughness"; + case TextureType::AMBIENT_OCCLUSION: return "ambient_occlusion"; + case TextureType::EMISSIVE: return "emissive"; + default: return "unknown"; + } + } + + static TextureType string_to_texture_type(const std::string &str) { + if (str == "diffuse") return TextureType::DIFFUSE; + if (str == "normal") return TextureType::NORMAL; + if (str == "specular") return TextureType::SPECULAR; + if (str == "metalness") return TextureType::METALNESS; + if (str == "roughness") return TextureType::ROUGHNESS; + if (str == "ambient_occlusion") return TextureType::AMBIENT_OCCLUSION; + if (str == "emissive") return TextureType::EMISSIVE; + return TextureType::DIFFUSE; // fallback + } + + bool MaterialSerializer::save_json(const std::filesystem::path &filepath, const MaterialData &mat) { + nlohmann::json j; + + j["version"] = VERSION; + j["name"] = mat.name; + + j["colors"] = { + {"diffuse", vec3_to_json(mat.diffuse_color)}, + {"ambient", vec3_to_json(mat.ambient_color)}, + {"specular", vec3_to_json(mat.specular_color)}, + {"emissive", vec3_to_json(mat.emissive_color)} + }; + + j["properties"] = { + {"opacity", mat.opacity}, + {"shininess", mat.shininess}, + {"metallic", mat.metallic}, + {"roughness", mat.roughness} + }; + + j["uv"] = { + {"scale", vec2_to_json(mat.uv_scale)}, + {"offset", vec2_to_json(mat.uv_offset)} + }; + + j["flags"] = { + {"double_sided", mat.double_sided}, + {"alpha_blend", mat.alpha_blend}, + {"alpha_cutoff", mat.alpha_cutoff} + }; + + // Textures + nlohmann::json textures = nlohmann::json::object(); + for (const auto &[type, asset_id]: mat.texture_assets) { + textures[texture_type_to_string(type)] = asset_id; + } + j["textures"] = textures; + + std::ofstream file(filepath); + if (!file) return false; + + file << j.dump(2); + return file.good(); + } + + std::optional MaterialSerializer::load_json(const std::filesystem::path &filepath) { + std::ifstream file(filepath); + if (!file) return std::nullopt; + + try { + nlohmann::json j; + file >> j; + + MaterialData mat; + + mat.name = j.value("name", "Material"); + + // Colors + if (j.contains("colors")) { + const auto &colors = j["colors"]; + if (auto v = json_get_vec3(colors, "diffuse")) mat.diffuse_color = *v; + if (auto v = json_get_vec3(colors, "ambient")) mat.ambient_color = *v; + if (auto v = json_get_vec3(colors, "specular")) mat.specular_color = *v; + if (auto v = json_get_vec3(colors, "emissive")) mat.emissive_color = *v; + } + + // Properties + if (j.contains("properties")) { + const auto &props = j["properties"]; + mat.opacity = props.value("opacity", 1.0f); + mat.shininess = props.value("shininess", 32.0f); + mat.metallic = props.value("metallic", 0.0f); + mat.roughness = props.value("roughness", 0.5f); + } + + // UV + if (j.contains("uv")) { + const auto &uv = j["uv"]; + if (auto v = json_get_vec2(uv, "scale")) mat.uv_scale = *v; + if (auto v = json_get_vec2(uv, "offset")) mat.uv_offset = *v; + } + + // Flags + if (j.contains("flags")) { + const auto &flags = j["flags"]; + mat.double_sided = flags.value("double_sided", false); + mat.alpha_blend = flags.value("alpha_blend", false); + mat.alpha_cutoff = flags.value("alpha_cutoff", 0.5f); + } + + // Textures + if (j.contains("textures") && j["textures"].is_object()) { + for (const auto &[key, value]: j["textures"].items()) { + TextureType type = string_to_texture_type(key); + AssetID asset_id = value.get(); + mat.texture_assets[type] = asset_id; + } + } + + return mat; + } catch (const std::exception &e) { + std::cerr << "MaterialSerializer: JSON parse error: " << e.what() << std::endl; + return std::nullopt; + } + } +} diff --git a/engine/src/hellfire/serializers/MaterialSerializer.h b/engine/src/hellfire/serializers/MaterialSerializer.h new file mode 100644 index 0000000..1a5f48f --- /dev/null +++ b/engine/src/hellfire/serializers/MaterialSerializer.h @@ -0,0 +1,29 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include +#include + +#include "glm/glm.hpp" +#include "hellfire/assets/AssetRegistry.h" +#include "hellfire/graphics/material/MaterialData.h" +#include "hellfire/graphics/texture/Texture.h" + +namespace hellfire { + + class MaterialSerializer { + public: + static constexpr uint32_t MAGIC = 0x54414D48; // "HMAT" + static constexpr uint32_t VERSION = 1; + + // Binary format (.hfmat) + static bool save(const std::filesystem::path& filepath, const MaterialData& material); + static std::optional load(const std::filesystem::path& filepath); + + // JSON format for debugging/tools + static bool save_json(const std::filesystem::path& filepath, const MaterialData& material); + static std::optional load_json(const std::filesystem::path& filepath); + }; +} diff --git a/engine/src/hellfire/serialization/ProjectSerializer.h b/engine/src/hellfire/serializers/ProjectSerializer.h similarity index 97% rename from engine/src/hellfire/serialization/ProjectSerializer.h rename to engine/src/hellfire/serializers/ProjectSerializer.h index cacb08e..559994b 100644 --- a/engine/src/hellfire/serialization/ProjectSerializer.h +++ b/engine/src/hellfire/serializers/ProjectSerializer.h @@ -3,7 +3,7 @@ // #pragma once -#include "hellfire/serialization/Serializer.h" +#include "hellfire/serializers/Serializer.h" #include "hellfire/core/Project.h" namespace hellfire { diff --git a/engine/src/hellfire/serialization/SceneSerializer.h b/engine/src/hellfire/serializers/SceneSerializer.h similarity index 98% rename from engine/src/hellfire/serialization/SceneSerializer.h rename to engine/src/hellfire/serializers/SceneSerializer.h index 4b71466..bb0da70 100644 --- a/engine/src/hellfire/serialization/SceneSerializer.h +++ b/engine/src/hellfire/serializers/SceneSerializer.h @@ -5,7 +5,7 @@ #pragma once #include "hellfire/ecs/ComponentRegistry.h" #include "hellfire/scene/Scene.h" -#include "hellfire/serialization/Serializer.h" +#include "hellfire/serializers/Serializer.h" namespace hellfire { template<> diff --git a/engine/src/hellfire/serialization/Serializer.h b/engine/src/hellfire/serializers/Serializer.h similarity index 100% rename from engine/src/hellfire/serialization/Serializer.h rename to engine/src/hellfire/serializers/Serializer.h diff --git a/engine/src/hellfire/utilities/MaterialSerializer.cpp b/engine/src/hellfire/utilities/MaterialSerializer.cpp deleted file mode 100644 index c928183..0000000 --- a/engine/src/hellfire/utilities/MaterialSerializer.cpp +++ /dev/null @@ -1,139 +0,0 @@ -// -// Created by denzel on 11/04/2025. -// - -#include "hellfire/utilities/MaterialSerializer.h" - -using namespace hellfire; - - -json MaterialSerializer::serialize_material(Material *material) { -// if (!material) return json(); -// -// json mat_data; -// mat_data["name"] = material->get_name(); -// -// if (auto *lambert = dynamic_cast(material)) { -// mat_data["type"] = "lambert"; -// mat_data["ambient"] = { -// lambert->get_ambient_color().r, -// lambert->get_ambient_color().g, -// lambert->get_ambient_color().b -// }; -// mat_data["diffuse"] = { -// lambert->get_diffuse_color().r, -// lambert->get_diffuse_color().g, -// lambert->get_diffuse_color().b -// }; -// } else if (auto *phong = dynamic_cast(material)) { -// mat_data["type"] = "phong"; -// mat_data["ambient"] = { -// phong->get_ambient_color().r, -// phong->get_ambient_color().g, -// phong->get_ambient_color().b -// }; -// mat_data["diffuse"] = { -// phong->get_diffuse_color().r, -// phong->get_diffuse_color().g, -// phong->get_diffuse_color().b -// }; -// mat_data["specular"] = { -// phong->get_specular_color().r, -// phong->get_specular_color().g, -// phong->get_specular_color().b -// }; -// mat_data["shininess"] = phong->get_shininess(); -// } -// -// // Serialize textures -// json textures = json::array(); -// for (const auto &texture: material->get_textures()) { -// json tex_data; -// tex_data["path"] = texture->get_path(); -// tex_data["type"] = texture->get_type(); -// textures.push_back(tex_data); -// } -// -// if (!textures.empty()) { -// mat_data["textures"] = textures; -// } -// -// return mat_data; - return json::array(); -} -// -Material *MaterialSerializer::deserialize_material(const json &data) { -// if (!data.contains("type")) { -// return nullptr; -// } -// -// std::string type = data["type"]; -// Material *material = nullptr; -// -// if (type == "lambert") { -// material = new LambertMaterial("Lambert Material"); -// } else if (type == "phong") { -// material = new PhongMaterial("Phong Material"); -// } else { -// return nullptr; -// } -// -// // Set common properties -// if (data.contains("name")) { -// material->set_name(data["name"]); -// } -// -// // Set type-specific properties -// if (type == "lambert" || type == "phong") { -// if (data.contains("ambient") && data["ambient"].is_array() && data["ambient"].size() == 3) { -// glm::vec3 ambient( -// data["ambient"][0], -// data["ambient"][1], -// data["ambient"][2] -// ); -// material->set_ambient_color(ambient); -// } -// -// if (data.contains("diffuse") && data["diffuse"].is_array() && data["diffuse"].size() == 3) { -// glm::vec3 diffuse( -// data["diffuse"][0], -// data["diffuse"][1], -// data["diffuse"][2] -// ); -// material->set_diffuse_color(diffuse); -// } -// } -// -// if (type == "phong") { -// auto *phong = static_cast(material); -// -// if (data.contains("specular") && data["specular"].is_array() && data["specular"].size() == 3) { -// glm::vec3 specular( -// data["specular"][0], -// data["specular"][1], -// data["specular"][2] -// ); -// phong->set_specular_color(specular); -// } -// -// if (data.contains("shininess")) { -// phong->set_shininess(data["shininess"]); -// } -// } -// -// // Load textures -// if (data.contains("textures") && data["textures"].is_array()) { -// for (const auto &texture_data: data["textures"]) { -// if (texture_data.contains("path") && texture_data.contains("type")) { -// std::string path = texture_data["path"]; -// int type = texture_data["type"]; -// material->set_texture(path, static_cast(type)); -// } -// } -// } -// -// return material; - // TODO: FIX this soon - int* test; - return nullptr; -} diff --git a/engine/src/hellfire/utilities/MaterialSerializer.h b/engine/src/hellfire/utilities/MaterialSerializer.h deleted file mode 100644 index 243fd0c..0000000 --- a/engine/src/hellfire/utilities/MaterialSerializer.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// Created by denzel on 11/04/2025. -// - -#ifndef MATERIALSERIALIZER_H -#define MATERIALSERIALIZER_H - -#include - -using json = nlohmann::json; -namespace hellfire { - class Material; - - class MaterialSerializer { - public: - static json serialize_material(Material* material); - - static Material* deserialize_material(const json& data); - }; -} -#endif //MATERIALSERIALIZER_H diff --git a/engine/src/hellfire/utilities/ObjectDeserializer.cpp b/engine/src/hellfire/utilities/ObjectDeserializer.cpp deleted file mode 100644 index b8dc607..0000000 --- a/engine/src/hellfire/utilities/ObjectDeserializer.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// -// Created by denzel on 11/04/2025. -// -#include "hellfire/utilities/ObjectDeserializer.h" - - -namespace hellfire { - -} \ No newline at end of file diff --git a/engine/src/hellfire/utilities/ObjectDeserializer.h b/engine/src/hellfire/utilities/ObjectDeserializer.h deleted file mode 100644 index c12208f..0000000 --- a/engine/src/hellfire/utilities/ObjectDeserializer.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// Created by denzel on 11/04/2025. -// -#pragma once - -#include -#include -#include -#include - -#include "MaterialSerializer.h" -#include "hellfire/assets/ModelLoader.h" - -using json = nlohmann::json; -using namespace hellfire; - -namespace hellfire { - class Light; - class DirectionalLight; - class Cameras; - class SceneManager; - -} -namespace hellfire { - class ObjectDeserializer { - - }; -} \ No newline at end of file diff --git a/engine/src/hellfire/utilities/ObjectSerializer.cpp b/engine/src/hellfire/utilities/ObjectSerializer.cpp deleted file mode 100644 index d641f93..0000000 --- a/engine/src/hellfire/utilities/ObjectSerializer.cpp +++ /dev/null @@ -1,8 +0,0 @@ -// -// Created by denzel on 11/04/2025. -// -#include "hellfire/utilities/ObjectSerializer.h" - -namespace hellfire { - -} diff --git a/engine/src/hellfire/utilities/ObjectSerializer.h b/engine/src/hellfire/utilities/ObjectSerializer.h deleted file mode 100644 index 685da66..0000000 --- a/engine/src/hellfire/utilities/ObjectSerializer.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Created by denzel on 11/04/2025. -// - -#pragma once -#include - -namespace hellfire { - class SceneManager; - - - using json = nlohmann::json; -} - -namespace hellfire { - -} diff --git a/engine/src/hellfire/utilities/TextureUtils.cpp b/engine/src/hellfire/utilities/TextureUtils.cpp deleted file mode 100644 index 71baf15..0000000 --- a/engine/src/hellfire/utilities/TextureUtils.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// -// Created by denzel on 19/09/2025. -// - -#include "hellfire/utilities/TextureUtils.h" -#define STB_IMAGE_RESIZE_IMPLEMENTATION -#include "stb/stb_image_resize2.h" - -unsigned char* resize_image(unsigned char* input, int input_w, int input_h, - int output_w, int output_h, int channels, bool is_srgb = true) { - unsigned char* output = new unsigned char[output_w * output_h * channels]; - - stbir_pixel_layout layout; - switch(channels) { - case 1: layout = STBIR_1CHANNEL; break; - case 2: layout = STBIR_2CHANNEL; break; - case 3: layout = STBIR_RGB; break; - case 4: layout = STBIR_RGBA; break; - default: - delete[] output; - return nullptr; - } - - unsigned char* result; - if (is_srgb) { - result = stbir_resize_uint8_srgb(input, input_w, input_h, 0, - output, output_w, output_h, 0, layout); - } else { - // Use linear for normal maps and data textures - result = stbir_resize_uint8_linear(input, input_w, input_h, 0, - output, output_w, output_h, 0, layout); - } - - if (!result) { - delete[] output; - return nullptr; - } - - return output; -} diff --git a/engine/src/hellfire/utilities/TextureUtils.h b/engine/src/hellfire/utilities/TextureUtils.h deleted file mode 100644 index 76ad866..0000000 --- a/engine/src/hellfire/utilities/TextureUtils.h +++ /dev/null @@ -1,10 +0,0 @@ -// -// Created by denzel on 19/09/2025. -// - -#pragma once - - - -unsigned char* resize_image(unsigned char* input, int input_w, int input_h, - int output_w, int output_h, int channels, bool use_srgb); diff --git a/tests/unit/serializers/test_transform_serializer.cpp b/tests/unit/serializers/test_transform_serializer.cpp index 39e13af..83685eb 100644 --- a/tests/unit/serializers/test_transform_serializer.cpp +++ b/tests/unit/serializers/test_transform_serializer.cpp @@ -4,7 +4,7 @@ #include -#include "hellfire/serialization/Serializer.h" +#include "hellfire/serializers/Serializer.h" #include #include From 1619d1694fad2a7ecf783d9e1eb65a1709af8335 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Mon, 8 Dec 2025 22:56:15 +0100 Subject: [PATCH 20/27] - W.I.P. Asset system for asset browser - Added serializers for models, textures, materials, and meshes --- engine/src/hellfire/assets/AssetManager.cpp | 112 +++++ engine/src/hellfire/assets/AssetManager.h | 36 ++ engine/src/hellfire/assets/AssetRegistry.cpp | 4 + engine/src/hellfire/assets/AssetRegistry.h | 3 +- .../hellfire/assets/models/ModelImporter.cpp | 386 ++++++++++++++++++ .../hellfire/assets/models/ModelImporter.h | 71 ++++ .../assets/models/ModelInstantiator.cpp | 101 +++++ .../assets/models/ModelInstantiator.h | 33 ++ .../hellfire/serializers/MeshSerializer.cpp | 130 ++++++ .../src/hellfire/serializers/MeshSerializer.h | 25 ++ .../hellfire/serializers/ModelSerializer.cpp | 218 ++++++++++ .../hellfire/serializers/ModelSerializer.h | 30 ++ .../serializers/TextureSerializer.cpp | 63 +++ .../hellfire/serializers/TextureSerializer.h | 43 ++ .../src/hellfire/utilities/SerializerUtils.h | 239 ++++++++++- 15 files changed, 1481 insertions(+), 13 deletions(-) create mode 100644 engine/src/hellfire/assets/AssetManager.cpp create mode 100644 engine/src/hellfire/assets/AssetManager.h create mode 100644 engine/src/hellfire/assets/models/ModelImporter.cpp create mode 100644 engine/src/hellfire/assets/models/ModelImporter.h create mode 100644 engine/src/hellfire/assets/models/ModelInstantiator.cpp create mode 100644 engine/src/hellfire/assets/models/ModelInstantiator.h create mode 100644 engine/src/hellfire/serializers/MeshSerializer.cpp create mode 100644 engine/src/hellfire/serializers/MeshSerializer.h create mode 100644 engine/src/hellfire/serializers/ModelSerializer.cpp create mode 100644 engine/src/hellfire/serializers/ModelSerializer.h create mode 100644 engine/src/hellfire/serializers/TextureSerializer.cpp create mode 100644 engine/src/hellfire/serializers/TextureSerializer.h diff --git a/engine/src/hellfire/assets/AssetManager.cpp b/engine/src/hellfire/assets/AssetManager.cpp new file mode 100644 index 0000000..56b9334 --- /dev/null +++ b/engine/src/hellfire/assets/AssetManager.cpp @@ -0,0 +1,112 @@ +// +// Created by denzel on 08/12/2025. +// + +#include "AssetManager.h" + +#include "hellfire/serializers/MaterialSerializer.h" +#include "hellfire/serializers/MeshSerializer.h" + +namespace hellfire { + AssetManager::AssetManager(AssetRegistry ®istry) : registry_(registry) {} + + std::shared_ptr AssetManager::get_mesh(AssetID id) { + // Check cache + if (auto it = mesh_cache_.find(id); it != mesh_cache_.end()) { + return it->second; + } + + // Load from disk + auto meta = registry_.get_asset(id); + if (!meta || meta->type != AssetType::MESH) { + std::cerr << "Invalid mesh asset: " << id << std::endl; + return nullptr; + } + + auto mesh = MeshSerializer::load(registry_.get_absolute_path(id)); + if (!mesh) { + std::cerr << "Failed to load mesh: " << std::endl; + return nullptr; + } + + mesh_cache_[id] = mesh; + return mesh; + } + + std::shared_ptr AssetManager::get_material(AssetID id) { + if (auto it = material_cache_.find(id); it != material_cache_.end()) { + return it->second; + } + + auto meta = registry_.get_asset(id); + if (!meta || meta->type != AssetType::MATERIAL) { + return nullptr; + } + + auto data = MaterialSerializer::load(registry_.get_absolute_path(id)); + if (!data) { + return nullptr; + } + + // Convert MaterialData to Material, loading textures + auto material = MaterialBuilder::create(data->name); + material->set_diffuse_color(data->diffuse_color); + material->set_ambient_color(data->ambient_color); + material->set_specular_color(data->specular_color); + material->set_emissive_color(data->emissive_color); + material->set_opacity(data->opacity); + material->set_shininess(data->shininess); + material->set_metallic(data->metallic); + material->set_roughness(data->roughness); + + for (const auto &tex_id: data->texture_assets | std::views::values) { + if (auto tex = get_texture(tex_id)) { + material->set_texture(tex, 0); + } + } + + material_cache_[id] = material; + return material; + } + + std::shared_ptr AssetManager::get_texture(AssetID id) { + if (auto it = texture_cache_.find(id); it != texture_cache_.end()) { + return it->second; + } + + auto meta = registry_.get_asset(id); + if (!meta || meta->type != AssetType::TEXTURE) { + return nullptr; + } + + auto texture = std::make_shared( + registry_.get_absolute_path(id).string() + ); + + if (!texture->is_valid()) { + return nullptr; + } + + texture_cache_[id] = texture; + return texture; + } + + void AssetManager::unload(AssetID id) { + mesh_cache_.erase(id); + material_cache_.erase(id); + texture_cache_.erase(id); + } + + void AssetManager::clear_cache() { + mesh_cache_.clear(); + material_cache_.clear(); + texture_cache_.clear(); + } + + void AssetManager::reload_modified() { + for (const AssetID id : registry_.get_modified_assets()) { + unload(id); + } + registry_.refresh_assets(); + } +} // hellfire \ No newline at end of file diff --git a/engine/src/hellfire/assets/AssetManager.h b/engine/src/hellfire/assets/AssetManager.h new file mode 100644 index 0000000..effe913 --- /dev/null +++ b/engine/src/hellfire/assets/AssetManager.h @@ -0,0 +1,36 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include "AssetRegistry.h" +#include "hellfire/graphics/Mesh.h" + +namespace hellfire { + class AssetManager { + public: + explicit AssetManager(AssetRegistry& registry); + + // Typed asset loading with caching + std::shared_ptr get_mesh(AssetID id); + std::shared_ptr get_material(AssetID id); + std::shared_ptr get_texture(AssetID id); + + // Cache management + void unload(AssetID id); + void clear_cache(); + void reload_modified(); // Reload assets that changed on disk + + // Stats + size_t get_loaded_mesh_count() const { return mesh_cache_.size(); } + size_t get_loaded_material_count() const { return material_cache_.size(); } + size_t get_loaded_texture_count() const { return texture_cache_.size(); } + + private: + AssetRegistry& registry_; + + std::unordered_map> mesh_cache_; + std::unordered_map> material_cache_; + std::unordered_map> texture_cache_; + }; +} // hellfire \ No newline at end of file diff --git a/engine/src/hellfire/assets/AssetRegistry.cpp b/engine/src/hellfire/assets/AssetRegistry.cpp index 9f2c2e2..e7b057f 100644 --- a/engine/src/hellfire/assets/AssetRegistry.cpp +++ b/engine/src/hellfire/assets/AssetRegistry.cpp @@ -238,10 +238,14 @@ namespace hellfire { {".gltf", AssetType::MODEL}, {".glb", AssetType::MODEL}, {".fbx", AssetType::MODEL}, + {".hfmodel", AssetType::MODEL}, {".hfmat", AssetType::MATERIAL}, + {".hfmesh", AssetType::MESH}, {".hfscene", AssetType::SCENE}, {".frag", AssetType::SHADER}, {".vert", AssetType::SHADER}, + {".glsl", AssetType::SHADER}, + {".shader", AssetType::SHADER} }; auto extension = filepath.extension().string(); diff --git a/engine/src/hellfire/assets/AssetRegistry.h b/engine/src/hellfire/assets/AssetRegistry.h index 6514253..c07afd6 100644 --- a/engine/src/hellfire/assets/AssetRegistry.h +++ b/engine/src/hellfire/assets/AssetRegistry.h @@ -13,11 +13,12 @@ namespace hellfire { enum class AssetType { MODEL, + MESH, TEXTURE, MATERIAL, SCENE, SHADER, - UNKNOWN + UNKNOWN, }; struct AssetMetadata { diff --git a/engine/src/hellfire/assets/models/ModelImporter.cpp b/engine/src/hellfire/assets/models/ModelImporter.cpp new file mode 100644 index 0000000..0bcc6f8 --- /dev/null +++ b/engine/src/hellfire/assets/models/ModelImporter.cpp @@ -0,0 +1,386 @@ +// +// Created by denzel on 08/12/2025. +// + +#include "ModelImporter.h" + + +#include +#include +#include +#include + +#include "glm/detail/type_mat4x4.hpp" +#include "glm/gtc/quaternion.hpp" +#include "glm/gtx/matrix_decompose.hpp" +#include "hellfire/graphics/Mesh.h" +#include "hellfire/graphics/Vertex.h" +#include "hellfire/graphics/material/MaterialData.h" +#include "hellfire/serializers/MaterialSerializer.h" +#include "hellfire/serializers/MeshSerializer.h" + +namespace hellfire { + ModelImporter::ModelImporter(AssetRegistry ®istry, + const std::filesystem::path &output_dir) : registry_(registry), + output_dir_(output_dir) { + std::filesystem::create_directories(output_dir_); + } + + ImportResult ModelImporter::import(const std::filesystem::path &source_path, const ImportSettings &settings) { + ImportResult result; + + source_path_ = source_path; + source_dir_ = source_path.parent_path(); + base_name_ = source_path.stem().string(); + + Assimp::Importer importer; + ai_scene_ = importer.ReadFile(source_path.string(), build_import_flags(settings)); + + if (!ai_scene_ || !ai_scene_->mRootNode) { + result.error_message = importer.GetErrorString(); + return result; + } + + // Process hierarchy starting from root + process_node_hierarchy(ai_scene_->mRootNode, result); + + result.success = true; + ai_scene_ = nullptr; + + return result; + } + + unsigned int ModelImporter::build_import_flags(const ImportSettings &settings) const { + unsigned int flags = aiProcess_ValidateDataStructure; + + if (settings.triangulate) + flags |= aiProcess_Triangulate; + if (settings.generate_normals) + flags |= aiProcess_GenSmoothNormals; + if (settings.generate_tangents) + flags |= aiProcess_CalcTangentSpace; + if (settings.flip_uvs) + flags |= aiProcess_FlipUVs; + if (settings.optimize_meshes) + flags |= aiProcess_OptimizeMeshes | aiProcess_OptimizeGraph; + + flags |= aiProcess_JoinIdenticalVertices; + flags |= aiProcess_ImproveCacheLocality; + + return flags; + } + + void ModelImporter::process_node_hierarchy(const aiNode *node, ImportResult &result, size_t parent_index) { + const size_t current_index = result.nodes.size(); + result.nodes.push_back(convert_node(node)); + auto &imported_node = result.nodes.back(); + + // Link to parent + if (parent_index != SIZE_MAX) { + result.nodes[parent_index].child_indices.push_back(current_index); + } else { + result.root_node_index = current_index; + } + + // Process meshes attached to this node + for (unsigned int i = 0; i < node->mNumMeshes; i++) { + const unsigned int mesh_idx = node->mMeshes[i]; + const aiMesh *ai_mesh = ai_scene_->mMeshes[mesh_idx]; + + ImportedMesh imported_mesh; + imported_mesh.name = ai_mesh->mName.length > 0 + ? ai_mesh->mName.C_Str() + : make_unique_name(base_name_, "mesh", mesh_idx); + + // Process and serialize mesh + imported_mesh.mesh_asset = process_mesh(ai_mesh, mesh_idx); + result.created_mesh_assets.push_back(imported_mesh.mesh_asset); + + // Process and serialize material + if (ai_mesh->mMaterialIndex < ai_scene_->mNumMaterials) { + const aiMaterial *material = ai_scene_->mMaterials[ai_mesh->mMaterialIndex]; + imported_mesh.material_asset = process_material(material, ai_mesh->mMaterialIndex); + + if (imported_mesh.material_asset != INVALID_ASSET_ID) { + result.created_material_assets.push_back(imported_mesh.material_asset); + } + } + + imported_node.mesh_indices.push_back(result.meshes.size()); + result.meshes.push_back(imported_mesh); + } + + // Recurse into children + for (unsigned int i = 0; i < node->mNumChildren; i++) { + process_node_hierarchy(node->mChildren[i], result, current_index); + } + } + + ImportedNode ModelImporter::convert_node(const aiNode *node) const { + ImportedNode result; + + result.name = node->mName.length > 0 ? node->mName.C_Str() : "Node"; + + glm::mat4 transform = convert_matrix(node->mTransformation); + glm::vec3 skew; + glm::vec4 perspective; + glm::quat rotation; + + glm::decompose(transform, result.scale, rotation, result.position, skew, perspective); + result.rotation = glm::eulerAngles(rotation); + + return result; + } + + AssetID ModelImporter::process_mesh(const aiMesh *ai_mesh, size_t mesh_index) { + std::vector vertices; + std::vector indices; + + vertices.reserve(ai_mesh->mNumVertices); + indices.reserve(ai_mesh->mNumFaces * 3); + + // Extract vertices + for (unsigned int i = 0; i < ai_mesh->mNumVertices; i++) { + Vertex v{}; + + v.position = { + ai_mesh->mVertices[i].x, + ai_mesh->mVertices[i].y, + ai_mesh->mVertices[i].z + }; + + if (ai_mesh->HasNormals()) { + v.normal = { + ai_mesh->mNormals[i].x, + ai_mesh->mNormals[i].y, + ai_mesh->mNormals[i].z + }; + } + + if (ai_mesh->HasTangentsAndBitangents()) { + v.tangent = { + ai_mesh->mTangents[i].x, + ai_mesh->mTangents[i].y, + ai_mesh->mTangents[i].z + }; + v.bitangent = { + ai_mesh->mBitangents[i].x, + ai_mesh->mBitangents[i].y, + ai_mesh->mBitangents[i].z + }; + } + + if (ai_mesh->HasTextureCoords(0)) { + v.texCoords = { + ai_mesh->mTextureCoords[0][i].x, + ai_mesh->mTextureCoords[0][i].y + }; + } + + if (ai_mesh->HasVertexColors(0)) { + v.color = { + ai_mesh->mColors[0][i].r, + ai_mesh->mColors[0][i].g, + ai_mesh->mColors[0][i].b + }; + } else { + v.color = glm::vec3(1.0f); + } + + vertices.push_back(v); + } + + // Extract indices + for (unsigned int i = 0; i < ai_mesh->mNumFaces; i++) { + const aiFace &face = ai_mesh->mFaces[i]; + for (unsigned int j = 0; j < face.mNumIndices; j++) { + indices.push_back(face.mIndices[j]); + } + } + + // Serialize to file + Mesh mesh(vertices, indices); + const std::string filename = make_unique_name(base_name_, "mesh", mesh_index) + ".hfmesh"; + const auto filepath = output_dir_ / filename; + + + if (!MeshSerializer::save(filepath, mesh)) { + std::cerr << "Failed to save mesh: " << filepath << std::endl; + return INVALID_ASSET_ID; + } + + return registry_.register_asset(filepath, AssetType::MESH); + } + + AssetID ModelImporter::process_material(const aiMaterial *ai_mat, size_t material_index) { + MaterialData data; + + // Get name + aiString name; + if (ai_mat->Get(AI_MATKEY_NAME, name) == AI_SUCCESS) { + data.name = name.C_Str(); + } else { + data.name = make_unique_name(base_name_, "material", material_index); + } + + // Colors + aiColor3D color; + if (ai_mat->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) + data.diffuse_color = {color.r, color.g, color.b}; + if (ai_mat->Get(AI_MATKEY_COLOR_AMBIENT, color) == AI_SUCCESS) + data.ambient_color = {color.r, color.g, color.b}; + if (ai_mat->Get(AI_MATKEY_COLOR_SPECULAR, color) == AI_SUCCESS) + data.specular_color = {color.r, color.g, color.b}; + if (ai_mat->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS) + data.emissive_color = {color.r, color.g, color.b}; + + // Scalars + float value; + if (ai_mat->Get(AI_MATKEY_OPACITY, value) == AI_SUCCESS) + data.opacity = value; + if (ai_mat->Get(AI_MATKEY_SHININESS, value) == AI_SUCCESS) + data.shininess = std::max(value, 1.0f); + if (ai_mat->Get(AI_MATKEY_METALLIC_FACTOR, value) == AI_SUCCESS) + data.metallic = value; + if (ai_mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, value) == AI_SUCCESS) + data.roughness = value; + + // Textures + auto try_load_texture = [&](aiTextureType ai_type, TextureType hf_type) { + if (ai_mat->GetTextureCount(ai_type) == 0) return; + + aiString tex_path; + if (ai_mat->GetTexture(ai_type, 0, &tex_path) != AI_SUCCESS) return; + + AssetID tex_asset = process_texture(tex_path.C_Str(), hf_type); + if (tex_asset != INVALID_ASSET_ID) { + data.texture_assets[hf_type] = tex_asset; + } + }; + + try_load_texture(aiTextureType_DIFFUSE, TextureType::DIFFUSE); + try_load_texture(aiTextureType_NORMALS, TextureType::NORMAL); + try_load_texture(aiTextureType_SPECULAR, TextureType::SPECULAR); + try_load_texture(aiTextureType_METALNESS, TextureType::METALNESS); + try_load_texture(aiTextureType_DIFFUSE_ROUGHNESS, TextureType::ROUGHNESS); + try_load_texture(aiTextureType_AMBIENT_OCCLUSION, TextureType::AMBIENT_OCCLUSION); + try_load_texture(aiTextureType_EMISSIVE, TextureType::EMISSIVE); + + // Serialize + const std::string filename = data.name + ".hfmat"; + const auto filepath = output_dir_ / filename; + + if (!MaterialSerializer::save(filepath, data)) { + std::cerr << "Failed to save material: " << filepath << std::endl; + return INVALID_ASSET_ID; + } + + return registry_.register_asset(filepath, AssetType::MATERIAL); + } + + AssetID ModelImporter::process_texture(const std::string &texture_ref, TextureType type) { + std::filesystem::path resolved_path; + + if (is_embedded_texture(texture_ref)) { + size_t index = std::stoul(texture_ref.substr(1)); + resolved_path = extract_embedded_texture(index); + } else { + auto path_opt = resolve_texture_path(texture_ref); + if (!path_opt) { + std::cerr << "Could not resolve texture: " << texture_ref << std::endl; + return INVALID_ASSET_ID; + } + resolved_path = *path_opt; + } + + // Check if already registered + if (auto existing = registry_.get_uuid_by_path(resolved_path)) { + return *existing; + } + + // For textures, we just register the original file + return registry_.register_asset(resolved_path, AssetType::TEXTURE); + } + + std::optional ModelImporter::resolve_texture_path(const std::string &texture_ref) const { + const std::filesystem::path tex_filename = std::filesystem::path(texture_ref).filename(); + + const std::vector search_paths = { + source_dir_ / texture_ref, + source_dir_ / tex_filename, + source_dir_ / "textures" / tex_filename, + source_dir_ / "Textures" / tex_filename, + source_dir_ / "materials" / tex_filename, + source_dir_.parent_path() / "textures" / tex_filename, + }; + + for (const auto &path: search_paths) { + if (std::filesystem::exists(path)) { + return std::filesystem::canonical(path); + } + } + + return std::nullopt; + } + + bool ModelImporter::is_embedded_texture(const std::string &path) const { + return !path.empty() && path[0] == '*'; + } + + std::filesystem::path ModelImporter::extract_embedded_texture(size_t index) { + if (index >= ai_scene_->mNumTextures) { + return {}; + } + + const aiTexture *tex = ai_scene_->mTextures[index]; + + std::string extension = tex->achFormatHint; + if (extension.empty()) { + // Detect from magic bytes + const auto *data = reinterpret_cast(tex->pcData); + if (data[0] == 0xFF && data[1] == 0xD8) extension = "jpg"; + else if (data[0] == 0x89 && data[1] == 0x50) extension = "png"; + else extension = "bin"; + } + + const std::string filename = make_unique_name(base_name_, "texture", index) + + "." + extension; + const auto filepath = output_dir_ / filename; + + std::ofstream file(filepath, std::ios::binary); + if (!file) return {}; + + // mHeight == 0 means compressed format, mWidth is byte count + const size_t size = (tex->mHeight == 0) + ? tex->mWidth + : tex->mWidth * tex->mHeight * 4; + + file.write(reinterpret_cast(tex->pcData), size); + + return filepath; + } + + glm::mat4 ModelImporter::convert_matrix(const aiMatrix4x4 &m) { + return glm::mat4( + m.a1, m.b1, m.c1, m.d1, + m.a2, m.b2, m.c2, m.d2, + m.a3, m.b3, m.c3, m.d3, + m.a4, m.b4, m.c4, m.d4 + ); + } + + bool ModelImporter::is_identity(const aiMatrix4x4 &m) { + constexpr float epsilon = 0.0001f; + return std::abs(m.a1 - 1.0f) < epsilon && std::abs(m.b2 - 1.0f) < epsilon + && std::abs(m.c3 - 1.0f) < epsilon && std::abs(m.d4 - 1.0f) < epsilon + && std::abs(m.a2) < epsilon && std::abs(m.a3) < epsilon && std::abs(m.a4) < epsilon + && std::abs(m.b1) < epsilon && std::abs(m.b3) < epsilon && std::abs(m.b4) < epsilon + && std::abs(m.c1) < epsilon && std::abs(m.c2) < epsilon && std::abs(m.c4) < epsilon + && std::abs(m.d1) < epsilon && std::abs(m.d2) < epsilon && std::abs(m.d3) < epsilon; + } + + std::string ModelImporter::make_unique_name(const std::string &base, const std::string &suffix, + size_t index) const { + return base + "_" + suffix + "_" + std::to_string(index); + } +} // hellfire diff --git a/engine/src/hellfire/assets/models/ModelImporter.h b/engine/src/hellfire/assets/models/ModelImporter.h new file mode 100644 index 0000000..1db400f --- /dev/null +++ b/engine/src/hellfire/assets/models/ModelImporter.h @@ -0,0 +1,71 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include "ImportResult.h" +#include "assimp/matrix4x4.h" +#include "assimp/scene.h" +#include "glm/detail/type_mat.hpp" +#include "hellfire/assets/AssetRegistry.h" +#include "hellfire/graphics/texture/Texture.h" + +namespace hellfire { + + struct ImportSettings { + bool generate_normals = true; + bool generate_tangents = true; + bool triangulate = true; + bool flip_uvs = false; + bool optimize_meshes = true; + float scale_factor = 1.0f; + }; + + /** + * @brief Converts external model formats (FBX, GLTF, OBJ) into internal assets + * this runs once during asset import, NOT at runtime! + */ + class ModelImporter { + ModelImporter(AssetRegistry& registry, const std::filesystem::path& output_dir); + + ImportResult import(const std::filesystem::path& source_path, const ImportSettings& settings = {}); + + private: + AssetRegistry& registry_; + std::filesystem::path output_dir_; + + // Current import state + const aiScene* ai_scene_ = nullptr; + std::filesystem::path source_path_; + std::filesystem::path source_dir_; + std::string base_name_; + + // Processing pipeline + unsigned int build_import_flags(const ImportSettings& settings) const; + + // Node processing + void process_node_hierarchy(const aiNode* node, ImportResult& result, + size_t parent_index = SIZE_MAX); + ImportedNode convert_node(const aiNode* node) const; + + // Mesh processing + AssetID process_mesh(const aiMesh* mesh, size_t mesh_index); + + // Material processing + AssetID process_material(const aiMaterial* ai_mat, size_t material_index); + AssetID process_texture(const std::string& texture_path, TextureType type); + + // Texture resolution + std::optional resolve_texture_path( + const std::string& texture_ref) const; + bool is_embedded_texture(const std::string& path) const; + std::filesystem::path extract_embedded_texture(size_t index); + + // Utility + static glm::mat4 convert_matrix(const aiMatrix4x4& m); + static bool is_identity(const aiMatrix4x4& m); + std::string make_unique_name(const std::string& base, + const std::string& suffix, + size_t index) const; + }; +} // hellfire \ No newline at end of file diff --git a/engine/src/hellfire/assets/models/ModelInstantiator.cpp b/engine/src/hellfire/assets/models/ModelInstantiator.cpp new file mode 100644 index 0000000..0de554e --- /dev/null +++ b/engine/src/hellfire/assets/models/ModelInstantiator.cpp @@ -0,0 +1,101 @@ +// +// Created by denzel on 08/12/2025. +// + +#include "ModelInstantiator.h" + +#include "hellfire/ecs/RenderableComponent.h" +#include "hellfire/ecs/TransformComponent.h" +#include "hellfire/ecs/components/MeshComponent.h" + +namespace hellfire { + ModelInstantiator::ModelInstantiator(Scene &scene, AssetManager &assets) : scene_(scene), assets_(assets) {} + + EntityID ModelInstantiator::instantiate(const ImportResult &result, const EntityID parent) { + if (!result.success || result.nodes.empty()) { + return 0; + } + + return instantiate_node(result, result.root_node_index, parent); + } + + EntityID ModelInstantiator::instantiate_mesh(AssetID mesh_asset, AssetID material_asset, EntityID parent) { + auto mesh = assets_.get_mesh(mesh_asset); + if (!mesh) return 0; + + EntityID entity_id = scene_.create_entity("Mesh"); + Entity* entity = scene_.get_entity(entity_id); + + if (parent != 0) { + scene_.set_parent(entity_id, parent); + } + + auto* mesh_comp = entity->add_component(); + mesh_comp->set_mesh(mesh); + + if (material_asset != INVALID_ASSET_ID) { + auto material = assets_.get_material(material_asset); + if (material) { + auto* renderable = entity->add_component(); + renderable->set_material(material); + } + } + + return entity_id; + } + + EntityID ModelInstantiator::instantiate_node(const ImportResult &result, size_t node_index, EntityID parent) { + const auto& node = result.nodes[node_index]; + + EntityID entity_id = scene_.create_entity(node.name); + Entity* entity = scene_.get_entity(entity_id); + + // Set transform + entity->transform()->set_position(node.position); + entity->transform()->set_rotation(node.rotation); + entity->transform()->set_scale(node.scale); + + if (parent != 0) { + scene_.set_parent(entity_id, parent); + } + + // Attach meshes + if (node.mesh_indices.size() == 1) { + // Single mesh: attach directly + const auto& mesh_data = result.meshes[node.mesh_indices[0]]; + + auto* mesh_comp = entity->add_component(); + mesh_comp->set_mesh(assets_.get_mesh(mesh_data.mesh_asset)); + + if (mesh_data.material_asset != INVALID_ASSET_ID) { + auto* renderable = entity->add_component(); + renderable->set_material(assets_.get_material(mesh_data.material_asset)); + } + } else if (node.mesh_indices.size() > 1) { + // Multiple meshes: create child entities + for (size_t mesh_idx : node.mesh_indices) { + const auto& mesh_data = result.meshes[mesh_idx]; + + EntityID mesh_entity = scene_.create_entity(mesh_data.name); + scene_.set_parent(mesh_entity, entity_id); + + Entity* mesh_ent = scene_.get_entity(mesh_entity); + + auto* mesh_comp = mesh_ent->add_component(); + mesh_comp->set_mesh(assets_.get_mesh(mesh_data.mesh_asset)); + + if (mesh_data.material_asset != INVALID_ASSET_ID) { + auto* renderable = mesh_ent->add_component(); + renderable->set_material(assets_.get_material(mesh_data.material_asset)); + } + } + } + + // Instantiate children + for (size_t child_idx : node.child_indices) { + instantiate_node(result, child_idx, entity_id); + } + + return entity_id; + } +} diff --git a/engine/src/hellfire/assets/models/ModelInstantiator.h b/engine/src/hellfire/assets/models/ModelInstantiator.h new file mode 100644 index 0000000..6a304cd --- /dev/null +++ b/engine/src/hellfire/assets/models/ModelInstantiator.h @@ -0,0 +1,33 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include "ImportResult.h" +#include "hellfire/assets/AssetManager.h" +#include "hellfire/scene/Scene.h" + +namespace hellfire { + /** + * @brief Creates scene entities from an ImportResult + */ + class ModelInstantiator { + public: + ModelInstantiator(Scene& scene, AssetManager& assets); + + EntityID instantiate(const ImportResult& result, EntityID parent = 0); + + // Instantiate a single mesh as an entity + EntityID instantiate_mesh(AssetID mesh_asset, + AssetID material_asset = INVALID_ASSET_ID, + EntityID parent = 0); + + private: + Scene& scene_; + AssetManager& assets_; + + EntityID instantiate_node(const ImportResult& result, + size_t node_index, + EntityID parent); + }; +} diff --git a/engine/src/hellfire/serializers/MeshSerializer.cpp b/engine/src/hellfire/serializers/MeshSerializer.cpp new file mode 100644 index 0000000..ab607a2 --- /dev/null +++ b/engine/src/hellfire/serializers/MeshSerializer.cpp @@ -0,0 +1,130 @@ +// +// Created by denzel on 08/12/2025. +// + +#include "hellfire/graphics/Vertex.h" +#include "MeshSerializer.h" + +#include + +#include "hellfire/utilities/SerializerUtils.h" + +namespace hellfire { + bool MeshSerializer::save(const std::filesystem::path &filepath, const Mesh &mesh) { + std::ofstream file(filepath, std::ios::binary); + if (!file) { + std::cerr << "MeshSerializer: Cannot open file for writing: " << filepath << std::endl; + return false; + } + + // Header + if (!write_header(file, MAGIC, VERSION)) { + return false; + } + + // Mesh flags + write_binary(file, mesh.is_wireframe); + + // Vertex data + write_vertex_vector(file, mesh.vertices); + + // Index data + write_binary_vector(file, mesh.indices); + + return file.good(); + } + + std::shared_ptr MeshSerializer::load(const std::filesystem::path &filepath) { + std::ifstream file(filepath, std::ios::binary); + if (!file) { + std::cerr << "MeshSerializer: Cannot open file: " << filepath << std::endl; + return nullptr; + } + + // Validate header + uint32_t version; + if (!read_and_validate_header(file, MAGIC, VERSION, version)) { + std::cerr << "MeshSerializer: Invalid file header: " << filepath << std::endl; + return nullptr; + } + + auto mesh = std::make_shared(); + + // Mesh flags + if (!read_binary(file, mesh->is_wireframe)) { + return nullptr; + } + + // Vertex data + if (!read_vertex_vector(file, mesh->vertices)) { + return nullptr; + } + + // Index data + if (!read_binary_vector(file, mesh->indices)) { + return nullptr; + } + + return mesh; + } + + bool MeshSerializer::save_json(const std::filesystem::path &filepath, const Mesh &mesh) { + nlohmann::json j; + j["version"] = VERSION; + j["is_wireframe"] = mesh.is_wireframe; + + // Vertices + auto &verts = j["vertices"]; + for (const auto &v: mesh.vertices) { + verts.push_back({ + {"position", vec3_to_json(v.position)}, + {"normal", vec3_to_json(v.normal)}, + {"texCoords", vec2_to_json(v.texCoords)}, + {"color", vec3_to_json(v.color)}, + {"tangent", vec3_to_json(v.tangent)}, + {"bitangent", vec3_to_json(v.bitangent)} + }); + } + + j["indices"] = mesh.indices; + + std::ofstream file(filepath); + if (!file) return false; + + file << j.dump(2); + return file.good(); + } + + std::shared_ptr MeshSerializer::load_json(const std::filesystem::path &filepath) { + std::ifstream file(filepath); + if (!file) return nullptr; + + try { + nlohmann::json j; + file >> j; + + auto mesh = std::make_shared(); + mesh->is_wireframe = j.value("is_wireframe", false); + + for (const auto &v: j["vertices"]) { + Vertex vert{}; + + if (auto pos = json_get_vec3(v, "position")) vert.position = *pos; + if (auto norm = json_get_vec3(v, "normal")) vert.normal = *norm; + if (auto tex = json_get_vec2(v, "texCoords")) vert.texCoords = *tex; + if (auto col = json_get_vec3(v, "color")) vert.color = *col; + if (auto tan = json_get_vec3(v, "tangent")) vert.tangent = *tan; + if (auto bitan = json_get_vec3(v, "bitangent")) vert.bitangent = *bitan; + + mesh->vertices.push_back(vert); + } + + mesh->indices = j["indices"].get >(); + + return mesh; + } catch (const std::exception &e) { + std::cerr << "MeshSerializer: JSON parse error: " << e.what() << std::endl; + return nullptr; + } + } +} diff --git a/engine/src/hellfire/serializers/MeshSerializer.h b/engine/src/hellfire/serializers/MeshSerializer.h new file mode 100644 index 0000000..3ffa981 --- /dev/null +++ b/engine/src/hellfire/serializers/MeshSerializer.h @@ -0,0 +1,25 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include +#include + +#include "hellfire/graphics/Mesh.h" + +namespace hellfire { + // Binary format for .hfmesh files + class MeshSerializer { + public: + static constexpr uint32_t MAGIC = 0x4853454D; // MESH + static constexpr uint32_t VERSION = 1; + + static bool save(const std::filesystem::path& filepath, const Mesh& mesh); + static std::shared_ptr load(const std::filesystem::path& filepath); + + // JSON format for debugging/tools + static bool save_json(const std::filesystem::path& filepath, const Mesh& mesh); + static std::shared_ptr load_json(const std::filesystem::path& filepath); + }; +} diff --git a/engine/src/hellfire/serializers/ModelSerializer.cpp b/engine/src/hellfire/serializers/ModelSerializer.cpp new file mode 100644 index 0000000..1fcc69b --- /dev/null +++ b/engine/src/hellfire/serializers/ModelSerializer.cpp @@ -0,0 +1,218 @@ +// +// Created by denzel on 08/12/2025. +// + +#include "ModelSerializer.h" + +#include +#include + +#include "hellfire/utilities/SerializerUtils.h" +#include "glm/glm.hpp" + +namespace hellfire { + bool ModelSerializer::save(const std::filesystem::path &filepath, const ImportResult &result) { + if (!result.success) { + std::cerr << "ModelSerializer: Cannot save failed import result" << std::endl; + return false; + } + + std::ofstream file(filepath, std::ios::binary); + if (!file) { + std::cerr << "ModelSerializer: Cannot open file for writing: " << filepath << std::endl; + return false; + } + + // Header + write_header(file, MAGIC, VERSION); + + // Root node index + write_binary(file, static_cast(result.root_node_index)); + + // Nodes + const uint32_t node_count = static_cast(result.nodes.size()); + write_binary(file, node_count); + + for (const auto& node : result.nodes) { + write_binary_string(file, node.name); + write_binary(file, node.position); + write_binary(file, node.rotation); + write_binary(file, node.scale); + + // Mesh indices + const uint32_t mesh_idx_count = static_cast(node.mesh_indices.size()); + write_binary(file, mesh_idx_count); + for (size_t idx : node.mesh_indices) { + write_binary(file, static_cast(idx)); + } + + // Child indices + const uint32_t child_idx_count = static_cast(node.child_indices.size()); + write_binary(file, child_idx_count); + for (size_t idx : node.child_indices) { + write_binary(file, static_cast(idx)); + } + } + + // Meshes + const uint32_t mesh_count = static_cast(result.meshes.size()); + write_binary(file, mesh_count); + + for (const auto& mesh : result.meshes) { + write_binary_string(file, mesh.name); + write_binary(file, mesh.mesh_asset); + write_binary(file, mesh.material_asset); + } + + return file.good(); + } + + std::optional ModelSerializer::load(const std::filesystem::path &filepath) { + std::ifstream file(filepath, std::ios::binary); + if (!file) { + std::cerr << "ModelSerializer: Cannot open file: " << filepath << std::endl; + return std::nullopt; + } + + // Validate header + uint32_t version; + if (!read_and_validate_header(file, MAGIC, VERSION, version)) { + std::cerr << "ModelSerializer: Invalid file header: " << filepath << std::endl; + return std::nullopt; + } + + ImportResult result; + result.success = true; + + // Root node index + uint32_t root_idx; + if (!read_binary(file, root_idx)) return std::nullopt; + result.root_node_index = root_idx; + + // Nodes + uint32_t node_count; + if (!read_binary(file, node_count)) return std::nullopt; + result.nodes.resize(node_count); + + for (auto& node : result.nodes) { + if (!read_binary_string(file, node.name)) return std::nullopt; + if (!read_binary(file, node.position)) return std::nullopt; + if (!read_binary(file, node.rotation)) return std::nullopt; + if (!read_binary(file, node.scale)) return std::nullopt; + + // Mesh indices + uint32_t mesh_idx_count; + if (!read_binary(file, mesh_idx_count)) return std::nullopt; + node.mesh_indices.resize(mesh_idx_count); + for (uint32_t i = 0; i < mesh_idx_count; i++) { + uint32_t idx; + if (!read_binary(file, idx)) return std::nullopt; + node.mesh_indices[i] = idx; + } + + // Child indices + uint32_t child_idx_count; + if (!read_binary(file, child_idx_count)) return std::nullopt; + node.child_indices.resize(child_idx_count); + for (uint32_t i = 0; i < child_idx_count; i++) { + uint32_t idx; + if (!read_binary(file, idx)) return std::nullopt; + node.child_indices[i] = idx; + } + } + + // Meshes + uint32_t mesh_count; + if (!read_binary(file, mesh_count)) return std::nullopt; + result.meshes.resize(mesh_count); + + for (auto& mesh : result.meshes) { + if (!read_binary_string(file, mesh.name)) return std::nullopt; + if (!read_binary(file, mesh.mesh_asset)) return std::nullopt; + if (!read_binary(file, mesh.material_asset)) return std::nullopt; + } + + return result; + } + + bool ModelSerializer::save_json(const std::filesystem::path &filepath, const ImportResult &result) { + if (!result.success) return false; + + nlohmann::json j; + j["version"] = VERSION; + j["root_node_index"] = result.root_node_index; + + // Nodes + auto& nodes_json = j["nodes"]; + for (const auto& node : result.nodes) { + nlohmann::json node_json; + node_json["name"] = node.name; + node_json["position"] = vec3_to_json(node.position); + node_json["rotation"] = vec3_to_json(node.rotation); + node_json["scale"] = vec3_to_json(node.scale); + node_json["mesh_indices"] = node.mesh_indices; + node_json["child_indices"] = node.child_indices; + nodes_json.push_back(node_json); + } + + // Meshes + auto& meshes_json = j["meshes"]; + for (const auto& mesh : result.meshes) { + meshes_json.push_back({ + {"name", mesh.name}, + {"mesh_asset", mesh.mesh_asset}, + {"material_asset", mesh.material_asset} + }); + } + + std::ofstream file(filepath); + if (!file) return false; + + file << j.dump(2); + return file.good(); + } + + std::optional ModelSerializer::load_json(const std::filesystem::path &filepath) { + std::ifstream file(filepath); + if (!file) return std::nullopt; + + try { + nlohmann::json j; + file >> j; + + ImportResult result; + result.success = true; + result.root_node_index = j.value("root_node_index", 0); + + // Nodes + for (const auto& node_json : j["nodes"]) { + ImportedNode node; + node.name = node_json.value("name", "Node"); + + if (auto v = json_get_vec3(node_json, "position")) node.position = *v; + if (auto v = json_get_vec3(node_json, "rotation")) node.rotation = *v; + if (auto v = json_get_vec3(node_json, "scale")) node.scale = *v; + else node.scale = glm::vec3(1.0f); + + node.mesh_indices = node_json["mesh_indices"].get>(); + node.child_indices = node_json["child_indices"].get>(); + + result.nodes.push_back(node); + } + + // Meshes + for (const auto& mesh_json : j["meshes"]) { + ImportedMesh mesh; + mesh.name = mesh_json.value("name", "Mesh"); + mesh.mesh_asset = mesh_json.value("mesh_asset", INVALID_ASSET_ID); + mesh.material_asset = mesh_json.value("material_asset", INVALID_ASSET_ID); + result.meshes.push_back(mesh); + } + + return result; + } catch (const std::exception& e) { + std::cerr << "ModelSerializer: JSON parse error: " << e.what() << std::endl; + return std::nullopt; + } + } +} // hellfire \ No newline at end of file diff --git a/engine/src/hellfire/serializers/ModelSerializer.h b/engine/src/hellfire/serializers/ModelSerializer.h new file mode 100644 index 0000000..5fd3299 --- /dev/null +++ b/engine/src/hellfire/serializers/ModelSerializer.h @@ -0,0 +1,30 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include +#include +#include + +#include "hellfire/assets/models/ImportResult.h" + +namespace hellfire { + /** + * @brief Serializes the ImportResult to a .hfmodel file + * + * This captures the hierarchy and asset references for instantiation + */ + class ModelSerializer { + public: + static constexpr uint32_t MAGIC = 0x4C444F4D; // "MODL" + static constexpr uint32_t VERSION = 1; + + static bool save(const std::filesystem::path& filepath, const ImportResult& result); + static std::optional load(const std::filesystem::path& filepath); + + // JSON for debugging + static bool save_json(const std::filesystem::path& filepath, const ImportResult& result); + static std::optional load_json(const std::filesystem::path& filepath); + }; +} // hellfire \ No newline at end of file diff --git a/engine/src/hellfire/serializers/TextureSerializer.cpp b/engine/src/hellfire/serializers/TextureSerializer.cpp new file mode 100644 index 0000000..15b81d1 --- /dev/null +++ b/engine/src/hellfire/serializers/TextureSerializer.cpp @@ -0,0 +1,63 @@ +// +// Created by denzel on 08/12/2025. +// + +#include "TextureSerializer.h" + +#include + +#include "json.hpp" + +namespace hellfire { + static std::filesystem::path get_meta_path(const std::filesystem::path& texture_path) { + return texture_path.string() + ".meta"; + } + + bool TextureSerializer::save_metadata(const std::filesystem::path &texture_path, const TextureMetadata &meta) { + nlohmann::json j; + + j["type"] = static_cast(meta.type); + j["generate_mipmaps"] = meta.generate_mipmaps; + j["srgb"] = meta.srgb; + j["filter"] = static_cast(meta.filter); + j["wrap_u"] = static_cast(meta.wrap_u); + j["wrap_v"] = static_cast(meta.wrap_v); + j["compressed"] = meta.compressed; + j["compression_format"] = meta.compression_format; + + std::ofstream file(get_meta_path(texture_path)); + if (!file) return false; + + file << j.dump(2); + return file.good(); + } + + std::optional TextureSerializer::load_metadata(const std::filesystem::path &texture_path) { + const auto meta_path = get_meta_path(texture_path); + if (!std::filesystem::exists(meta_path)) { + return std::nullopt; // No metadata, use defaults + } + + std::ifstream file(meta_path); + if (!file) return std::nullopt; + + try { + nlohmann::json j; + file >> j; + + TextureMetadata meta; + meta.type = static_cast(j.value("type", 0)); + meta.generate_mipmaps = j.value("generate_mipmaps", true); + meta.srgb = j.value("srgb", true); + meta.filter = static_cast(j.value("filter", 2)); + meta.wrap_u = static_cast(j.value("wrap_u", 0)); + meta.wrap_v = static_cast(j.value("wrap_v", 0)); + meta.compressed = j.value("compressed", false); + meta.compression_format = j.value("compression_format", ""); + + return meta; + } catch (...) { + return std::nullopt; + } + } +} // hellfire \ No newline at end of file diff --git a/engine/src/hellfire/serializers/TextureSerializer.h b/engine/src/hellfire/serializers/TextureSerializer.h new file mode 100644 index 0000000..984cb5e --- /dev/null +++ b/engine/src/hellfire/serializers/TextureSerializer.h @@ -0,0 +1,43 @@ +// +// Created by denzel on 08/12/2025. +// + +#pragma once +#include + +#include "hellfire/graphics/texture/Texture.h" + +namespace hellfire { + /** + * @brief Metadata for texture assets + * + * The actual pixel data is stored in standard formats (PNG, JPG) + */ + struct TextureMetadata { + TextureType type = TextureType::DIFFUSE; + bool generate_mipmaps = true; + bool srgb = true; + + // Filtering + enum class FilterMode { NEAREST, LINEAR, TRILINEAR }; + FilterMode filter = FilterMode::TRILINEAR; + + // Wrapping + enum class WrapMode { REPEAT, CLAMP, MIRROR }; + WrapMode wrap_u = WrapMode::REPEAT; + WrapMode wrap_v = WrapMode::REPEAT; + + // Compression (for baked assets) + bool compressed = false; + std::string compression_format; // "BC1", "BC3", "BC7", etc. + }; + + class TextureSerializer { + public: + static bool save_metadata(const std::filesystem::path& texture_path, + const TextureMetadata& meta); + + static std::optional load_metadata( + const std::filesystem::path& texture_path); + }; +} // hellfire \ No newline at end of file diff --git a/engine/src/hellfire/utilities/SerializerUtils.h b/engine/src/hellfire/utilities/SerializerUtils.h index cda3048..f49098a 100644 --- a/engine/src/hellfire/utilities/SerializerUtils.h +++ b/engine/src/hellfire/utilities/SerializerUtils.h @@ -5,35 +5,250 @@ #pragma once #include -#include "glm/glm/detail/type_vec.hpp" +#include "glm/glm.hpp" +#include "glm/gtc/quaternion.hpp" +#include "hellfire/graphics/Vertex.h" #include "nlohmann/json.hpp" namespace hellfire { + /// Binary I/O + template + void write_binary(std::ostream &out, const T &val) { + static_assert(std::is_trivially_copyable_v, "Type must be trivially copyable"); + out.write(reinterpret_cast(&val), sizeof(T)); + } + + template + bool read_binary(std::istream &in, T &val) { + static_assert(std::is_trivially_copyable_v, "Type must be trivially copyable"); + in.read(reinterpret_cast(&val), sizeof(T)); + return in.good(); + } + + template + void write_binary_vector(std::ostream &out, const std::vector &vec) { + static_assert(std::is_trivially_copyable_v, "Type must be trivially copyable"); + const uint32_t size = static_cast(vec.size()); + write_binary(out, size); + if (size > 0) { + out.write(reinterpret_cast(vec.data()), size * sizeof(T)); + } + } + + template + bool read_binary_vector(std::istream &in, std::vector &vec) { + static_assert(std::is_trivially_copyable_v, "Type must be trivially copyable"); + uint32_t size; + if (!read_binary(in, size)) return false; + vec.resize(size); + if (size > 0) { + in.read(reinterpret_cast(vec.data()), size * sizeof(T)); + } + return in.good(); + } + + inline void write_binary_string(std::ostream &out, const std::string &str) { + const uint32_t length = static_cast(str.size()); + write_binary(out, length); + if (length > 0) { + out.write(str.data(), length); + } + } + + inline bool read_binary_string(std::istream &in, std::string &str) { + uint32_t length; + if (!read_binary(in, length)) return false; + str.resize(length); + if (length > 0) { + in.read(str.data(), length); + } + return in.good(); + } + + + + /// GLM Binary I/O + inline void write_binary(std::ostream &out, const glm::vec2 &v) { + out.write(reinterpret_cast(&v.x), sizeof(float) * 2); + } + + inline void write_binary(std::ostream &out, const glm::vec3 &v) { + out.write(reinterpret_cast(&v.x), sizeof(float) * 3); + } + + inline void write_binary(std::ostream &out, const glm::vec4 &v) { + out.write(reinterpret_cast(&v.x), sizeof(float) * 4); + } + + inline void write_binary(std::ostream &out, const glm::quat &q) { + out.write(reinterpret_cast(&q.x), sizeof(float) * 4); + } + + inline void write_binary(std::ostream &out, const glm::mat4 &m) { + out.write(reinterpret_cast(&m[0][0]), sizeof(float) * 16); + } + + inline void write_binary(std::ostream &out, const glm::mat3 &m) { + out.write(reinterpret_cast(&m[0][0]), sizeof(float) * 9); + } + + inline bool read_binary(std::istream &in, glm::vec2 &v) { + in.read(reinterpret_cast(&v.x), sizeof(float) * 2); + return in.good(); + } + + inline bool read_binary(std::istream &in, glm::vec3 &v) { + in.read(reinterpret_cast(&v.x), sizeof(float) * 3); + return in.good(); + } + + inline bool read_binary(std::istream &in, glm::vec4 &v) { + in.read(reinterpret_cast(&v.x), sizeof(float) * 4); + return in.good(); + } + + inline bool read_binary(std::istream &in, glm::quat &q) { + in.read(reinterpret_cast(&q.x), sizeof(float) * 4); + return in.good(); + } + + inline bool read_binary(std::istream &in, glm::mat4 &m) { + in.read(reinterpret_cast(&m[0][0]), sizeof(float) * 16); + return in.good(); + } + + inline bool read_binary(std::istream &in, glm::mat3 &m) { + in.read(reinterpret_cast(&m[0][0]), sizeof(float) * 9); + return in.good(); + } + + /// Vertex I/O + inline void write_binary(std::ostream& out, const Vertex& v) { + write_binary(out, v.position); + write_binary(out, v.normal); + write_binary(out, v.texCoords); + write_binary(out, v.color); + write_binary(out, v.tangent); + write_binary(out, v.bitangent); + } + + inline bool read_binary(std::istream& in, Vertex& v) { + if (!read_binary(in, v.position)) return false; + if (!read_binary(in, v.normal)) return false; + if (!read_binary(in, v.texCoords)) return false; + if (!read_binary(in, v.color)) return false; + if (!read_binary(in, v.tangent)) return false; + if (!read_binary(in, v.bitangent)) return false; + return true; + } + + // Vector of vertices (can't use the template version) + inline void write_vertex_vector(std::ostream& out, const std::vector& vec) { + const uint32_t size = static_cast(vec.size()); + write_binary(out, size); + for (const auto& v : vec) { + write_binary(out, v); + } + } + + inline bool read_vertex_vector(std::istream& in, std::vector& vec) { + uint32_t size; + if (!read_binary(in, size)) return false; + vec.resize(size); + for (auto& v : vec) { + if (!read_binary(in, v)) return false; + } + return true; + } + + /// JSON Helpers + inline std::optional json_get_vec2(const nlohmann::json &j, const std::string &key) { + if (!j.contains(key) || !j[key].is_array() || j[key].size() < 2) { + return std::nullopt; + } + return glm::vec2(j[key][0].get(), j[key][1].get()); + } + + inline std::optional json_get_vec3(const nlohmann::json &j, const std::string &key) { + if (!j.contains(key) || !j[key].is_array() || j[key].size() < 3) { + return std::nullopt; + } + return glm::vec3( + j[key][0].get(), + j[key][1].get(), + j[key][2].get() + ); + } - inline std::optional json_to_vec3(const nlohmann::json& j) { + inline std::optional json_to_vec3(const nlohmann::json &j) { if (!j.is_array() || j.size() != 3) return std::nullopt; - - for (const auto& elem : j) { + + for (const auto &elem: j) { if (!elem.is_number()) return std::nullopt; } - + return glm::vec3(j[0].get(), j[1].get(), j[2].get()); } - inline std::optional json_get_vec3(const nlohmann::json& j, const std::string& key) { - if (!j.contains(key)) return std::nullopt; - return json_to_vec3(j.at(key)); + inline std::optional json_get_vec4(const nlohmann::json &j, const std::string &key) { + if (!j.contains(key) || !j[key].is_array() || j[key].size() < 4) { + return std::nullopt; + } + return glm::vec4( + j[key][0].get(), + j[key][1].get(), + j[key][2].get(), + j[key][3].get() + ); + } + + inline nlohmann::json vec2_to_json(const glm::vec2 &v) { + return {v.x, v.y}; } + inline nlohmann::json vec3_to_json(const glm::vec3 &v) { + return {v.x, v.y, v.z}; + } + + inline nlohmann::json vec4_to_json(const glm::vec4 &v) { + return {v.x, v.y, v.z, v.w}; + } + + + /// File Header Validation + struct FileHeader { + uint32_t magic; + uint32_t version; + }; + + inline bool write_header(std::ostream &out, uint32_t magic, uint32_t version) { + write_binary(out, magic); + write_binary(out, version); + return out.good(); + } + + inline bool read_and_validate_header(std::istream &in, uint32_t expected_magic, uint32_t max_version, + uint32_t &out_version) { + uint32_t magic; + if (!read_binary(in, magic) || magic != expected_magic) { + return false; + } + + if (!read_binary(in, out_version) || out_version > max_version) { + return false; + } + + return true; + } } // namespace hellfire -// GLM Helper extensions namespace glm { - inline void to_json(nlohmann::json& j, const vec3& v) { + /// GLM Helper extensions + inline void to_json(nlohmann::json &j, const vec3 &v) { j = {v.x, v.y, v.z}; } - - inline void from_json(const nlohmann::json& j, vec3& v) { + + inline void from_json(const nlohmann::json &j, vec3 &v) { v = {j[0].get(), j[1].get(), j[2].get()}; } } // namespace glm From 6d38a6f9a38262eb5203d65f414f654bb9509e76 Mon Sep 17 00:00:00 2001 From: OriginalDCAM Date: Tue, 9 Dec 2025 09:02:22 +0100 Subject: [PATCH 21/27] - Const correctness fix --- editor/src/project/ProjectManager.cpp | 4 ++-- editor/src/project/ProjectManager.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/editor/src/project/ProjectManager.cpp b/editor/src/project/ProjectManager.cpp index 745c9a6..14b5b36 100644 --- a/editor/src/project/ProjectManager.cpp +++ b/editor/src/project/ProjectManager.cpp @@ -46,11 +46,11 @@ namespace hellfire::editor { } emit_progress("[INFO] Created Scenes/SampleScene.hfscene", 0.8f); emit_progress("[INFO] Initializing editor...", 0.9f); + // Switch to main thread for final setup - // add_to_recent(name, current_project_ ); - context_.queue_main_thread([this, p = std::move(project)]() mutable { current_project_ = std::move(p); + current_project_->initialize_managers(); add_to_recent(current_project_->get_name(), current_project_->get_project_root() / "project.hfproj"); emit_progress("[INFO] Done!", 1.0f); event_bus_.dispatch(); diff --git a/editor/src/project/ProjectManager.h b/editor/src/project/ProjectManager.h index af16a56..4188036 100644 --- a/editor/src/project/ProjectManager.h +++ b/editor/src/project/ProjectManager.h @@ -38,8 +38,8 @@ namespace hellfire::editor { explicit ProjectManager(EventBus &event_bus, EditorContext &context); // Current project - Project *get_current_project() { return current_project_.get(); } - bool has_project() { return current_project_ != nullptr; } + Project *get_current_project() const { return current_project_.get(); } + bool has_project() const { return current_project_ != nullptr; } // Project create/open/close void create_project_async(const std::string &name, From 41e0cbb8be318b36081fed815c2dba5703e96cc5 Mon Sep 17 00:00:00 2001 From: OriginalDCAM Date: Tue, 9 Dec 2025 12:01:04 +0100 Subject: [PATCH 22/27] - Meshes can now be saved in the json file of the scene and are serialized correctly. - Fixed a bug with the scenemanger where it wouldn't register scenes as assets internally --- editor/src/project/ProjectManager.cpp | 2 +- editor/src/project/ProjectManager.h | 8 +- editor/src/scenes/DefaultScene.h | 4 +- .../ui/Panels/MenuBar/MenuBarComponent.cpp | 2 + engine/src/hellfire-core.h | 1 - engine/src/hellfire/EntryPoint.h | 3 +- .../src/hellfire/assets/SceneAssetResolver.h | 47 ++++ .../assets/importers/AssetImportManager.cpp | 203 ++++++++++++++++++ .../assets/importers/AssetImportManager.h | 44 ++++ .../hellfire/assets/models/ModelImporter.cpp | 10 +- .../hellfire/assets/models/ModelImporter.h | 1 + engine/src/hellfire/core/Project.cpp | 22 +- engine/src/hellfire/core/Project.h | 3 + .../hellfire/ecs/ComponentRegistration.cpp | 2 + engine/src/hellfire/ecs/RenderableComponent.h | 23 +- .../hellfire/ecs/components/MeshComponent.h | 8 + engine/src/hellfire/graphics/Mesh.cpp | 4 + engine/src/hellfire/graphics/Mesh.h | 2 + .../hellfire/graphics/renderer/graphics_api.h | 4 + engine/src/hellfire/scene/Scene.h | 3 + engine/src/hellfire/scene/SceneManager.cpp | 58 ++++- engine/src/hellfire/scene/SceneManager.h | 7 +- .../hellfire/serializers/MeshSerializer.cpp | 1 + engine/src/hellfire/serializers/Serializer.h | 84 +++++++- 24 files changed, 505 insertions(+), 41 deletions(-) create mode 100644 engine/src/hellfire/assets/SceneAssetResolver.h create mode 100644 engine/src/hellfire/assets/importers/AssetImportManager.cpp create mode 100644 engine/src/hellfire/assets/importers/AssetImportManager.h diff --git a/editor/src/project/ProjectManager.cpp b/editor/src/project/ProjectManager.cpp index 14b5b36..f59691c 100644 --- a/editor/src/project/ProjectManager.cpp +++ b/editor/src/project/ProjectManager.cpp @@ -69,7 +69,7 @@ namespace hellfire::editor { emit_progress("[ERROR] Failed to load project", 1.0f); return; } - + emit_progress("[INFO] Done!", 1.0f); context_.queue_main_thread([this, p = std::move(project), project_file]() mutable { diff --git a/editor/src/project/ProjectManager.h b/editor/src/project/ProjectManager.h index 4188036..2faab37 100644 --- a/editor/src/project/ProjectManager.h +++ b/editor/src/project/ProjectManager.h @@ -38,8 +38,8 @@ namespace hellfire::editor { explicit ProjectManager(EventBus &event_bus, EditorContext &context); // Current project - Project *get_current_project() const { return current_project_.get(); } - bool has_project() const { return current_project_ != nullptr; } + [[nodiscard]] Project *get_current_project() const { return current_project_.get(); } + [[nodiscard]] bool has_project() const { return current_project_ != nullptr; } // Project create/open/close void create_project_async(const std::string &name, @@ -51,7 +51,7 @@ namespace hellfire::editor { void close_project(); // Recent projects - std::vector get_recent_projects() const; + [[nodiscard]] std::vector get_recent_projects() const; void clear_recent_projects(); void remove_from_recent(const std::filesystem::path& path); std::filesystem::path get_recent_projects_path() const { @@ -59,7 +59,7 @@ namespace hellfire::editor { } // Templates - std::vector get_templates() const; + [[nodiscard]] std::vector get_templates() const; private: void add_to_recent(const std::string& name, const std::filesystem::path& path); diff --git a/editor/src/scenes/DefaultScene.h b/editor/src/scenes/DefaultScene.h index d8fa072..1e7e6bc 100644 --- a/editor/src/scenes/DefaultScene.h +++ b/editor/src/scenes/DefaultScene.h @@ -46,8 +46,8 @@ inline void create_default_scene() { const auto new_scene = sm->create_scene(scene_name); setup_default_scene_with_default_entities(new_scene); - - sm->save_scene(save_path, new_scene); + + sm->save_scene_as(save_path, new_scene); sm->set_active_scene(new_scene); } } diff --git a/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp b/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp index 88c4bf8..821b820 100644 --- a/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp +++ b/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp @@ -72,6 +72,8 @@ namespace hellfire::editor { void MenuBarComponent::render_scene_list() const { const auto sm = ServiceLocator::get_service(); + if (!sm) return; // nullcheck to make sure it isn't initialized + for (const auto element: sm->get_scenes()) { const bool is_selected = element == sm->get_active_scene(); if (ImGui::MenuItem(element->get_name().c_str(), nullptr, is_selected)) { diff --git a/engine/src/hellfire-core.h b/engine/src/hellfire-core.h index ab4f690..5df988b 100644 --- a/engine/src/hellfire-core.h +++ b/engine/src/hellfire-core.h @@ -10,7 +10,6 @@ #include "hellfire/core/Application.h" #include "hellfire/scene/Scene.h" #include "hellfire/graphics/Mesh.h" -#include "hellfire/assets/AnimationSystem.h" #include "hellfire/graphics/Skybox.h" #include "hellfire/assets/Asset.h" #include "hellfire/scene/CameraFactory.h" diff --git a/engine/src/hellfire/EntryPoint.h b/engine/src/hellfire/EntryPoint.h index 8f68a5b..d1041dc 100644 --- a/engine/src/hellfire/EntryPoint.h +++ b/engine/src/hellfire/EntryPoint.h @@ -49,11 +49,12 @@ extern std::unique_ptr create_application_config() int main() { const auto config = create_application_config(); - + hellfire::Application app(config->get_window_width(), config->get_window_height(), config->get_title()); + config->register_plugins(app); app.initialize(); diff --git a/engine/src/hellfire/assets/SceneAssetResolver.h b/engine/src/hellfire/assets/SceneAssetResolver.h new file mode 100644 index 0000000..2e44897 --- /dev/null +++ b/engine/src/hellfire/assets/SceneAssetResolver.h @@ -0,0 +1,47 @@ +// +// Created by denzel on 09/12/2025. +// + +#pragma once + +#include "hellfire/assets/AssetManager.h" +#include "hellfire/scene/Scene.h" +#include "hellfire/ecs/components/MeshComponent.h" +#include "hellfire/ecs/RenderableComponent.h" + + namespace hellfire { + + class SceneAssetResolver { + public: + explicit SceneAssetResolver(AssetManager& assets) : assets_(assets) {} + + void resolve(Scene& scene) { + for (const auto id: scene.get_all_entities() | std::views::keys) { + Entity* entity = scene.get_entity(id); + if (!entity) continue; + + resolve_entity(*entity); + } + } + + private: + void resolve_entity(Entity& entity) { + if (auto* mesh = entity.get_component()) { + AssetID id = mesh->get_mesh_asset(); + if (id != INVALID_ASSET_ID) { + mesh->set_mesh(assets_.get_mesh(id)); + } + } + + if (auto* renderable = entity.get_component()) { + AssetID id = renderable->get_material_asset(); + if (id != INVALID_ASSET_ID) { + renderable->set_material(assets_.get_material(id)); + } + } + } + + AssetManager& assets_; + }; + + } diff --git a/engine/src/hellfire/assets/importers/AssetImportManager.cpp b/engine/src/hellfire/assets/importers/AssetImportManager.cpp new file mode 100644 index 0000000..d3996aa --- /dev/null +++ b/engine/src/hellfire/assets/importers/AssetImportManager.cpp @@ -0,0 +1,203 @@ +// +// Created by denzel on 09/12/2025. +// + +#include "AssetImportManager.h" + +#include "hellfire/assets/models/ModelImporter.h" +#include "hellfire/serializers/ModelSerializer.h" +#include "hellfire/serializers/TextureSerializer.h" + +namespace hellfire { + TextureType infer_texture_type(const std::string& name) { + std::string lower_name = name; + std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower); + + if (lower_name.find("normal") != std::string::npos || + lower_name.find("nrm") != std::string::npos || + lower_name.find("_n.") != std::string::npos) { + return TextureType::NORMAL; + } + if (lower_name.find("rough") != std::string::npos) { + return TextureType::ROUGHNESS; + } + if (lower_name.find("metal") != std::string::npos) { + return TextureType::METALNESS; + } + if (lower_name.find("ao") != std::string::npos || + lower_name.find("occlusion") != std::string::npos) { + return TextureType::AMBIENT_OCCLUSION; + } + if (lower_name.find("emissive") != std::string::npos || + lower_name.find("emission") != std::string::npos) { + return TextureType::EMISSIVE; + } + if (lower_name.find("spec") != std::string::npos) { + return TextureType::SPECULAR; + } + + return TextureType::DIFFUSE; // Default + } + + AssetImportManager::AssetImportManager(AssetRegistry ®istry, AssetManager &asset_manager, + const std::filesystem::path &project_root) : registry_(registry), + asset_manager_(asset_manager), project_root_(project_root), + import_output_dir_(project_root / "assets" / "imported") { + create_directory(import_output_dir_); + } + + void AssetImportManager::import_all_pending() { + std::cout << "=== Scanning for assets to import ===" << std::endl; + + import_all_models(); + import_all_textures(); + + std::cout << "=== Import complete ===" << std::endl; + } + + void AssetImportManager::import_all_models() { + auto models = registry_.get_assets_by_type(AssetType::MODEL); + + for (const auto &meta: models) { + // Skip already-imported internal formats + std::string ext = meta.filepath.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), tolower); + + if (ext == ".hfmodel" || ext == ".hfmesh") { + continue; // Already internal format + } + + if (needs_import(meta.uuid)) { + std::cout << "Importing model: " << meta.name << std::endl; + import_model(meta); + } else { + std::cout << "Skipping (already imported): " << meta.name << std::endl; + } + } + } + + void AssetImportManager::import_all_textures() { + auto textures = registry_.get_assets_by_type(AssetType::TEXTURE); + + for (const auto &meta: textures) { + // For textures, we just create metadata files if they don't exist + auto meta_path = registry_.get_absolute_path(meta.uuid).string() + ".meta"; + + if (!std::filesystem::exists(meta_path)) { + std::cout << "Creating texture metadata: " << meta.name << std::endl; + + // Infer texture type from filename + TextureMetadata tex_meta; + tex_meta.type = infer_texture_type(meta.name); + tex_meta.generate_mipmaps = true; + tex_meta.srgb = (tex_meta.type == TextureType::DIFFUSE || + tex_meta.type == TextureType::EMISSIVE); + + TextureSerializer::save_metadata( + registry_.get_absolute_path(meta.uuid), + tex_meta + ); + } + } + } + + bool AssetImportManager::import_asset(AssetID id) { + auto meta = registry_.get_asset(id); + if (!meta) return false; + + switch (meta->type) { + case AssetType::MODEL: + return import_model(*meta); + case AssetType::TEXTURE: + return import_texture(*meta); + default: + return false; + } + } + + bool AssetImportManager::needs_import(AssetID id) const { + auto meta = registry_.get_asset(id); + if (!meta) return false; + + // Check if .hfmodel file exists for this source + auto imported_path = get_imported_path(*meta, ".hfmodel"); + if (!exists(imported_path)) { + return true; + } + + // Check if source is newer than imported + auto source_path = registry_.get_absolute_path(id); + auto source_time = last_write_time(source_path); + auto imported_time = last_write_time(imported_path); + + return source_time > imported_time; + } + + bool AssetImportManager::import_model(const AssetMetadata &meta) { + auto source_path = project_root_ / meta.filepath; + + if (!std::filesystem::exists(source_path)) { + std::cerr << "Source file not found: " << source_path << std::endl; + return false; + } + + // Create output directory for this model's assets + auto model_output_dir = import_output_dir_ / meta.name; + std::filesystem::create_directories(model_output_dir); + + // Import using ModelImporter + ModelImporter importer(registry_, model_output_dir); + ImportSettings settings; + settings.generate_normals = true; + settings.generate_tangents = true; + settings.optimize_meshes = true; + + ImportResult result = importer.import(source_path, settings); + + if (!result.success) { + std::cerr << "Failed to import model: " << meta.name + << " - " << result.error_message << std::endl; + return false; + } + + // Save the ImportResult as .hfmodel + auto model_path = model_output_dir / (meta.name + ".hfmodel"); + if (!ModelSerializer::save(model_path, result)) { + std::cerr << "Failed to save .hfmodel: " << model_path << std::endl; + return false; + } + + // Register the new .hfmodel in the registry + registry_.register_asset(model_path, AssetType::MODEL); + + std::cout << " Created: " << model_path.filename() << std::endl; + std::cout << " Meshes: " << result.created_mesh_assets.size() << std::endl; + std::cout << " Materials: " << result.created_material_assets.size() << std::endl; + std::cout << " Textures: " << result.created_texture_assets.size() << std::endl; + + return true; + } + + bool AssetImportManager::import_texture(const AssetMetadata &meta) { + // Textures don't need conversion, just metadata + auto source_path = registry_.get_absolute_path(meta.uuid); + + TextureMetadata tex_meta; + tex_meta.type = infer_texture_type(meta.name); + tex_meta.generate_mipmaps = true; + tex_meta.srgb = (tex_meta.type == TextureType::DIFFUSE); + + return TextureSerializer::save_metadata(source_path, tex_meta); + } + + bool AssetImportManager::has_imported_mesh(AssetID original_id) const { + return asset_manager_.get_mesh(original_id) != nullptr; + } + + std::filesystem::path AssetImportManager::get_imported_path(const AssetMetadata &meta, + const std::string &extension) const { + return import_output_dir_ / meta.name / (meta.name + extension); + } + + +} // hellfire diff --git a/engine/src/hellfire/assets/importers/AssetImportManager.h b/engine/src/hellfire/assets/importers/AssetImportManager.h new file mode 100644 index 0000000..944fb7b --- /dev/null +++ b/engine/src/hellfire/assets/importers/AssetImportManager.h @@ -0,0 +1,44 @@ +// +// Created by denzel on 09/12/2025. +// + +#pragma once +#include "hellfire/assets/AssetManager.h" +#include "hellfire/assets/AssetRegistry.h" + +namespace hellfire { +class AssetImportManager { + public: + AssetImportManager(AssetRegistry& registry, AssetManager& asset_manager, const std::filesystem::path& project_root); + + /** + * @brief Import all unprocessed assets in registry + */ + void import_all_pending(); + + void import_all_models(); + void import_all_textures(); + bool import_asset(AssetID id); + + // Check if asset needs import (no corresponding .hfmesh/.hfmat exists) + bool needs_import(AssetID id) const; + + // Get import output directory + std::filesystem::path get_import_output_dir() const { return import_output_dir_; }; +private: + AssetRegistry& registry_; + AssetManager& asset_manager_; + std::filesystem::path project_root_; + std::filesystem::path import_output_dir_; + + bool import_model(const AssetMetadata& meta); + bool import_texture(const AssetMetadata& meta); + + // Check if imported version exists + bool has_imported_mesh(AssetID original_id) const; + std::filesystem::path get_imported_path(const AssetMetadata& meta, + const std::string& extension) const; +}; + +} // hellfire + diff --git a/engine/src/hellfire/assets/models/ModelImporter.cpp b/engine/src/hellfire/assets/models/ModelImporter.cpp index 0bcc6f8..39d7a08 100644 --- a/engine/src/hellfire/assets/models/ModelImporter.cpp +++ b/engine/src/hellfire/assets/models/ModelImporter.cpp @@ -23,7 +23,7 @@ namespace hellfire { ModelImporter::ModelImporter(AssetRegistry ®istry, const std::filesystem::path &output_dir) : registry_(registry), output_dir_(output_dir) { - std::filesystem::create_directories(output_dir_); + create_directories(output_dir_); } ImportResult ModelImporter::import(const std::filesystem::path &source_path, const ImportSettings &settings) { @@ -126,8 +126,8 @@ namespace hellfire { glm::vec4 perspective; glm::quat rotation; - glm::decompose(transform, result.scale, rotation, result.position, skew, perspective); - result.rotation = glm::eulerAngles(rotation); + decompose(transform, result.scale, rotation, result.position, skew, perspective); + result.rotation = eulerAngles(rotation); return result; } @@ -315,8 +315,8 @@ namespace hellfire { }; for (const auto &path: search_paths) { - if (std::filesystem::exists(path)) { - return std::filesystem::canonical(path); + if (exists(path)) { + return canonical(path); } } diff --git a/engine/src/hellfire/assets/models/ModelImporter.h b/engine/src/hellfire/assets/models/ModelImporter.h index 1db400f..0db147c 100644 --- a/engine/src/hellfire/assets/models/ModelImporter.h +++ b/engine/src/hellfire/assets/models/ModelImporter.h @@ -26,6 +26,7 @@ namespace hellfire { * this runs once during asset import, NOT at runtime! */ class ModelImporter { + public: ModelImporter(AssetRegistry& registry, const std::filesystem::path& output_dir); ImportResult import(const std::filesystem::path& source_path, const ImportSettings& settings = {}); diff --git a/engine/src/hellfire/core/Project.cpp b/engine/src/hellfire/core/Project.cpp index b564e16..4fa8c99 100644 --- a/engine/src/hellfire/core/Project.cpp +++ b/engine/src/hellfire/core/Project.cpp @@ -10,6 +10,8 @@ #include "imgui_internal.h" #include "json.hpp" +#include "hellfire/assets/AssetManager.h" +#include "hellfire/assets/importers/AssetImportManager.h" #include "hellfire/scene/Scene.h" #include "hellfire/serializers/ProjectSerializer.h" #include "hellfire/utilities/ServiceLocator.h" @@ -143,12 +145,12 @@ namespace hellfire { } void Project::create_directory_structure() const { - std::filesystem::create_directories(project_root_path_ / "settings"); - std::filesystem::create_directories(project_root_path_ / "assets"); - std::filesystem::create_directories(project_root_path_ / "assets" / "scenes"); - std::filesystem::create_directories(project_root_path_ / "assets" / "textures"); - std::filesystem::create_directories(project_root_path_ / "assets" / "models"); - std::filesystem::create_directories(project_root_path_ / "assets" / "scripts"); + create_directories(project_root_path_ / "settings"); + create_directories(project_root_path_ / "assets"); + create_directories(project_root_path_ / "assets" / "scenes"); + create_directories(project_root_path_ / "assets" / "textures"); + create_directories(project_root_path_ / "assets" / "models"); + create_directories(project_root_path_ / "assets" / "scripts"); } void Project::initialize_default_assets() { @@ -165,6 +167,13 @@ namespace hellfire { ServiceLocator::register_service(asset_registry_.get()); asset_registry_->register_directory(get_assets_path(), true); + asset_manager_ = std::make_unique(*asset_registry_.get()); + ServiceLocator::register_service(asset_manager_.get()); + + AssetImportManager import_manager(*asset_registry_, *asset_manager_, project_root_path_); + import_manager.import_all_pending(); + asset_registry_->save(); + scene_manager_ = std::make_unique(); ServiceLocator::register_service(scene_manager_.get()); @@ -181,6 +190,7 @@ namespace hellfire { void Project::cleanup_managers() { ServiceLocator::unregister_service(); ServiceLocator::unregister_service(); + ServiceLocator::unregister_service(); scene_manager_.reset(); asset_registry_.reset(); } diff --git a/engine/src/hellfire/core/Project.h b/engine/src/hellfire/core/Project.h index 508e743..20d893d 100644 --- a/engine/src/hellfire/core/Project.h +++ b/engine/src/hellfire/core/Project.h @@ -6,6 +6,7 @@ #include #include +#include "hellfire/assets/AssetManager.h" #include "hellfire/assets/AssetRegistry.h" #include "hellfire/scene/SceneManager.h" @@ -57,6 +58,8 @@ namespace hellfire { std::unique_ptr scene_manager_ = nullptr; std::unique_ptr asset_registry_ = nullptr; + std::unique_ptr asset_manager_ = nullptr; + std::vector recent_scenes_; private: diff --git a/engine/src/hellfire/ecs/ComponentRegistration.cpp b/engine/src/hellfire/ecs/ComponentRegistration.cpp index f612aeb..99c8900 100644 --- a/engine/src/hellfire/ecs/ComponentRegistration.cpp +++ b/engine/src/hellfire/ecs/ComponentRegistration.cpp @@ -11,5 +11,7 @@ namespace hellfire { void register_all_components() { auto ® = ComponentRegistry::instance(); reg.register_component("TransformComponent"); + reg.register_component("MeshComponent"); + reg.register_component("RenderableComponent"); } } diff --git a/engine/src/hellfire/ecs/RenderableComponent.h b/engine/src/hellfire/ecs/RenderableComponent.h index b4e41e9..8e65151 100644 --- a/engine/src/hellfire/ecs/RenderableComponent.h +++ b/engine/src/hellfire/ecs/RenderableComponent.h @@ -2,6 +2,7 @@ #include #include "Component.h" +#include "hellfire/assets/AssetRegistry.h" #include "hellfire/graphics/managers/MaterialManager.h" namespace hellfire { @@ -12,17 +13,27 @@ namespace hellfire { // Material management void set_material(const std::shared_ptr& material) { material_ = material; } + void set_material_asset(AssetID id) { material_asset_id_ = id; } + [[nodiscard]] std::shared_ptr get_material() const { return material_; } + AssetID get_material_asset() const { return material_asset_id_; } [[nodiscard]] bool has_material() const { return material_ != nullptr; } // Rendering settings - void set_cast_shadows(bool cast) { cast_shadows_ = cast; } - void set_receive_shadows(bool receive) { receive_shadows_ = receive; } - [[nodiscard]] bool get_cast_shadows() const { return cast_shadows_; } - [[nodiscard]] bool get_receive_shadows() const { return receive_shadows_; } + void set_cast_shadows(bool cast) { cast_shadows = cast; } + void set_receive_shadows(bool receive) { receive_shadows = receive; } + [[nodiscard]] bool get_cast_shadows() const { return cast_shadows; } + [[nodiscard]] bool get_receive_shadows() const { return receive_shadows; } + + // Render settings + bool cast_shadows = true; + bool receive_shadows = true; + bool visible = true; + uint32_t render_layer = 0; private: std::shared_ptr material_; - bool cast_shadows_ = true; - bool receive_shadows_ = true; + AssetID material_asset_id_ = INVALID_ASSET_ID; + + }; } diff --git a/engine/src/hellfire/ecs/components/MeshComponent.h b/engine/src/hellfire/ecs/components/MeshComponent.h index 30f5cf9..dde23fd 100644 --- a/engine/src/hellfire/ecs/components/MeshComponent.h +++ b/engine/src/hellfire/ecs/components/MeshComponent.h @@ -5,6 +5,7 @@ #pragma once #include "hellfire/ecs/Component.h" #include "hellfire/graphics/Mesh.h" +#include "hellfire/assets/AssetRegistry.h" namespace hellfire { enum class MeshSource { @@ -28,14 +29,21 @@ namespace hellfire { [[nodiscard]] std::shared_ptr get_mesh() const { return mesh_; } [[nodiscard]] bool has_mesh() const { return mesh_ != nullptr; } + void set_mesh_asset(AssetID id) { mesh_asset_id_ = id; } + + AssetID get_mesh_asset() const { return mesh_asset_id_; } + void set_source(const MeshSource source, MeshInternalType type = MeshInternalType::NONE) { source_ = source; internal_type = type; } MeshSource get_source() const { return source_;} + + bool is_wireframe = false; private: std::shared_ptr mesh_; MeshSource source_ = MeshSource::EXTERNAL; MeshInternalType internal_type = MeshInternalType::NONE; + AssetID mesh_asset_id_ = INVALID_ASSET_ID; }; } diff --git a/engine/src/hellfire/graphics/Mesh.cpp b/engine/src/hellfire/graphics/Mesh.cpp index a115b07..68dbbb4 100644 --- a/engine/src/hellfire/graphics/Mesh.cpp +++ b/engine/src/hellfire/graphics/Mesh.cpp @@ -21,6 +21,10 @@ namespace hellfire { vao_->unbind(); } + void Mesh::build() { + create_mesh(); + } + void Mesh::create_mesh() { vao_ = std::make_unique(); vbo_ = std::make_unique(); diff --git a/engine/src/hellfire/graphics/Mesh.h b/engine/src/hellfire/graphics/Mesh.h index 1e03183..97fd619 100644 --- a/engine/src/hellfire/graphics/Mesh.h +++ b/engine/src/hellfire/graphics/Mesh.h @@ -17,6 +17,8 @@ namespace hellfire { void unbind() const; + void build(); + // mesh data std::vector vertices; std::vector indices; diff --git a/engine/src/hellfire/graphics/renderer/graphics_api.h b/engine/src/hellfire/graphics/renderer/graphics_api.h index 38f769b..c8b4275 100644 --- a/engine/src/hellfire/graphics/renderer/graphics_api.h +++ b/engine/src/hellfire/graphics/renderer/graphics_api.h @@ -36,6 +36,10 @@ namespace hellfire { bool wireframe = false; }; + struct ShadowSettings { + + }; + /// Abstract graphics context class IGraphicsContext { public: diff --git a/engine/src/hellfire/scene/Scene.h b/engine/src/hellfire/scene/Scene.h index f54757a..110e40f 100644 --- a/engine/src/hellfire/scene/Scene.h +++ b/engine/src/hellfire/scene/Scene.h @@ -179,6 +179,9 @@ namespace hellfire { void save(); SceneEnvironment* environment() const { return environment_.get(); } + + std::unordered_map>& get_all_entities() { return entities_; } + private: // All entities owned by scene std::unordered_map > entities_; diff --git a/engine/src/hellfire/scene/SceneManager.cpp b/engine/src/hellfire/scene/SceneManager.cpp index 39f1f9f..6b957d4 100644 --- a/engine/src/hellfire/scene/SceneManager.cpp +++ b/engine/src/hellfire/scene/SceneManager.cpp @@ -5,8 +5,10 @@ #include #include +#include "hellfire/assets/SceneAssetResolver.h" #include "hellfire/ecs/ComponentRegistration.h" #include "hellfire/serializers/SceneSerializer.h" +#include "hellfire/utilities/ServiceLocator.h" namespace hellfire { SceneManager::SceneManager() : active_scene_(nullptr) { @@ -22,8 +24,8 @@ namespace hellfire { return scenes_; } - void SceneManager::save_current_scene() const { - save_scene(active_scene_->get_source_filename().string(), active_scene_); + void SceneManager::save_current_scene() { + save_scene_as(active_scene_->get_source_filename().string(), active_scene_); } Scene *SceneManager::create_scene(const std::string &name) { @@ -41,12 +43,14 @@ namespace hellfire { } } + // Open the file for reading std::ifstream file(filename); if (!file.is_open()) { std::cerr << "Failed to open scene file: " << filename << std::endl; return nullptr; } + // Create and empty scene and deserialize components Scene* new_scene = create_scene(); if (!Serializer::deserialize(file, new_scene)) { std::cerr << "Failed to deserialize scene: " << filename << std::endl; @@ -54,6 +58,23 @@ namespace hellfire { return nullptr; } + + // Resolve asset references + if (auto* asset_manager = ServiceLocator::get_service()) { + SceneAssetResolver resolver(*asset_manager); + resolver.resolve(*new_scene); + } else { + std::cerr << "Warning: No AssetManager available, assets not resolved for scene: " + << filename << std::endl; + } + + // Track scene asset ID for hot-reload / saving + if (auto* asset_registry = ServiceLocator::get_service()) { + if (const auto scene_uuid = asset_registry->get_uuid_by_path(filename)) { + scene_asset_ids_[new_scene] = *scene_uuid; + } + } + new_scene->set_source_filename(filename); return new_scene; } @@ -78,25 +99,48 @@ namespace hellfire { return get_scene_asset_id(active_scene_); } - bool SceneManager::save_scene(const std::string &filepath, Scene *scene) const { + bool SceneManager::save_scene(Scene* scene) { + if (!scene) return false; + + std::filesystem::path filepath = scene->get_source_filename(); + if (filepath.empty()) { + std::cerr << "Scene has no source filename, use save_scene_as()" << std::endl; + return false; + } + + return save_scene_as(filepath.string(), scene); + } + + bool SceneManager::save_scene_as(const std::string &filename, Scene *scene) { if (!scene) scene = get_active_scene(); if (!scene) return false; - std::ofstream file(filepath); + std::ofstream file(filename); if (!file.is_open()) { - std::cerr << "Failed to open file for writing: " << filepath << std::endl; + std::cerr << "Failed to open file for writing: " << filename << std::endl; return false; } if (!Serializer::serialize(file, scene)) { - std::cerr << "Failed to serialize scene: " << filepath << std::endl; + std::cerr << "Failed to serialize scene: " << filename << std::endl; return false; } - scene->set_source_filename(filepath); + scene->set_source_filename(filename); + + // Register with asset registry if not already + if (auto* asset_registry = ServiceLocator::get_service()) { + if (!asset_registry->get_uuid_by_path(filename)) { + AssetID new_id = asset_registry->register_asset(filename, AssetType::SCENE); + scene_asset_ids_[scene] = new_id; + } + } + + std::cout << "Scene saved: " << filename << std::endl; return true; } + void SceneManager::update(float delta_time) { if (active_scene_) { active_scene_->update(delta_time); diff --git a/engine/src/hellfire/scene/SceneManager.h b/engine/src/hellfire/scene/SceneManager.h index 79007b4..e1b8eb1 100644 --- a/engine/src/hellfire/scene/SceneManager.h +++ b/engine/src/hellfire/scene/SceneManager.h @@ -26,7 +26,7 @@ namespace hellfire { std::vector get_scenes(); - void save_current_scene() const; + void save_current_scene(); // Setup callback for when a scene is activated using SceneActivatedCallback = std::function; @@ -44,9 +44,12 @@ namespace hellfire { std::optional get_scene_asset_id(Scene* scene) const; std::optional get_active_scene_asset_id() const; + + bool save_scene(Scene *scene); + void set_scene_asset_id(Scene* scene, AssetID id); - bool save_scene(const std::string &filename, Scene *scene) const; + bool save_scene_as(const std::string &filename, Scene *scene) ; // Scene management void update(float delta_time); diff --git a/engine/src/hellfire/serializers/MeshSerializer.cpp b/engine/src/hellfire/serializers/MeshSerializer.cpp index ab607a2..c649535 100644 --- a/engine/src/hellfire/serializers/MeshSerializer.cpp +++ b/engine/src/hellfire/serializers/MeshSerializer.cpp @@ -65,6 +65,7 @@ namespace hellfire { return nullptr; } + mesh->build(); return mesh; } diff --git a/engine/src/hellfire/serializers/Serializer.h b/engine/src/hellfire/serializers/Serializer.h index 66d2911..d43197b 100644 --- a/engine/src/hellfire/serializers/Serializer.h +++ b/engine/src/hellfire/serializers/Serializer.h @@ -5,20 +5,23 @@ #include +#include "hellfire/ecs/RenderableComponent.h" #include "hellfire/ecs/TransformComponent.h" +#include "hellfire/ecs/components/MeshComponent.h" #include "hellfire/scene/Scene.h" #include "hellfire/utilities/SerializerUtils.h" namespace hellfire { template struct Serializer { - static bool serialize(std::ostream& output, const T* obj); - static bool deserialize(std::istream& input, T* obj); + static bool serialize(std::ostream &output, const T *obj); + + static bool deserialize(std::istream &input, T *obj); }; template<> struct Serializer { - static bool serialize(std::ostream& output, const TransformComponent* obj) { + static bool serialize(std::ostream &output, const TransformComponent *obj) { if (obj == nullptr) return false; const nlohmann::json j = { @@ -31,10 +34,10 @@ namespace hellfire { return output.good(); } - static bool deserialize(std::istream& input, TransformComponent* obj) { + static bool deserialize(std::istream &input, TransformComponent *obj) { try { if (obj == nullptr) return false; - + nlohmann::json j; input >> j; @@ -43,11 +46,80 @@ namespace hellfire { const auto scale = json_get_vec3(j, "scale"); if (!position || !rotation || !scale) return false; - + obj->set_position(*position); obj->set_rotation(*rotation); obj->set_scale(*scale); + + return true; + } catch (...) { + return false; + } + } + }; + + template<> + struct Serializer { + static bool serialize(std::ostream &output, const MeshComponent *obj) { + if (!obj) return false; + + nlohmann::json j = { + {"mesh_asset", obj->get_mesh_asset()}, + {"is_wireframe", obj->is_wireframe} + }; + + output << j.dump(4); + return output.good(); + } + + static bool deserialize(std::istream &input, MeshComponent *obj) { + if (!obj) return false; + + try { + nlohmann::json j; + input >> j; + + obj->set_mesh_asset(j.value("mesh_asset", INVALID_ASSET_ID)); + obj->is_wireframe = j.value("is_wireframe", false); + + return true; + } catch (...) { + return false; + } + } + }; + + template<> + struct Serializer { + static bool serialize(std::ostream &output, const RenderableComponent *obj) { + if (!obj) return false; + + nlohmann::json j = { + {"material_asset", obj->get_material_asset()}, + {"cast_shadows", obj->cast_shadows}, + {"receive_shadows", obj->receive_shadows}, + {"visible", obj->visible}, + {"render_layer", obj->render_layer} + }; + + output << j.dump(4); + return output.good(); + } + + static bool deserialize(std::istream &input, RenderableComponent *obj) { + if (!obj) return false; + + try { + nlohmann::json j; + input >> j; + + obj->set_material_asset(j.value("material_asset", INVALID_ASSET_ID)); + obj->cast_shadows = j.value("cast_shadows", true); + obj->receive_shadows = j.value("receive_shadows", true); + obj->visible = j.value("visible", true); + obj->render_layer = j.value("render_layer", 0u); + return true; } catch (...) { return false; From c9bade63af09d1bb345fa082f0ba860bae1bf9bb Mon Sep 17 00:00:00 2001 From: OriginalDCAM Date: Tue, 9 Dec 2025 16:34:13 +0100 Subject: [PATCH 23/27] - Lights can now also be saved. --- editor/src/states/Editor/EditorState.cpp | 2 + editor/src/states/Editor/EditorState.h | 1 - .../ui/Panels/AssetExplorer/AssetExplorer.cpp | 24 +++++++++ .../ui/Panels/AssetExplorer/AssetExplorer.h | 14 ++++++ .../hellfire/ecs/ComponentRegistration.cpp | 1 + engine/src/hellfire/serializers/Serializer.h | 49 ++++++++++++++++++- 6 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 editor/src/ui/Panels/AssetExplorer/AssetExplorer.cpp create mode 100644 editor/src/ui/Panels/AssetExplorer/AssetExplorer.h diff --git a/editor/src/states/Editor/EditorState.cpp b/editor/src/states/Editor/EditorState.cpp index 7664cf9..b7a33bd 100644 --- a/editor/src/states/Editor/EditorState.cpp +++ b/editor/src/states/Editor/EditorState.cpp @@ -7,6 +7,7 @@ #include "hellfire/graphics/renderer/Renderer.h" #include "hellfire/scene/SceneManager.h" #include "scenes/DefaultScene.h" +#include "ui/Panels/AssetExplorer/AssetExplorer.h" #include "ui/Panels/Inspector/InspectorPanel.h" #include "ui/Panels/MenuBar/MenuBarComponent.h" #include "ui/Panels/SceneHierarchy/SceneHierarchyPanel.h" @@ -24,6 +25,7 @@ namespace hellfire::editor { panel_manager_.add_panel(); panel_manager_.add_panel(); panel_manager_.add_panel(); + panel_manager_.add_panel(); viewport_panel_ = panel_manager_.add_panel(); panel_manager_.set_context(context_); diff --git a/editor/src/states/Editor/EditorState.h b/editor/src/states/Editor/EditorState.h index 0162622..f938f71 100644 --- a/editor/src/states/Editor/EditorState.h +++ b/editor/src/states/Editor/EditorState.h @@ -6,7 +6,6 @@ #include "core/ApplicationState.h" #include "ui/PanelManager.h" #include "ui/Panels/MenuBar/MenuBarComponent.h" -#include "ui/Panels/Projects/NewProjectPanel.h" #include "ui/Panels/Viewport/ViewportPanel.h" namespace hellfire::editor { diff --git a/editor/src/ui/Panels/AssetExplorer/AssetExplorer.cpp b/editor/src/ui/Panels/AssetExplorer/AssetExplorer.cpp new file mode 100644 index 0000000..e23d55f --- /dev/null +++ b/editor/src/ui/Panels/AssetExplorer/AssetExplorer.cpp @@ -0,0 +1,24 @@ +// +// Created by denzel on 09/12/2025. +// + +#include "AssetExplorer.h" + +#include "ui/ui.h" + +namespace hellfire::editor { + void AssetExplorer::render() { + if (ui::Window window {"Asset Browser"}) { + if (auto asset_registry = ServiceLocator::get_service()) { + auto assets = asset_registry->get_all_assets(); + + for (auto asset : assets) { + ImGui::Text(asset.name.c_str()); + ImGui::SameLine(); + ImGui::Text("%u", asset.type); + } + } + } + } + +} // hellfire \ No newline at end of file diff --git a/editor/src/ui/Panels/AssetExplorer/AssetExplorer.h b/editor/src/ui/Panels/AssetExplorer/AssetExplorer.h new file mode 100644 index 0000000..2b28c2a --- /dev/null +++ b/editor/src/ui/Panels/AssetExplorer/AssetExplorer.h @@ -0,0 +1,14 @@ +// +// Created by denzel on 09/12/2025. +// +#pragma once + +#include "ui/Panels/EditorPanel.h" + +namespace hellfire::editor { +class AssetExplorer : public EditorPanel { +public: + void render() override; +}; + +} // hellfire \ No newline at end of file diff --git a/engine/src/hellfire/ecs/ComponentRegistration.cpp b/engine/src/hellfire/ecs/ComponentRegistration.cpp index 99c8900..e391add 100644 --- a/engine/src/hellfire/ecs/ComponentRegistration.cpp +++ b/engine/src/hellfire/ecs/ComponentRegistration.cpp @@ -13,5 +13,6 @@ namespace hellfire { reg.register_component("TransformComponent"); reg.register_component("MeshComponent"); reg.register_component("RenderableComponent"); + reg.register_component("LightComponent"); } } diff --git a/engine/src/hellfire/serializers/Serializer.h b/engine/src/hellfire/serializers/Serializer.h index d43197b..b4201f3 100644 --- a/engine/src/hellfire/serializers/Serializer.h +++ b/engine/src/hellfire/serializers/Serializer.h @@ -5,6 +5,7 @@ #include +#include "hellfire/ecs/LightComponent.h" #include "hellfire/ecs/RenderableComponent.h" #include "hellfire/ecs/TransformComponent.h" #include "hellfire/ecs/components/MeshComponent.h" @@ -66,7 +67,7 @@ namespace hellfire { nlohmann::json j = { {"mesh_asset", obj->get_mesh_asset()}, {"is_wireframe", obj->is_wireframe} - + }; output << j.dump(4); @@ -126,4 +127,50 @@ namespace hellfire { } } }; + + template<> + struct Serializer { + static bool serialize(std::ostream &output, const LightComponent *obj) { + if (!obj) return false; + + nlohmann::json j = { + {"light_type", obj->get_light_type()}, + {"color", obj->get_color()}, + {"intensity", obj->get_intensity()}, + {"should_cast_shadows", obj->should_cast_shadows()}, + }; + + if (obj->get_light_type() == LightComponent::POINT) { + j["range"] = obj->get_range(); + j["attenuation"] = obj->get_attenuation(); + } + + output << j.dump(2); + + return output.good(); + } + + static bool deserialize(std::istream &input, LightComponent *obj) { + if (!obj) return false; + + try { + nlohmann::json j; + input >> j; + + obj->set_light_type(j.value("light_type", LightComponent::POINT)); + obj->set_color(j.value("color", glm::vec3(0))); + obj->set_intensity(j.value("intensity", 1.0f)); + obj->set_cast_shadows(j.value("should_cast_shadows", false)); + + if (obj->get_light_type() == LightComponent::POINT) { + obj->set_range(j.value("range", 0.0f)); + obj->set_attenuation(j.value("attenuation", 0.0f)); + } + + return true; + } catch (...) { + return false; + } + } + }; } From c0214e0617d7978c026f4ee0b0ecbb91fdf82314 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Wed, 10 Dec 2025 06:59:36 +0100 Subject: [PATCH 24/27] - W.I.P. integrating test engine into project. --- README.md | 2 +- cmake/AssetSymlink.cmake | 17 +++++++++ editor/CMakeLists.txt | 74 +++++++++++++++------------------------- tests/CMakeLists.txt | 70 ++++++++++++++++++++++++++++++++----- tests/ui/main.cpp | 3 ++ vendor/imgui/imconfig.h | 3 ++ 6 files changed, 113 insertions(+), 56 deletions(-) create mode 100644 cmake/AssetSymlink.cmake create mode 100644 tests/ui/main.cpp diff --git a/README.md b/README.md index 8741253..7ece163 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Hellfire Game Engine README +# Hellfire Game Engine A lightweight, experimental 3D game engine built with modern C++ and OpenGL, featuring an integrated editor and entity-component architecture. diff --git a/cmake/AssetSymlink.cmake b/cmake/AssetSymlink.cmake new file mode 100644 index 0000000..283ec67 --- /dev/null +++ b/cmake/AssetSymlink.cmake @@ -0,0 +1,17 @@ +function(setup_asset_symlink TARGET_NAME ASSETS_SOURCE_DIR) + if (WIN32) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E remove -f "$/assets" + COMMAND cmd /c "mklink /J \"$\\assets\" \"${ASSETS_SOURCE_DIR}\"" + COMMENT "Creating directory junction to assets directory for ${TARGET_NAME}" + ) + else () + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E remove -f "$/assets" + COMMAND ${CMAKE_COMMAND} -E create_symlink + "${ASSETS_SOURCE_DIR}" + "$/assets" + COMMENT "Creating symlink to assets directory for ${TARGET_NAME}" + ) + endif () +endfunction() \ No newline at end of file diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 75bafac..38f2a93 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1,83 +1,63 @@ set(APP_NAME HellfireEditor) +set(LIB_NAME HellfireEditorLib) -add_compile_definitions(HELLFIRE_EDITOR_ENABLED) - -file(GLOB_RECURSE APP_SOURCES "src/*.cpp") -file(GLOB_RECURSE APP_HEADERS "src/*.h" "src/*.hpp") -add_executable(${APP_NAME} ${APP_SOURCES} ${APP_HEADERS}) - - -# Add the imguizmo library set(IMGUIZMO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/imguizmo) add_library(imguizmo STATIC ${IMGUIZMO_DIR}/ImGuizmo.h ${IMGUIZMO_DIR}/ImGuizmo.cpp ) -target_link_libraries(imguizmo - PRIVATE imgui -) +target_link_libraries(imguizmo PRIVATE imgui) target_include_directories(imguizmo PUBLIC ${IMGUIZMO_DIR} - PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/imgui + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/imgui ) -target_include_directories(${APP_NAME} - PRIVATE +file(GLOB_RECURSE LIB_SOURCES "src/*.cpp") +file(GLOB_RECURSE LIB_HEADERS "src/*.h" "src/*.hpp") + +# Remove main.cpp from library sources +list(FILTER LIB_SOURCES EXCLUDE REGEX ".*main\\.cpp$") + +add_library(${LIB_NAME} STATIC ${LIB_SOURCES} ${LIB_HEADERS}) + +target_include_directories(${LIB_NAME} + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ) -target_link_libraries(${APP_NAME} - PRIVATE +target_link_libraries(${LIB_NAME} + PUBLIC HellfireEngine imgui imguizmo glfw ) -if (WIN32) - # Create a directory junction for assets - add_custom_command(TARGET ${APP_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E remove -f "$/assets" - COMMAND cmd /c "mklink /J \"$\\assets\" \"${CMAKE_CURRENT_SOURCE_DIR}\\assets\"" - COMMENT "Creating directory junction to assets directory" - ) -else () - # Unix symlink for assets - add_custom_command(TARGET ${APP_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E remove -f "$/assets" - COMMAND ${CMAKE_COMMAND} -E create_symlink - "${CMAKE_CURRENT_SOURCE_DIR}/assets" - "$/assets" - COMMENT "Creating symlink to assets directory" - ) -endif () +target_compile_definitions(${LIB_NAME} PUBLIC HELLFIRE_EDITOR_ENABLED) -target_compile_definitions(${APP_NAME} PRIVATE HELLFIRE_EDITOR_ENABLED) +add_executable(${APP_NAME} src/main.cpp) +target_link_libraries(${APP_NAME} PRIVATE ${LIB_NAME}) -# Set working directory for debugging in various IDEs +# Asset symlinks +include(${CMAKE_SOURCE_DIR}/cmake/AssetSymlink.cmake) +setup_asset_symlink(${APP_NAME} "${CMAKE_CURRENT_SOURCE_DIR}/assets") + +# IDE Settings set_target_properties(${APP_NAME} PROPERTIES - # Visual Studio VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" - - # Xcode XCODE_SCHEME_WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" - - # For other IDEs that support it WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ) if (MSVC) - # Try additional VS properties for working directory set_target_properties(${APP_NAME} PROPERTIES VS_DEBUGGER_COMMAND_ARGUMENTS "" VS_DEBUGGER_ENVIRONMENT "PATH=%PATH%" ) - # Add source grouping - source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/src" PREFIX "Source Files" FILES ${APP_SOURCES}) - source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/src" PREFIX "Header Files" FILES ${APP_HEADERS}) -endif () - - + # Source grouping for library + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/src" PREFIX "Source Files" FILES ${LIB_SOURCES}) + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/src" PREFIX "Header Files" FILES ${LIB_HEADERS}) +endif () \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 151804d..c88ea4d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,14 +1,68 @@ -set(APP_NAME Tests) -add_compile_definitions(HELLFIRE_TESTING_ENABLED) +# Unit tests +set(UNIT_TEST_NAME UnitTests) +file(GLOB_RECURSE UNIT_TEST_SOURCES "unit/*.cpp") +file(GLOB_RECURSE UNIT_TEST_HEADERS "unit/*.h" "unit/*.hpp") -file(GLOB_RECURSE APP_SOURCES "*.cpp") -file(GLOB_RECURSE APP_HEADERS "*.h" "*.hpp") -add_executable(${APP_NAME} ${APP_SOURCES} ${APP_HEADERS}) - -target_link_libraries(${APP_NAME} +add_executable(${UNIT_TEST_NAME} ${UNIT_TEST_SOURCES} ${UNIT_TEST_HEADERS}) +target_compile_definitions(${UNIT_TEST_NAME} PRIVATE HELLFIRE_TESTING_ENABLED) +target_link_libraries(${UNIT_TEST_NAME} PRIVATE HellfireEngine Catch2::Catch2WithMain ) +catch_discover_tests(${UNIT_TEST_NAME}) + +# UI tests +option(HELLFIRE_UI_TESTS "Build UI tests with ImGui Test Engine" ON) + +if(HELLFIRE_UI_TESTS) + set(UI_TEST_NAME UITests) + + include(FetchContent) + include(${CMAKE_SOURCE_DIR}/cmake/AssetSymlink.cmake) + + # Fetch ImGui Test Engine + FetchContent_Declare( + imgui_test_engine + GIT_REPOSITORY https://github.com/ocornut/imgui_test_engine.git + GIT_TAG v1.92.4 + ) + FetchContent_MakeAvailable(imgui_test_engine) + + set(IMGUI_TEST_ENGINE_DIR "${imgui_test_engine_SOURCE_DIR}/imgui_test_engine") + + file(GLOB_RECURSE UI_TEST_SOURCES "ui/*.cpp") + file(GLOB_RECURSE UI_TEST_HEADERS "ui/*.h" "ui/*.hpp") + + add_executable(${UI_TEST_NAME} ${UI_TEST_SOURCES} ${UI_TEST_HEADERS}) + + target_compile_definitions(${UI_TEST_NAME} PRIVATE HELLFIRE_TESTING_ENABLED) + + target_sources(${UI_TEST_NAME} PRIVATE + ${IMGUI_TEST_ENGINE_DIR}/imgui_te_context.cpp + ${IMGUI_TEST_ENGINE_DIR}/imgui_te_coroutine.cpp + ${IMGUI_TEST_ENGINE_DIR}/imgui_te_engine.cpp + ${IMGUI_TEST_ENGINE_DIR}/imgui_te_exporters.cpp + ${IMGUI_TEST_ENGINE_DIR}/imgui_te_perftool.cpp + ${IMGUI_TEST_ENGINE_DIR}/imgui_te_ui.cpp + ${IMGUI_TEST_ENGINE_DIR}/imgui_te_utils.cpp + ${IMGUI_TEST_ENGINE_DIR}/imgui_capture_tool.cpp + ) + + target_include_directories(${UI_TEST_NAME} PRIVATE ${IMGUI_TEST_ENGINE_DIR}) + + target_include_directories(${UI_TEST_NAME} PRIVATE ${IMGUI_DIR}) + + target_link_libraries(${UI_TEST_NAME} + PRIVATE + HellfireEditorLib # Link the static library + ) + + setup_asset_symlink(${UI_TEST_NAME} "${CMAKE_SOURCE_DIR}/editor/assets") -catch_discover_tests(${APP_NAME}) \ No newline at end of file + add_test( + NAME UITests + COMMAND ${UI_TEST_NAME} --exit-on-completion + WORKING_DIRECTORY $ + ) +endif() \ No newline at end of file diff --git a/tests/ui/main.cpp b/tests/ui/main.cpp new file mode 100644 index 0000000..ff42581 --- /dev/null +++ b/tests/ui/main.cpp @@ -0,0 +1,3 @@ +// +// Created by denzel on 10/12/2025. +// \ No newline at end of file diff --git a/vendor/imgui/imconfig.h b/vendor/imgui/imconfig.h index 4dab1b6..e0a9f0f 100644 --- a/vendor/imgui/imconfig.h +++ b/vendor/imgui/imconfig.h @@ -143,3 +143,6 @@ namespace ImGui void MyFunction(const char* name, MyMatrix44* mtx); } */ + +#define IMGUI_ENABLE_TEST_ENGINE +#define IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL 1 From 671d2daa92e4e559afcd9c5eb21c5c98dc132819 Mon Sep 17 00:00:00 2001 From: OriginalDCAM Date: Wed, 10 Dec 2025 16:53:21 +0100 Subject: [PATCH 25/27] - W.I.P. for home --- .github/workflows/cmake-single-platform.yml | 13 +- editor/src/core/EditorApplication.h | 10 +- .../ProjectCreator/ProjectCreatorState.cpp | 2 +- engine/src/hellfire/core/Application.cpp | 48 ++-- engine/src/hellfire/core/Application.h | 10 + tests/CMakeLists.txt | 129 +++++++++- tests/ui/UITestHarness.cpp | 86 +++++++ tests/ui/UITestHarness.h | 36 +++ tests/ui/main.cpp | 37 ++- tests/ui/tests/ProjectCreatorTests.h | 237 ++++++++++++++++++ vendor/imgui/imconfig.h | 3 - 11 files changed, 563 insertions(+), 48 deletions(-) create mode 100644 tests/ui/UITestHarness.cpp create mode 100644 tests/ui/UITestHarness.h create mode 100644 tests/ui/tests/ProjectCreatorTests.h diff --git a/.github/workflows/cmake-single-platform.yml b/.github/workflows/cmake-single-platform.yml index 66e9b73..c20f919 100644 --- a/.github/workflows/cmake-single-platform.yml +++ b/.github/workflows/cmake-single-platform.yml @@ -1,6 +1,4 @@ -# This starter workflow is for a CMake project running on a single platform. There is a different starter workflow if you need cross-platform coverage. -# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-multi-platform.yml -name: CMake on a single platform +name: Build Project on: push: @@ -9,31 +7,22 @@ on: branches: [ "main" ] env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release jobs: build: - # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. - # You can convert this to a matrix build if you need cross-platform coverage. - # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Configure CMake - # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. - # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} - name: Build - # Build your program with the given configuration run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Test working-directory: ${{github.workspace}}/build - # Execute tests defined by the CMake configuration. - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: ctest -C ${{env.BUILD_TYPE}} diff --git a/editor/src/core/EditorApplication.h b/editor/src/core/EditorApplication.h index b560d13..44d50c3 100644 --- a/editor/src/core/EditorApplication.h +++ b/editor/src/core/EditorApplication.h @@ -11,7 +11,7 @@ #include "../ui/Panels/Inspector/InspectorPanel.h" namespace hellfire::editor { - class EditorApplication final : public IApplicationPlugin { + class EditorApplication : public IApplicationPlugin { public: void on_initialize(Application &app) override; @@ -43,12 +43,14 @@ namespace hellfire::editor { void on_window_focus(bool focused) override; - Entity * get_render_camera_override() override; + Entity *get_render_camera_override() override; - private: + protected: EditorContext editor_context_; + + private: bool imgui_initialized_ = false; - StateManager state_manager_; + StateManager state_manager_; }; } diff --git a/editor/src/states/ProjectCreator/ProjectCreatorState.cpp b/editor/src/states/ProjectCreator/ProjectCreatorState.cpp index 96354ce..3a42907 100644 --- a/editor/src/states/ProjectCreator/ProjectCreatorState.cpp +++ b/editor/src/states/ProjectCreator/ProjectCreatorState.cpp @@ -26,7 +26,7 @@ namespace hellfire::editor { ImGui::SetNextWindowSize(viewport->Size); if (ui::Window window{ - "Hellfire - Project Hub", nullptr, + "Create New Project", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize }) { // Center the form diff --git a/engine/src/hellfire/core/Application.cpp b/engine/src/hellfire/core/Application.cpp index ad20087..9c191c3 100644 --- a/engine/src/hellfire/core/Application.cpp +++ b/engine/src/hellfire/core/Application.cpp @@ -103,26 +103,28 @@ namespace hellfire { } void Application::run() { - while (!window_->should_close()) { - if (window_info_.minimized) { - window_->wait_for_events(); - continue; - } + // while (!should_exit()) { + while (!window_->should_close()) { + if (window_info_.minimized) { + window_->wait_for_events(); + continue; + } - // Poll the window for events (mouse inputs, keys, window stuff, etc.) - window_->poll_events(); - // Make sure the timer is updated - Time::update(); + // Poll the window for events (mouse inputs, keys, window stuff, etc.) + window_->poll_events(); + // Make sure the timer is updated + Time::update(); - input_manager_->update(); + input_manager_->update(); - // Update scene - if (auto sm = ServiceLocator::get_service()) { - sm->update(Time::delta_time); - } + // Update scene + if (auto sm = ServiceLocator::get_service()) { + sm->update(Time::delta_time); + } - on_render(); - } + on_render(); + } + // } } @@ -262,4 +264,18 @@ namespace hellfire { void Application::on_window_minimize(bool minimized) { window_info_.minimized = minimized; } + + void Application::set_exit_condition(std::function condition) { + exit_condition_ = std::move(condition); + } + + void Application::request_exit() { + should_exit_ = true; + } + + bool Application::should_exit() const { + if (should_exit_) return true; + if (exit_condition_ && exit_condition_()) return true; + return false; + } } diff --git a/engine/src/hellfire/core/Application.h b/engine/src/hellfire/core/Application.h index 2e13f2e..7f5da35 100644 --- a/engine/src/hellfire/core/Application.h +++ b/engine/src/hellfire/core/Application.h @@ -62,6 +62,11 @@ namespace hellfire { plugins_.push_back(std::move(plugin)); } + void set_exit_condition(std::function condition); + + void request_exit(); + + bool should_exit() const; protected: // IWindowEventHandler implementation void on_render() override; @@ -82,7 +87,12 @@ namespace hellfire { void on_window_minimize(bool minimized) override; + + private: + std::function exit_condition_; + bool should_exit_ = false; + // Managers std::vector > plugins_; std::unique_ptr window_; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c88ea4d..b083e25 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,15 +30,20 @@ if(HELLFIRE_UI_TESTS) FetchContent_MakeAvailable(imgui_test_engine) set(IMGUI_TEST_ENGINE_DIR "${imgui_test_engine_SOURCE_DIR}/imgui_test_engine") + set(IMGUI_DIR "${CMAKE_SOURCE_DIR}/vendor/imgui") + set(IMGUIZMO_DIR "${CMAKE_SOURCE_DIR}/editor/vendor/imguizmo") - file(GLOB_RECURSE UI_TEST_SOURCES "ui/*.cpp") - file(GLOB_RECURSE UI_TEST_HEADERS "ui/*.h" "ui/*.hpp") - - add_executable(${UI_TEST_NAME} ${UI_TEST_SOURCES} ${UI_TEST_HEADERS}) - - target_compile_definitions(${UI_TEST_NAME} PRIVATE HELLFIRE_TESTING_ENABLED) - - target_sources(${UI_TEST_NAME} PRIVATE + # ============================================ + # ImGui with Test Engine + # ============================================ + add_library(imgui_with_test_engine STATIC + ${IMGUI_DIR}/imgui.cpp + ${IMGUI_DIR}/imgui_demo.cpp + ${IMGUI_DIR}/imgui_draw.cpp + ${IMGUI_DIR}/imgui_tables.cpp + ${IMGUI_DIR}/imgui_widgets.cpp + ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp + ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp ${IMGUI_TEST_ENGINE_DIR}/imgui_te_context.cpp ${IMGUI_TEST_ENGINE_DIR}/imgui_te_coroutine.cpp ${IMGUI_TEST_ENGINE_DIR}/imgui_te_engine.cpp @@ -49,13 +54,115 @@ if(HELLFIRE_UI_TESTS) ${IMGUI_TEST_ENGINE_DIR}/imgui_capture_tool.cpp ) - target_include_directories(${UI_TEST_NAME} PRIVATE ${IMGUI_TEST_ENGINE_DIR}) + target_include_directories(imgui_with_test_engine PUBLIC + ${CMAKE_SOURCE_DIR}/vendor + ${IMGUI_DIR} + ${IMGUI_DIR}/backends + ${IMGUI_TEST_ENGINE_DIR} + ${glew_SOURCE_DIR}/include + ) + + target_compile_definitions(imgui_with_test_engine PUBLIC + IMGUI_ENABLE_TEST_ENGINE + IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL=1 + ) + + target_link_libraries(imgui_with_test_engine PUBLIC + libglew_static + glfw + ${CMAKE_DL_LIBS} + ) + + if (WIN32) + target_link_libraries(imgui_with_test_engine PUBLIC opengl32) + elseif (APPLE) + target_link_libraries(imgui_with_test_engine PUBLIC "-framework OpenGL") + else () + target_link_libraries(imgui_with_test_engine PUBLIC GL) + endif () + + # ============================================ + # ImGuizmo with Test Engine + # ============================================ + add_library(imguizmo_test STATIC + ${IMGUIZMO_DIR}/ImGuizmo.h + ${IMGUIZMO_DIR}/ImGuizmo.cpp + ) + + target_link_libraries(imguizmo_test PRIVATE imgui_with_test_engine) + target_include_directories(imguizmo_test PUBLIC ${IMGUIZMO_DIR}) + + # ============================================ + # Engine Library for Testing (links against test-enabled imgui) + # ============================================ + file(GLOB_RECURSE ENGINE_LIB_SOURCES "${CMAKE_SOURCE_DIR}/engine/src/*.cpp") + file(GLOB_RECURSE ENGINE_LIB_HEADERS "${CMAKE_SOURCE_DIR}/engine/src/*.h" "${CMAKE_SOURCE_DIR}/engine/src/*.hpp") + + add_library(HellfireEngine_Test STATIC ${ENGINE_LIB_SOURCES} ${ENGINE_LIB_HEADERS}) + + target_include_directories(HellfireEngine_Test PUBLIC + ${CMAKE_SOURCE_DIR}/engine/src + ${CMAKE_SOURCE_DIR}/engine/include + ) - target_include_directories(${UI_TEST_NAME} PRIVATE ${IMGUI_DIR}) + find_package(OpenGL REQUIRED) + + # Copy the same link libraries as HellfireEngine, but swap imgui for imgui_with_test_engine + target_link_libraries(HellfireEngine_Test + PUBLIC + libglew_static + glfw + GLM::GLM + OpenGL::GL + imgui_with_test_engine # Use test-enabled imgui + assimp + PRIVATE + stb_image + json + ) + + # ============================================ + # Editor Library for Testing + # ============================================ + file(GLOB_RECURSE EDITOR_LIB_SOURCES "${CMAKE_SOURCE_DIR}/editor/src/*.cpp") + file(GLOB_RECURSE EDITOR_LIB_HEADERS "${CMAKE_SOURCE_DIR}/editor/src/*.h" "${CMAKE_SOURCE_DIR}/editor/src/*.hpp") + list(FILTER EDITOR_LIB_SOURCES EXCLUDE REGEX ".*main\\.cpp$") + + add_library(HellfireEditorLib_Test STATIC ${EDITOR_LIB_SOURCES} ${EDITOR_LIB_HEADERS}) + + target_include_directories(HellfireEditorLib_Test PUBLIC + ${CMAKE_SOURCE_DIR}/editor/src + ) + + target_compile_definitions(HellfireEditorLib_Test PUBLIC + HELLFIRE_EDITOR_ENABLED + HELLFIRE_UI_TESTS_BUILD + ) + + # Link against test-enabled engine and imgui + target_link_libraries(HellfireEditorLib_Test PUBLIC + HellfireEngine_Test # Use test-enabled engine + imgui_with_test_engine + imguizmo_test + glfw + ) + + # ============================================ + # UI Test Executable + # ============================================ + file(GLOB_RECURSE UI_TEST_SOURCES "ui/*.cpp") + file(GLOB_RECURSE UI_TEST_HEADERS "ui/*.h" "ui/*.hpp") + + add_executable(${UI_TEST_NAME} ${UI_TEST_SOURCES} ${UI_TEST_HEADERS}) + + target_compile_definitions(${UI_TEST_NAME} PRIVATE + HELLFIRE_TESTING_ENABLED + HELLFIRE_UI_TESTS_BUILD + ) target_link_libraries(${UI_TEST_NAME} PRIVATE - HellfireEditorLib # Link the static library + HellfireEditorLib_Test ) setup_asset_symlink(${UI_TEST_NAME} "${CMAKE_SOURCE_DIR}/editor/assets") diff --git a/tests/ui/UITestHarness.cpp b/tests/ui/UITestHarness.cpp new file mode 100644 index 0000000..02cfc77 --- /dev/null +++ b/tests/ui/UITestHarness.cpp @@ -0,0 +1,86 @@ +// +// Created by denzel on 10/12/2025. +// + +#include "UITestHarness.h" + +#include +#include + +#include "tests/ProjectCreatorTests.h" + +namespace hellfire::tests { + UITestHarness* UITestHarness::instance = nullptr; + + void UITestHarness::on_initialize(Application &app) { + instance = this; + + EditorApplication::on_initialize(app); + + test_engine_ = ImGuiTestEngine_CreateContext(); + ImGuiTestEngineIO &io = ImGuiTestEngine_GetIO(test_engine_); + + io.ConfigVerboseLevel = ImGuiTestVerboseLevel_Info; + io.ConfigVerboseLevelOnError = ImGuiTestVerboseLevel_Debug; + io.ConfigRunSpeed = ImGuiTestRunSpeed_Fast; + io.ConfigNoThrottle = true; // Run as fast as possible + + ImGuiTestEngine_Start(test_engine_, ImGui::GetCurrentContext()); + + // Register all test suites + RegisterProjectHubTests(test_engine_); + RegisterProjectCreatorTests(test_engine_); + RegisterEditorStateTests(test_engine_); + + // Queue all tests + ImGuiTestEngine_QueueTests(test_engine_, ImGuiTestGroup_Tests); + } + + void UITestHarness::on_render() { + EditorApplication::on_render(); + +#ifndef HELLFIRE_CI_BUILD + ImGuiTestEngine_ShowTestEngineWindows(test_engine_, nullptr); +#endif + + // Check if tests are done + if (!tests_complete_ && test_engine_) { + ImGuiTestEngineIO &io = ImGuiTestEngine_GetIO(test_engine_); + if (!io.IsRunningTests) { + // Tests were queued and have finished running + tests_complete_ = true; + } + } + } + + void UITestHarness::on_end_frame() { + if (test_engine_) { + ImGuiTestEngine_PostSwap(test_engine_); + } + EditorApplication::on_end_frame(); + } + + int UITestHarness::get_exit_code() const { + if (!test_engine_) return 1; + + int tested = 0, success = 0; + ImGuiTestEngine_GetResult(test_engine_, tested, success); + + return (tested > 0 && tested == success) ? 0 : 1; + } + + UITestHarness::~UITestHarness() { + instance = nullptr; + + if (test_engine_) { + ImGuiTestEngine_Stop(test_engine_); + cleanup_imgui(); + ImGuiTestEngine_DestroyContext(test_engine_); + } + } + + + + void RegisterEditorStateTests(ImGuiTestEngine *engine) { + } +} diff --git a/tests/ui/UITestHarness.h b/tests/ui/UITestHarness.h new file mode 100644 index 0000000..8808609 --- /dev/null +++ b/tests/ui/UITestHarness.h @@ -0,0 +1,36 @@ +// +// Created by denzel on 10/12/2025. +// + +#pragma once + +#include "core/EditorApplication.h" +#include "imgui_te_engine.h" + +namespace hellfire::tests { + class UITestHarness : public editor::EditorApplication { + public: + static UITestHarness* instance; + + void on_initialize(Application& app) override; + void on_render() override; + void on_end_frame() override; + + bool is_complete() const { return tests_complete_; } + int get_exit_code() const; + + ImGuiTestEngine* get_test_engine() const { return test_engine_; } + editor::EditorContext* get_editor_context() { return &editor_context_; } + + ~UITestHarness() override; + + private: + ImGuiTestEngine* test_engine_ = nullptr; + bool tests_complete_ = false; + }; + + // Test registration functions + void RegisterEditorStateTests(ImGuiTestEngine* engine); + +} // namespace hellfire::tests + diff --git a/tests/ui/main.cpp b/tests/ui/main.cpp index ff42581..d1498a7 100644 --- a/tests/ui/main.cpp +++ b/tests/ui/main.cpp @@ -1,3 +1,38 @@ // // Created by denzel on 10/12/2025. -// \ No newline at end of file +// +#include "UITestHarness.h" +#include "hellfire/core/Application.h" + + +int main(int argc, char** argv) { + bool exit_on_completion = false; + + for (int i = 1; i < argc; ++i) { + if (std::string(argv[i]) == "--exit-on-completion") { + exit_on_completion = true; + } + } + + hellfire::Application app; + auto test_harness = std::make_unique(); + auto* harness = test_harness.get(); // Get raw pointer before move + app.register_plugin(std::move(test_harness)); + + if (exit_on_completion) { + app.set_exit_condition([harness]() { + return harness->is_complete(); + }); + } + + app.initialize(); + app.run(); + + int result = harness->get_exit_code(); + + std::cout << "\n========================================\n"; + std::cout << "UI Tests " << (result == 0 ? "PASSED" : "FAILED") << "\n"; + std::cout << "========================================\n"; + + return result; +} \ No newline at end of file diff --git a/tests/ui/tests/ProjectCreatorTests.h b/tests/ui/tests/ProjectCreatorTests.h new file mode 100644 index 0000000..29fde0e --- /dev/null +++ b/tests/ui/tests/ProjectCreatorTests.h @@ -0,0 +1,237 @@ +#pragma once + +#include "../UITestHarness.h" +#include "events/StateEvents.h" +#include + +namespace hellfire::tests { + inline ImGuiTest *RegisterTestWithTeardown( + ImGuiTestEngine *engine, + const char *category, + const char *name + ) { + ImGuiTest *t = IM_REGISTER_TEST(engine, category, name); + t->TeardownFunc = [](ImGuiTestContext *ctx) { + auto *harness = UITestHarness::instance; + if (harness) { + harness->get_editor_context()->event_bus.dispatch(); + ctx->Yield(20); + } + }; + return t; + } + + + inline void RegisterProjectHubTests(ImGuiTestEngine *engine) { + ImGuiTest *t = nullptr; + t = RegisterTestWithTeardown(engine, "ProjectHub", "WindowVisible"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(5); // Let the app initialize + ctx->WindowInfo("Project Hub", ImGuiTestOpFlags_None); + }; + + t = RegisterTestWithTeardown(engine, "ProjectHub", "CreateButton_OpensCreator"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + IM_CHECK(ctx->WindowInfo("Hellfire - Project Hub").ID != 0); + ctx->SetRef("Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + IM_CHECK(ctx->WindowInfo("//Create New Project").ID != 0); + }; + } + + inline void RegisterProjectCreatorTests(ImGuiTestEngine *engine) { + ImGuiTest *t = nullptr; + + // ============================================ + // Navigation Tests + // ============================================ + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "WindowOpens_FromProjectHub"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + IM_CHECK(ctx->ItemInfo("//Create New Project").ID != 0); + }; + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "CancelButton_ReturnsToHub"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + ctx->SetRef("//Create New Project"); + ctx->ItemClick("**/Cancel"); + ctx->Yield(10); + + IM_CHECK(ctx->ItemInfo("//Hellfire - Project Hub").ID != 0); + }; + + // ============================================ + // Form Input Tests + // ============================================ + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "FormInputs_AcceptText"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + ctx->SetRef("//Create New Project"); + + ctx->ItemClick("**/##name"); + ctx->KeyCharsAppend("MyTestProject"); + ctx->Yield(5); + + ctx->ItemClick("**/##location"); + ctx->KeyCharsAppend("C:/Projects"); + ctx->Yield(5); + }; + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "CreateButton_DisabledWhenEmpty"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + ctx->SetRef("//Create New Project"); + }; + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "CreateButton_EnabledWhenFormFilled"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + ctx->SetRef("//Create New Project"); + + ctx->ItemClick("**/##name"); + ctx->KeyCharsAppend("MyTestProject"); + + ctx->ItemClick("**/##location"); + ctx->KeyCharsAppend("C:/Projects"); + ctx->Yield(5); + + // Create button should now be enabled + ImGuiTestItemInfo create_btn = ctx->ItemInfo("//Create"); + IM_CHECK(create_btn.ID != 0); + IM_CHECK((create_btn.ItemFlags & ImGuiItemFlags_Disabled) == 0); + }; + + // ============================================ + // Template Selection Tests + // ============================================ + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "TemplateList_CanSelectTemplate"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + ctx->SetRef("//Create New Project"); + + ctx->ItemClick("**/##templates"); + ctx->Yield(5); + + // If you have specific template names: + ctx->ItemClick("**/##templates/Empty Project"); + }; + + // ============================================ + // End-to-End Project Creation Test + // ============================================ + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "CreateProject_EndToEnd"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + ctx->SetRef("//Create New Project"); + + ctx->ItemClick("**/##name"); + ctx->KeyCharsAppend("E2ETestProject"); + + ctx->ItemClick("**/##location"); + ctx->KeyCharsAppend("C:/TestProjects"); + ctx->Yield(5); + + ctx->ItemClick("**/Create"); + ctx->Yield(60); // Wait for project loading + + // Should transition to editor + // IM_CHECK(ctx->ItemInfo("//Editor")->ID != 0); + }; + + // ============================================ + // Form Reset Test + // ============================================ + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "FormResets_WhenReopened"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + // Open creator and fill form + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + ctx->SetRef("//Create New Project"); + ctx->ItemClick("**/##name"); + ctx->KeyCharsAppend("SomeProject"); + ctx->Yield(5); + + // Cancel + ctx->ItemClick("**/Cancel"); + ctx->Yield(10); + + // Reopen + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + // Form should be reset - Create button should be disabled again + ctx->SetRef("//Create New Project"); + ImGuiTestItemInfo create_btn = ctx->ItemInfo("**/Create"); + IM_CHECK((create_btn.ItemFlags & ImGuiItemFlags_Disabled) != 0); + }; + + // ============================================ + // Folder Browser Button Test + // ============================================ + + t = RegisterTestWithTeardown(engine, "ProjectCreator", "FolderButton_Exists"); + t->TestFunc = [](ImGuiTestContext *ctx) { + ctx->Yield(30); + + ctx->SetRef("//Hellfire - Project Hub"); + ctx->ItemClick("HubLayout/New Project"); + ctx->Yield(10); + + ctx->SetRef("//Create New Project"); + + // If you add ##FolderBrowse ID to your button: + // ImGuiTestItemInfo* folder_btn = ctx->ItemInfo("##FolderBrowse"); + // IM_CHECK(folder_btn != nullptr); + // IM_CHECK(folder_btn->ID != 0); + }; + } +} // namespace hellfire::tests diff --git a/vendor/imgui/imconfig.h b/vendor/imgui/imconfig.h index e0a9f0f..4dab1b6 100644 --- a/vendor/imgui/imconfig.h +++ b/vendor/imgui/imconfig.h @@ -143,6 +143,3 @@ namespace ImGui void MyFunction(const char* name, MyMatrix44* mtx); } */ - -#define IMGUI_ENABLE_TEST_ENGINE -#define IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL 1 From decd5285669a7f370a289a113c75d88fdc8c801f Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Wed, 10 Dec 2025 19:58:45 +0100 Subject: [PATCH 26/27] - Project cleanup - Removed unnecessary scene files --- editor/CMakeLists.txt | 4 +-- editor/Tested.hfscene | 5 ---- editor/Untitled.hfscene | 5 ---- editor/Untitsssasled.hfscene | 5 ---- editorconfig.ini | 52 ------------------------------------ imgui.ini | 52 ------------------------------------ tests/CMakeLists.txt | 11 +------- 7 files changed, 2 insertions(+), 132 deletions(-) delete mode 100644 editor/Tested.hfscene delete mode 100644 editor/Untitled.hfscene delete mode 100644 editor/Untitsssasled.hfscene delete mode 100644 editorconfig.ini delete mode 100644 imgui.ini diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 38f2a93..7b38dde 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -8,7 +8,6 @@ add_library(imguizmo STATIC ) target_link_libraries(imguizmo PRIVATE imgui) - target_include_directories(imguizmo PUBLIC ${IMGUIZMO_DIR} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/imgui @@ -17,9 +16,8 @@ target_include_directories(imguizmo file(GLOB_RECURSE LIB_SOURCES "src/*.cpp") file(GLOB_RECURSE LIB_HEADERS "src/*.h" "src/*.hpp") -# Remove main.cpp from library sources +# Exclude main.cpp from library sources list(FILTER LIB_SOURCES EXCLUDE REGEX ".*main\\.cpp$") - add_library(${LIB_NAME} STATIC ${LIB_SOURCES} ${LIB_HEADERS}) target_include_directories(${LIB_NAME} diff --git a/editor/Tested.hfscene b/editor/Tested.hfscene deleted file mode 100644 index 126b27e..0000000 --- a/editor/Tested.hfscene +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entities": [], - "name": "Tested.hfscene", - "version": "1.0" -} diff --git a/editor/Untitled.hfscene b/editor/Untitled.hfscene deleted file mode 100644 index 8efa32b..0000000 --- a/editor/Untitled.hfscene +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entities": [], - "name": "Untitled.hfscene", - "version": "1.0" -} diff --git a/editor/Untitsssasled.hfscene b/editor/Untitsssasled.hfscene deleted file mode 100644 index 0562397..0000000 --- a/editor/Untitsssasled.hfscene +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entities": [], - "name": "Untitsssasled.hfscene", - "version": "1.0" -} diff --git a/editorconfig.ini b/editorconfig.ini deleted file mode 100644 index ac3ad9d..0000000 --- a/editorconfig.ini +++ /dev/null @@ -1,52 +0,0 @@ -[Window][Debug##Default] -Pos=60,60 -Size=400,400 -Collapsed=0 - -[Window][Scene Hierarchy] -Pos=0,0 -Size=366,1440 -Collapsed=0 -DockId=0x00000002,0 - -[Window][Properties] -Pos=1866,0 -Size=694,720 -Collapsed=0 -DockId=0x00000004,0 - -[Window][Material Editor] -Pos=1866,722 -Size=694,718 -Collapsed=0 -DockId=0x00000005,0 - -[Window][WindowOverViewport_11111111] -Pos=0,0 -Size=2560,1440 -Collapsed=0 - -[Window][Dear ImGui Demo] -Size=549,1440 -Collapsed=0 -DockId=0x00000006,0 - -[Window][Scene Viewport] -Pos=368,0 -Size=2192,892 -Collapsed=0 -DockId=0x00000009,0 - -[Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,0 Size=2560,1440 Split=X - DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=366,1440 Selected=0xB8729153 - DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=2192,1440 Split=X - DockNode ID=0x00000008 Parent=0x00000003 SizeRef=1496,1440 Split=X - DockNode ID=0x00000006 Parent=0x00000008 SizeRef=549,1440 Selected=0x5E5F7166 - DockNode ID=0x00000007 Parent=0x00000008 SizeRef=2009,1440 Split=Y Selected=0x00B71885 - DockNode ID=0x00000009 Parent=0x00000007 SizeRef=1496,892 Selected=0x00B71885 - DockNode ID=0x0000000A Parent=0x00000007 SizeRef=1496,546 CentralNode=1 - DockNode ID=0x00000001 Parent=0x00000003 SizeRef=694,1440 Split=Y Selected=0x8C72BEA8 - DockNode ID=0x00000004 Parent=0x00000001 SizeRef=694,720 Selected=0x8C72BEA8 - DockNode ID=0x00000005 Parent=0x00000001 SizeRef=694,718 Selected=0x3D0FF072 - diff --git a/imgui.ini b/imgui.ini deleted file mode 100644 index ac3ad9d..0000000 --- a/imgui.ini +++ /dev/null @@ -1,52 +0,0 @@ -[Window][Debug##Default] -Pos=60,60 -Size=400,400 -Collapsed=0 - -[Window][Scene Hierarchy] -Pos=0,0 -Size=366,1440 -Collapsed=0 -DockId=0x00000002,0 - -[Window][Properties] -Pos=1866,0 -Size=694,720 -Collapsed=0 -DockId=0x00000004,0 - -[Window][Material Editor] -Pos=1866,722 -Size=694,718 -Collapsed=0 -DockId=0x00000005,0 - -[Window][WindowOverViewport_11111111] -Pos=0,0 -Size=2560,1440 -Collapsed=0 - -[Window][Dear ImGui Demo] -Size=549,1440 -Collapsed=0 -DockId=0x00000006,0 - -[Window][Scene Viewport] -Pos=368,0 -Size=2192,892 -Collapsed=0 -DockId=0x00000009,0 - -[Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,0 Size=2560,1440 Split=X - DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=366,1440 Selected=0xB8729153 - DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=2192,1440 Split=X - DockNode ID=0x00000008 Parent=0x00000003 SizeRef=1496,1440 Split=X - DockNode ID=0x00000006 Parent=0x00000008 SizeRef=549,1440 Selected=0x5E5F7166 - DockNode ID=0x00000007 Parent=0x00000008 SizeRef=2009,1440 Split=Y Selected=0x00B71885 - DockNode ID=0x00000009 Parent=0x00000007 SizeRef=1496,892 Selected=0x00B71885 - DockNode ID=0x0000000A Parent=0x00000007 SizeRef=1496,546 CentralNode=1 - DockNode ID=0x00000001 Parent=0x00000003 SizeRef=694,1440 Split=Y Selected=0x8C72BEA8 - DockNode ID=0x00000004 Parent=0x00000001 SizeRef=694,720 Selected=0x8C72BEA8 - DockNode ID=0x00000005 Parent=0x00000001 SizeRef=694,718 Selected=0x3D0FF072 - diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b083e25..7a83737 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -81,9 +81,7 @@ if(HELLFIRE_UI_TESTS) target_link_libraries(imgui_with_test_engine PUBLIC GL) endif () - # ============================================ # ImGuizmo with Test Engine - # ============================================ add_library(imguizmo_test STATIC ${IMGUIZMO_DIR}/ImGuizmo.h ${IMGUIZMO_DIR}/ImGuizmo.cpp @@ -92,9 +90,7 @@ if(HELLFIRE_UI_TESTS) target_link_libraries(imguizmo_test PRIVATE imgui_with_test_engine) target_include_directories(imguizmo_test PUBLIC ${IMGUIZMO_DIR}) - # ============================================ - # Engine Library for Testing (links against test-enabled imgui) - # ============================================ + # Engine Library for Testing file(GLOB_RECURSE ENGINE_LIB_SOURCES "${CMAKE_SOURCE_DIR}/engine/src/*.cpp") file(GLOB_RECURSE ENGINE_LIB_HEADERS "${CMAKE_SOURCE_DIR}/engine/src/*.h" "${CMAKE_SOURCE_DIR}/engine/src/*.hpp") @@ -107,7 +103,6 @@ if(HELLFIRE_UI_TESTS) find_package(OpenGL REQUIRED) - # Copy the same link libraries as HellfireEngine, but swap imgui for imgui_with_test_engine target_link_libraries(HellfireEngine_Test PUBLIC libglew_static @@ -121,9 +116,7 @@ if(HELLFIRE_UI_TESTS) json ) - # ============================================ # Editor Library for Testing - # ============================================ file(GLOB_RECURSE EDITOR_LIB_SOURCES "${CMAKE_SOURCE_DIR}/editor/src/*.cpp") file(GLOB_RECURSE EDITOR_LIB_HEADERS "${CMAKE_SOURCE_DIR}/editor/src/*.h" "${CMAKE_SOURCE_DIR}/editor/src/*.hpp") list(FILTER EDITOR_LIB_SOURCES EXCLUDE REGEX ".*main\\.cpp$") @@ -147,9 +140,7 @@ if(HELLFIRE_UI_TESTS) glfw ) - # ============================================ # UI Test Executable - # ============================================ file(GLOB_RECURSE UI_TEST_SOURCES "ui/*.cpp") file(GLOB_RECURSE UI_TEST_HEADERS "ui/*.h" "ui/*.hpp") From 7b1a4fd6b5f9a82070ce2cce2906e27a74d06da2 Mon Sep 17 00:00:00 2001 From: TrueStatement Date: Thu, 11 Dec 2025 07:08:19 +0100 Subject: [PATCH 27/27] - Fixed layouts - Users can now open the project folder from the hub - Users can now instantiate their models in their scenes - Models get autoimported and then transformed to engine specific format --- editor/assets/layouts/default.ini | 80 ++++++++++ editor/imgui.ini | 45 ------ editor/src/core/EditorApplication.cpp | 38 +++-- editor/src/core/EditorApplication.h | 3 + editor/src/scenes/DefaultScene.h | 1 + editor/src/states/Editor/EditorState.cpp | 4 +- .../src/states/ProjectHub/ProjectHubState.cpp | 10 ++ editor/src/ui/PanelManager.h | 4 + .../ui/Panels/AssetExplorer/AssetExplorer.cpp | 14 +- .../ui/Panels/MenuBar/MenuBarComponent.cpp | 22 ++- .../src/ui/Panels/MenuBar/MenuBarComponent.h | 3 + .../src/ui/Panels/Viewport/ViewportPanel.cpp | 5 +- engine/src/hellfire/assets/AssetManager.cpp | 4 + .../assets/importers/AssetImportManager.cpp | 80 +++++++++- .../assets/importers/AssetImportManager.h | 7 +- .../hellfire/assets/models/ModelImporter.cpp | 2 +- .../hellfire/assets/models/ModelImporter.h | 2 +- .../assets/models/ModelInstantiator.cpp | 147 ++++++++++-------- .../assets/models/ModelInstantiator.h | 27 ++-- engine/src/hellfire/core/Application.cpp | 73 ++++----- engine/src/hellfire/core/Application.h | 1 - engine/src/hellfire/core/Project.cpp | 6 + engine/src/hellfire/core/Project.h | 2 + engine/src/hellfire/graphics/Mesh.cpp | 6 + engine/src/hellfire/graphics/Mesh.h | 12 +- .../hellfire/serializers/TextureSerializer.h | 2 +- 26 files changed, 411 insertions(+), 189 deletions(-) create mode 100644 editor/assets/layouts/default.ini delete mode 100644 editor/imgui.ini diff --git a/editor/assets/layouts/default.ini b/editor/assets/layouts/default.ini new file mode 100644 index 0000000..74bb028 --- /dev/null +++ b/editor/assets/layouts/default.ini @@ -0,0 +1,80 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Window][Hellfire - Project Hub] +Pos=-190,-190 +Size=1280,720 +Collapsed=0 + +[Window][DockSpace] +Pos=0,0 +Size=2560,1369 +Collapsed=0 + +[Window][Creating Project] +Pos=-242,-242 +Size=1280,720 +Collapsed=0 + +[Window][Scene Hierarchy Panel] +Pos=0,22 +Size=432,1347 +Collapsed=0 +DockId=0x00000001,0 + +[Window][Renderer Settings] +Pos=1971,22 +Size=589,1347 +Collapsed=0 +DockId=0x00000006,0 + +[Window][Asset Browser] +Pos=434,731 +Size=1535,638 +Collapsed=0 +DockId=0x0000000A,0 + +[Window][Viewport] +Pos=434,22 +Size=1535,701 +Collapsed=0 +DockId=0x00000003,0 + +[Window][Inspector] +Pos=1971,22 +Size=589,1347 +Collapsed=0 +DockId=0x00000006,1 + +[Window][ Test_Scene.hfscene] +Pos=434,22 +Size=255,664 +Collapsed=0 +DockId=0x00000007,0 + +[Window][Scene Viewport] +Pos=434,22 +Size=1535,707 +Collapsed=0 +DockId=0x00000009,0 + +[Table][0x67B58617,2] +RefScale=16 +Column 0 Width=150 +Column 1 Weight=1.0000 + +[Docking][Data] +DockSpace ID=0xCCBD8CF7 Window=0x3DA2F1DE Pos=0,45 Size=2560,1347 Split=X + DockNode ID=0x00000005 Parent=0xCCBD8CF7 SizeRef=1969,1347 Split=X + DockNode ID=0x00000001 Parent=0x00000005 SizeRef=432,1347 Selected=0x2E557BF6 + DockNode ID=0x00000002 Parent=0x00000005 SizeRef=1535,1347 Split=Y + DockNode ID=0x00000003 Parent=0x00000002 SizeRef=2126,701 Selected=0xC450F867 + DockNode ID=0x00000004 Parent=0x00000002 SizeRef=2126,644 Split=Y Selected=0x36AF052B + DockNode ID=0x00000007 Parent=0x00000004 SizeRef=1535,692 Selected=0x7025CC7F + DockNode ID=0x00000008 Parent=0x00000004 SizeRef=1535,653 Split=Y Selected=0x36AF052B + DockNode ID=0x00000009 Parent=0x00000008 SizeRef=255,707 Selected=0x00B71885 + DockNode ID=0x0000000A Parent=0x00000008 SizeRef=255,638 CentralNode=1 Selected=0x36AF052B + DockNode ID=0x00000006 Parent=0xCCBD8CF7 SizeRef=589,1347 Selected=0x47251AAB + diff --git a/editor/imgui.ini b/editor/imgui.ini deleted file mode 100644 index 79388f5..0000000 --- a/editor/imgui.ini +++ /dev/null @@ -1,45 +0,0 @@ -[Window][DockSpace] -Pos=0,0 -Size=1280,720 -Collapsed=0 - -[Window][Scene Hierarchy Panel] -Pos=0,22 -Size=394,698 -Collapsed=0 -DockId=0x00000001,0 - -[Window][Debug##Default] -Pos=60,60 -Size=400,400 -Collapsed=0 - -[Window][Inspector] -Pos=1154,22 -Size=126,698 -Collapsed=0 -DockId=0x00000004,0 - -[Window][ Test] -Pos=396,22 -Size=756,698 -Collapsed=0 -DockId=0x00000002,0 - -[Window][RenameModal] -Pos=584,75 -Size=302,277 -Collapsed=0 - -[Window][Rename entity] -Pos=589,307 -Size=243,261 -Collapsed=0 - -[Docking][Data] -DockSpace ID=0xCCBD8CF7 Window=0x3DA2F1DE Pos=60,105 Size=1280,698 Split=X - DockNode ID=0x00000003 Parent=0xCCBD8CF7 SizeRef=1152,698 Split=X - DockNode ID=0x00000001 Parent=0x00000003 SizeRef=394,698 Selected=0x2E557BF6 - DockNode ID=0x00000002 Parent=0x00000003 SizeRef=1396,698 CentralNode=1 Selected=0x7DE84AAA - DockNode ID=0x00000004 Parent=0xCCBD8CF7 SizeRef=126,698 Selected=0x36DC96AB - diff --git a/editor/src/core/EditorApplication.cpp b/editor/src/core/EditorApplication.cpp index b654e3c..d833de5 100644 --- a/editor/src/core/EditorApplication.cpp +++ b/editor/src/core/EditorApplication.cpp @@ -42,31 +42,31 @@ namespace hellfire::editor { state_manager_.set_context(&editor_context_); editor_context_.project_manager = std::make_unique(editor_context_.event_bus, editor_context_); - auto* pm = editor_context_.project_manager.get(); + auto *pm = editor_context_.project_manager.get(); // Subscribe to state transitions - editor_context_.event_bus.subscribe([this](const auto&) { + editor_context_.event_bus.subscribe([this](const auto &) { state_manager_.switch_to(); }); - - editor_context_.event_bus.subscribe([this](const auto&) { + + editor_context_.event_bus.subscribe([this](const auto &) { state_manager_.switch_to(); }); - - editor_context_.event_bus.subscribe([this, pm](const CreateProjectEvent& e) { + + editor_context_.event_bus.subscribe([this, pm](const CreateProjectEvent &e) { state_manager_.switch_to(); pm->create_project_async(e.name, e.location, e.template_id); }); - - editor_context_.event_bus.subscribe([this, pm](const OpenProjectEvent& e) { + + editor_context_.event_bus.subscribe([this, pm](const OpenProjectEvent &e) { state_manager_.switch_to(); pm->open_project_async(e.path); }); - - editor_context_.event_bus.subscribe([this](const auto&) { + + editor_context_.event_bus.subscribe([this](const auto &) { state_manager_.switch_to(); }); - - editor_context_.event_bus.subscribe([this, pm](const auto&) { + + editor_context_.event_bus.subscribe([this, pm](const auto &) { pm->close_project(); state_manager_.switch_to(); }); @@ -75,12 +75,24 @@ namespace hellfire::editor { state_manager_.switch_to(); } + void EditorApplication::load_editor_ui_config(ImGuiIO& io) { + io.IniFilename = nullptr; + + if (std::filesystem::exists("imgui.ini")) { + ImGui::LoadIniSettingsFromDisk("imgui.ini"); + } else { + ImGui::LoadIniSettingsFromDisk("assets/layouts/default.ini"); + } + } + void EditorApplication::initialize_imgui(IWindow *window) { // Setup ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO &io = ImGui::GetIO(); + load_editor_ui_config(io); + // Load default font io.Fonts->AddFontFromFileTTF("assets/fonts/Roboto-Regular.ttf", 16.0f); static constexpr ImWchar icons_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; @@ -100,7 +112,7 @@ namespace hellfire::editor { io.ConfigViewportsNoDecoration = false; io.ConfigViewportsNoTaskBarIcon = false; io.ConfigViewportsNoAutoMerge = false; - + io.ConfigErrorRecoveryEnableAssert = true; // Setup style diff --git a/editor/src/core/EditorApplication.h b/editor/src/core/EditorApplication.h index 44d50c3..e86bf9d 100644 --- a/editor/src/core/EditorApplication.h +++ b/editor/src/core/EditorApplication.h @@ -4,6 +4,7 @@ #pragma once +#include "imgui.h" #include "StateManager.h" #include "../ui/Panels/EditorPanel.h" #include "hellfire/Interfaces/IApplicationPlugin.h" @@ -15,6 +16,8 @@ namespace hellfire::editor { public: void on_initialize(Application &app) override; + void load_editor_ui_config(ImGuiIO &io); + void initialize_imgui(IWindow *window); ~EditorApplication() override; diff --git a/editor/src/scenes/DefaultScene.h b/editor/src/scenes/DefaultScene.h index 1e7e6bc..39dbd63 100644 --- a/editor/src/scenes/DefaultScene.h +++ b/editor/src/scenes/DefaultScene.h @@ -4,6 +4,7 @@ #pragma once #include "RotateScript.h" +#include "hellfire/assets/models/ModelInstantiator.h" #include "hellfire/graphics/Skybox.h" #include "hellfire/graphics/geometry/Cube.h" #include "hellfire/graphics/geometry/Sphere.h" diff --git a/editor/src/states/Editor/EditorState.cpp b/editor/src/states/Editor/EditorState.cpp index b7a33bd..2612a9f 100644 --- a/editor/src/states/Editor/EditorState.cpp +++ b/editor/src/states/Editor/EditorState.cpp @@ -33,14 +33,14 @@ namespace hellfire::editor { void EditorState::on_exit() { ApplicationState::on_exit(); + panel_manager_.remove_all_panels(); + } void EditorState::render() { // Create main dockspace create_dockspace(); - - panel_manager_.render_all(); } diff --git a/editor/src/states/ProjectHub/ProjectHubState.cpp b/editor/src/states/ProjectHub/ProjectHubState.cpp index 7920d64..7d23abb 100644 --- a/editor/src/states/ProjectHub/ProjectHubState.cpp +++ b/editor/src/states/ProjectHub/ProjectHubState.cpp @@ -4,6 +4,11 @@ #include "ProjectHubState.h" +#if WIN32 +#include +#include +#endif + #include "IconsFontAwesome6.h" #include "imgui.h" #include "events/StateEvents.h" @@ -75,6 +80,11 @@ namespace hellfire::editor { context_->project_manager->remove_from_recent(project.path); recent_projects_ = context_->project_manager->get_recent_projects(); } + if (ImGui::MenuItem("Open in Explorer")) { +#if WIN32 + ShellExecuteW(NULL, L"open", project.path.parent_path().c_str(), NULL, NULL, SW_SHOW); +#endif + } ImGui::EndPopup(); } diff --git a/editor/src/ui/PanelManager.h b/editor/src/ui/PanelManager.h index d01bc5f..ecf6d72 100644 --- a/editor/src/ui/PanelManager.h +++ b/editor/src/ui/PanelManager.h @@ -35,6 +35,10 @@ namespace hellfire::editor { } } + void remove_all_panels() { + panels_.clear(); + } + private: std::vector> panels_; }; diff --git a/editor/src/ui/Panels/AssetExplorer/AssetExplorer.cpp b/editor/src/ui/Panels/AssetExplorer/AssetExplorer.cpp index e23d55f..e2fc071 100644 --- a/editor/src/ui/Panels/AssetExplorer/AssetExplorer.cpp +++ b/editor/src/ui/Panels/AssetExplorer/AssetExplorer.cpp @@ -4,18 +4,26 @@ #include "AssetExplorer.h" +#include "hellfire/assets/models/ModelInstantiator.h" #include "ui/ui.h" namespace hellfire::editor { void AssetExplorer::render() { if (ui::Window window {"Asset Browser"}) { - if (auto asset_registry = ServiceLocator::get_service()) { - auto assets = asset_registry->get_all_assets(); + if (auto asset_registry = ServiceLocator::get_service(); auto asset_manager = ServiceLocator::get_service()) { + auto assets = asset_registry->get_assets_by_type(AssetType::MODEL); + ModelInstantiator instantiator(*asset_manager, *asset_registry); for (auto asset : assets) { + if (asset.uuid == INVALID_ASSET_ID || asset.filepath.extension() != ".hfmodel") continue; + + ImGui::PushID(asset.uuid); ImGui::Text(asset.name.c_str()); ImGui::SameLine(); - ImGui::Text("%u", asset.type); + if (ImGui::Button("Instantiate Model")) { + instantiator.instantiate(*context_->active_scene, asset.uuid); + }; + ImGui::PopID(); } } } diff --git a/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp b/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp index 821b820..7230538 100644 --- a/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp +++ b/editor/src/ui/Panels/MenuBar/MenuBarComponent.cpp @@ -5,6 +5,8 @@ #include #include "imgui.h" +#include "imgui_internal.h" +#include "events/StateEvents.h" #include "hellfire/core/Project.h" #include "hellfire/ecs/ComponentRegistration.h" #include "hellfire/scene/CameraFactory.h" @@ -20,18 +22,23 @@ namespace hellfire::editor { if (ImGui::BeginMenuBar()) { render_file_menu(); render_scene_menu(); + render_layout_menu(); ImGui::EndMenuBar(); } } void MenuBarComponent::render_file_menu() { if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New Project")) { + if (ImGui::MenuItem("Open Project")) { context_->event_bus.dispatch(); } if (ImGui::MenuItem("Save Project")) { context_->project_manager->get_current_project()->save(); } + if (ImGui::MenuItem("Quit to Project Hub")) { + + context_->event_bus.dispatch(); + } ImGui::EndMenu(); } } @@ -48,6 +55,19 @@ namespace hellfire::editor { } } + void MenuBarComponent::render_layout_menu() { + if (ImGui::BeginMenu("Editor")) { + if (ImGui::BeginMenu("Layouts")) { + if (ImGui::MenuItem("Default")) { + ImGui::ClearIniSettings(); + ImGui::LoadIniSettingsFromDisk("assets/layouts/default.ini"); + } + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + } + void MenuBarComponent::handle_new_scene() { if (ImGui::MenuItem("New Scene")) { create_default_scene(); diff --git a/editor/src/ui/Panels/MenuBar/MenuBarComponent.h b/editor/src/ui/Panels/MenuBar/MenuBarComponent.h index c3379b4..9231a01 100644 --- a/editor/src/ui/Panels/MenuBar/MenuBarComponent.h +++ b/editor/src/ui/Panels/MenuBar/MenuBarComponent.h @@ -9,10 +9,13 @@ namespace hellfire::editor { class MenuBarComponent : public EditorPanel { public: MenuBarComponent(EditorContext* ctx) : EditorPanel(ctx) {} + + void render() override; private: void render_file_menu(); void render_scene_menu(); + void render_layout_menu(); void handle_new_scene(); void handle_open_scene() const; void render_scene_list() const; diff --git a/editor/src/ui/Panels/Viewport/ViewportPanel.cpp b/editor/src/ui/Panels/Viewport/ViewportPanel.cpp index c933d25..177381f 100644 --- a/editor/src/ui/Panels/Viewport/ViewportPanel.cpp +++ b/editor/src/ui/Panels/Viewport/ViewportPanel.cpp @@ -262,11 +262,8 @@ namespace hellfire::editor { ImGui::SetNextWindowPos(default_pos, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSizeConstraints(ImVec2(320, 180), ImVec2(FLT_MAX, FLT_MAX)); - const std::string window_name = context_->active_scene - ? ICON_FA_EYE " " + context_->active_scene->get_name() - : "Viewport"; - if (ui::Window window{window_name}) { + if (ui::Window window{"Scene Viewport"}) { if (!context_->active_scene) return; // Store the viewport bound for outside usage viewport_pos_ = ImGui::GetWindowPos(); diff --git a/engine/src/hellfire/assets/AssetManager.cpp b/engine/src/hellfire/assets/AssetManager.cpp index 56b9334..4a87dfd 100644 --- a/engine/src/hellfire/assets/AssetManager.cpp +++ b/engine/src/hellfire/assets/AssetManager.cpp @@ -29,6 +29,10 @@ namespace hellfire { return nullptr; } + if (mesh && !mesh->is_built()) { + mesh->build(); + } + mesh_cache_[id] = mesh; return mesh; } diff --git a/engine/src/hellfire/assets/importers/AssetImportManager.cpp b/engine/src/hellfire/assets/importers/AssetImportManager.cpp index d3996aa..adf1dca 100644 --- a/engine/src/hellfire/assets/importers/AssetImportManager.cpp +++ b/engine/src/hellfire/assets/importers/AssetImportManager.cpp @@ -8,6 +8,11 @@ #include "hellfire/serializers/ModelSerializer.h" #include "hellfire/serializers/TextureSerializer.h" +#include +#include +#include +#include + namespace hellfire { TextureType infer_texture_type(const std::string& name) { std::string lower_name = name; @@ -58,22 +63,40 @@ namespace hellfire { void AssetImportManager::import_all_models() { auto models = registry_.get_assets_by_type(AssetType::MODEL); + std::vector to_import; + for (const auto &meta: models) { // Skip already-imported internal formats std::string ext = meta.filepath.extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), tolower); - if (ext == ".hfmodel" || ext == ".hfmesh") { - continue; // Already internal format - } + if (ext == ".hfmodel" || ext == ".hfmesh") continue; if (needs_import(meta.uuid)) { - std::cout << "Importing model: " << meta.name << std::endl; - import_model(meta); + to_import.push_back(meta); + } + } + + // Parallel import + std::mutex output_mutex; + std::mutex registry_mutex; + + auto worker = [&](const AssetMetadata& meta) { + bool success = import_model_threaded(meta, registry_mutex); + + std::lock_guard lock(output_mutex); + if (success) { + std::cout << "Imported: " << meta.name << std::endl; } else { - std::cout << "Skipping (already imported): " << meta.name << std::endl; + std::cerr << "Failed: " << meta.name << std::endl; } + }; + + std::vector> futures; + for (const auto& meta : to_import) { + futures.push_back(std::async(std::launch::async, worker, meta)); } + for (auto& f : futures) f.get(); } void AssetImportManager::import_all_textures() { @@ -178,6 +201,51 @@ namespace hellfire { return true; } + bool AssetImportManager::import_model_threaded( + const AssetMetadata& meta, + std::mutex& registry_mutex) + { + auto source_path = project_root_ / meta.filepath; + + if (!std::filesystem::exists(source_path)) { + std::cerr << "Source file not found: " << source_path << std::endl; + return false; + } + + // Create output directory for this model's assets + auto model_output_dir = import_output_dir_ / meta.name; + std::filesystem::create_directories(model_output_dir); + + // Import using ModelImporter + ModelImporter importer(registry_, model_output_dir); + ImportSettings settings; + settings.generate_normals = true; + settings.generate_tangents = true; + settings.optimize_meshes = true; + + ImportResult result = importer.import(source_path, settings); + + if (!result.success) { + std::cerr << "Failed to import model: " << meta.name + << " - " << result.error_message << std::endl; + return false; + } + + // Save the ImportResult as .hfmodel + auto model_path = model_output_dir / (meta.name + ".hfmodel"); + if (!ModelSerializer::save(model_path, result)) { + std::cerr << "Failed to save .hfmodel: " << model_path << std::endl; + return false; + } + + { + std::lock_guard lock(registry_mutex); + registry_.register_asset(model_path, AssetType::MODEL); + } + + return true; + } + bool AssetImportManager::import_texture(const AssetMetadata &meta) { // Textures don't need conversion, just metadata auto source_path = registry_.get_absolute_path(meta.uuid); diff --git a/engine/src/hellfire/assets/importers/AssetImportManager.h b/engine/src/hellfire/assets/importers/AssetImportManager.h index 944fb7b..fc6296f 100644 --- a/engine/src/hellfire/assets/importers/AssetImportManager.h +++ b/engine/src/hellfire/assets/importers/AssetImportManager.h @@ -3,6 +3,8 @@ // #pragma once +#include + #include "hellfire/assets/AssetManager.h" #include "hellfire/assets/AssetRegistry.h" @@ -32,7 +34,10 @@ class AssetImportManager { std::filesystem::path import_output_dir_; bool import_model(const AssetMetadata& meta); - bool import_texture(const AssetMetadata& meta); + + bool import_model_threaded(const AssetMetadata &meta, std::mutex ®istry_mutex); + + bool import_texture(const AssetMetadata& meta); // Check if imported version exists bool has_imported_mesh(AssetID original_id) const; diff --git a/engine/src/hellfire/assets/models/ModelImporter.cpp b/engine/src/hellfire/assets/models/ModelImporter.cpp index 39d7a08..dde1a4d 100644 --- a/engine/src/hellfire/assets/models/ModelImporter.cpp +++ b/engine/src/hellfire/assets/models/ModelImporter.cpp @@ -199,7 +199,7 @@ namespace hellfire { } // Serialize to file - Mesh mesh(vertices, indices); + Mesh mesh(vertices, indices, true); // defer building, because opengl isn't thread safe const std::string filename = make_unique_name(base_name_, "mesh", mesh_index) + ".hfmesh"; const auto filepath = output_dir_ / filename; diff --git a/engine/src/hellfire/assets/models/ModelImporter.h b/engine/src/hellfire/assets/models/ModelImporter.h index 0db147c..0cb75aa 100644 --- a/engine/src/hellfire/assets/models/ModelImporter.h +++ b/engine/src/hellfire/assets/models/ModelImporter.h @@ -16,7 +16,7 @@ namespace hellfire { bool generate_normals = true; bool generate_tangents = true; bool triangulate = true; - bool flip_uvs = false; + bool flip_uvs = true; bool optimize_meshes = true; float scale_factor = 1.0f; }; diff --git a/engine/src/hellfire/assets/models/ModelInstantiator.cpp b/engine/src/hellfire/assets/models/ModelInstantiator.cpp index 0de554e..73cc860 100644 --- a/engine/src/hellfire/assets/models/ModelInstantiator.cpp +++ b/engine/src/hellfire/assets/models/ModelInstantiator.cpp @@ -7,95 +7,118 @@ #include "hellfire/ecs/RenderableComponent.h" #include "hellfire/ecs/TransformComponent.h" #include "hellfire/ecs/components/MeshComponent.h" +#include "hellfire/serializers/ModelSerializer.h" namespace hellfire { - ModelInstantiator::ModelInstantiator(Scene &scene, AssetManager &assets) : scene_(scene), assets_(assets) {} - - EntityID ModelInstantiator::instantiate(const ImportResult &result, const EntityID parent) { - if (!result.success || result.nodes.empty()) { - return 0; + ModelInstantiator::ModelInstantiator(AssetManager& assets, AssetRegistry& registry) + : assets_(assets) + , registry_(registry) + {} + + EntityID ModelInstantiator::instantiate(Scene& scene, AssetID model_asset_id, EntityID parent) { + auto meta = registry_.get_asset(model_asset_id); + if (!meta) { + std::cerr << "ModelInstantiator: Asset not found: " << model_asset_id << std::endl; + return INVALID_ENTITY; } - return instantiate_node(result, result.root_node_index, parent); + return instantiate(scene, registry_.get_absolute_path(model_asset_id), parent); } - EntityID ModelInstantiator::instantiate_mesh(AssetID mesh_asset, AssetID material_asset, EntityID parent) { - auto mesh = assets_.get_mesh(mesh_asset); - if (!mesh) return 0; - - EntityID entity_id = scene_.create_entity("Mesh"); - Entity* entity = scene_.get_entity(entity_id); - - if (parent != 0) { - scene_.set_parent(entity_id, parent); + EntityID ModelInstantiator::instantiate(Scene& scene, + const std::filesystem::path& hfmodel_path, + EntityID parent) { + auto model_opt = ModelSerializer::load(hfmodel_path); + if (!model_opt) { + std::cerr << "ModelInstantiator: Failed to load model: " << hfmodel_path << std::endl; + return INVALID_ENTITY; } - auto* mesh_comp = entity->add_component(); - mesh_comp->set_mesh(mesh); + return instantiate(scene, *model_opt, parent); + } - if (material_asset != INVALID_ASSET_ID) { - auto material = assets_.get_material(material_asset); - if (material) { - auto* renderable = entity->add_component(); - renderable->set_material(material); - } + EntityID ModelInstantiator::instantiate(Scene& scene, + const ImportResult& model, + EntityID parent) { + if (!model.success || model.nodes.empty()) { + std::cerr << "ModelInstantiator: Invalid model data" << std::endl; + return INVALID_ENTITY; } - return entity_id; + return instantiate_node(scene, model, model.root_node_index, parent); } - EntityID ModelInstantiator::instantiate_node(const ImportResult &result, size_t node_index, EntityID parent) { - const auto& node = result.nodes[node_index]; + EntityID ModelInstantiator::instantiate_node(Scene& scene, + const ImportResult& model, + size_t node_index, + EntityID parent) { + const auto& node = model.nodes[node_index]; - EntityID entity_id = scene_.create_entity(node.name); - Entity* entity = scene_.get_entity(entity_id); + // Create entity + EntityID entity_id = scene.create_entity(node.name); + Entity* entity = scene.get_entity(entity_id); // Set transform - entity->transform()->set_position(node.position); - entity->transform()->set_rotation(node.rotation); - entity->transform()->set_scale(node.scale); + auto* transform = entity->get_component(); + if (transform) { + transform->set_position(node.position); + transform->set_rotation(node.rotation); + transform->set_scale(node.scale); + } - if (parent != 0) { - scene_.set_parent(entity_id, parent); + // Set parent + if (parent != INVALID_ENTITY) { + scene.set_parent(entity_id, parent); } - // Attach meshes + // Attach mesh components if (node.mesh_indices.size() == 1) { - // Single mesh: attach directly - const auto& mesh_data = result.meshes[node.mesh_indices[0]]; - - auto* mesh_comp = entity->add_component(); - mesh_comp->set_mesh(assets_.get_mesh(mesh_data.mesh_asset)); - - if (mesh_data.material_asset != INVALID_ASSET_ID) { - auto* renderable = entity->add_component(); - renderable->set_material(assets_.get_material(mesh_data.material_asset)); - } - } else if (node.mesh_indices.size() > 1) { - // Multiple meshes: create child entities + // Single mesh - attach directly to this entity + const auto& mesh_ref = model.meshes[node.mesh_indices[0]]; + attach_mesh_components(entity, mesh_ref); + } + else if (node.mesh_indices.size() > 1) { + // Multiple meshes - create child entities for each for (size_t mesh_idx : node.mesh_indices) { - const auto& mesh_data = result.meshes[mesh_idx]; - - EntityID mesh_entity = scene_.create_entity(mesh_data.name); - scene_.set_parent(mesh_entity, entity_id); - - Entity* mesh_ent = scene_.get_entity(mesh_entity); - - auto* mesh_comp = mesh_ent->add_component(); - mesh_comp->set_mesh(assets_.get_mesh(mesh_data.mesh_asset)); - - if (mesh_data.material_asset != INVALID_ASSET_ID) { - auto* renderable = mesh_ent->add_component(); - renderable->set_material(assets_.get_material(mesh_data.material_asset)); - } + const auto& mesh_ref = model.meshes[mesh_idx]; + + EntityID mesh_entity_id = scene.create_entity(mesh_ref.name); + Entity* mesh_entity = scene.get_entity(mesh_entity_id); + scene.set_parent(mesh_entity_id, entity_id); + + attach_mesh_components(mesh_entity, mesh_ref); } } - // Instantiate children + // Recursively instantiate children for (size_t child_idx : node.child_indices) { - instantiate_node(result, child_idx, entity_id); + instantiate_node(scene, model, child_idx, entity_id); } return entity_id; } + + void ModelInstantiator::attach_mesh_components(Entity* entity, const ImportedMesh& mesh_ref) { + // Add MeshComponent + auto* mesh_comp = entity->add_component(); + mesh_comp->set_mesh_asset(mesh_ref.mesh_asset); + + // Load actual mesh data + if (auto mesh = assets_.get_mesh(mesh_ref.mesh_asset)) { + mesh_comp->set_mesh(mesh); + } + + // Add RenderableComponent with material + auto* renderable = entity->add_component(); + renderable->set_material_asset(mesh_ref.material_asset); + + // Load actual material data + if (mesh_ref.material_asset != INVALID_ASSET_ID) { + if (auto material = assets_.get_material(mesh_ref.material_asset)) { + renderable->set_material(material); + } + } + } + + } diff --git a/engine/src/hellfire/assets/models/ModelInstantiator.h b/engine/src/hellfire/assets/models/ModelInstantiator.h index 6a304cd..a03b8eb 100644 --- a/engine/src/hellfire/assets/models/ModelInstantiator.h +++ b/engine/src/hellfire/assets/models/ModelInstantiator.h @@ -13,21 +13,28 @@ namespace hellfire { */ class ModelInstantiator { public: - ModelInstantiator(Scene& scene, AssetManager& assets); + ModelInstantiator(AssetManager& assets, AssetRegistry& registry); - EntityID instantiate(const ImportResult& result, EntityID parent = 0); + // Instantiate from AssetID (looks up .hfmodel file) + EntityID instantiate(Scene& scene, AssetID model_asset_id, EntityID parent = INVALID_ENTITY); - // Instantiate a single mesh as an entity - EntityID instantiate_mesh(AssetID mesh_asset, - AssetID material_asset = INVALID_ASSET_ID, - EntityID parent = 0); + // Instantiate from path directly + EntityID instantiate(Scene& scene, const std::filesystem::path& hfmodel_path, EntityID parent = INVALID_ENTITY); + + // Instantiate from already-loaded ImportResult + EntityID instantiate(Scene& scene, const ImportResult& model, EntityID parent = INVALID_ENTITY); private: - Scene& scene_; AssetManager& assets_; + AssetRegistry& registry_; + + EntityID instantiate_node(Scene& scene, + const ImportResult& model, + size_t node_index, + EntityID parent); - EntityID instantiate_node(const ImportResult& result, - size_t node_index, - EntityID parent); + void attach_mesh_components(Entity *entity, const ImportedMesh &mesh_ref); }; + + } diff --git a/engine/src/hellfire/core/Application.cpp b/engine/src/hellfire/core/Application.cpp index 9c191c3..d140e43 100644 --- a/engine/src/hellfire/core/Application.cpp +++ b/engine/src/hellfire/core/Application.cpp @@ -84,18 +84,17 @@ namespace hellfire { } // Register services - ServiceLocator::register_service(&renderer_); ServiceLocator::register_service(input_manager_.get()); ServiceLocator::register_service(&shader_manager_); ServiceLocator::register_service(window_.get()); // Initialize engine systems Time::init(); - renderer_.init(); + // renderer_.init(); // Create fallback shader - Shader *fallback = ensure_fallback_shader(); - renderer_.set_fallback_shader(*fallback); + // Shader *fallback = ensure_fallback_shader(); + // renderer_.set_fallback_shader(*fallback); call_plugins([this](IApplicationPlugin &plugin) { plugin.on_initialize(*this); @@ -104,26 +103,26 @@ namespace hellfire { void Application::run() { // while (!should_exit()) { - while (!window_->should_close()) { - if (window_info_.minimized) { - window_->wait_for_events(); - continue; - } - - // Poll the window for events (mouse inputs, keys, window stuff, etc.) - window_->poll_events(); - // Make sure the timer is updated - Time::update(); + while (!window_->should_close()) { + if (window_info_.minimized) { + window_->wait_for_events(); + continue; + } - input_manager_->update(); + // Poll the window for events (mouse inputs, keys, window stuff, etc.) + window_->poll_events(); + // Make sure the timer is updated + Time::update(); - // Update scene - if (auto sm = ServiceLocator::get_service()) { - sm->update(Time::delta_time); - } + input_manager_->update(); - on_render(); + // Update scene + if (auto sm = ServiceLocator::get_service()) { + sm->update(Time::delta_time); } + + on_render(); + } // } } @@ -133,29 +132,33 @@ namespace hellfire { call_plugins([](IApplicationPlugin &plugin) { plugin.on_begin_frame(); }); - renderer_.begin_frame(); - - if (auto sm = ServiceLocator::get_service()) { - if (auto *active_scene = sm->get_active_scene()) { - Entity *camera_override = nullptr; + if (auto renderer = ServiceLocator::get_service()) { + renderer->begin_frame(); - call_plugins([&camera_override](IApplicationPlugin &plugin) { - if (!camera_override) { - camera_override = plugin.get_render_camera_override(); - } - }); + if (auto sm = ServiceLocator::get_service()) { + if (auto *active_scene = sm->get_active_scene()) { + Entity *camera_override = nullptr; - renderer_.render(*active_scene, camera_override); + call_plugins([&camera_override](IApplicationPlugin &plugin) { + if (!camera_override) { + camera_override = plugin.get_render_camera_override(); + } + }); + + renderer->render(*active_scene, camera_override); + } } } + // Plugin render call_plugins([](IApplicationPlugin &plugin) { plugin.on_render(); }); - - renderer_.end_frame(); + if (auto renderer = ServiceLocator::get_service()) { + renderer->end_frame(); + } // Plugin end_frame call_plugins([](IApplicationPlugin &plugin) { plugin.on_end_frame(); @@ -268,11 +271,11 @@ namespace hellfire { void Application::set_exit_condition(std::function condition) { exit_condition_ = std::move(condition); } - + void Application::request_exit() { should_exit_ = true; } - + bool Application::should_exit() const { if (should_exit_) return true; if (exit_condition_ && exit_condition_()) return true; diff --git a/engine/src/hellfire/core/Application.h b/engine/src/hellfire/core/Application.h index 7f5da35..1c78e85 100644 --- a/engine/src/hellfire/core/Application.h +++ b/engine/src/hellfire/core/Application.h @@ -100,7 +100,6 @@ namespace hellfire { ShaderManager shader_manager_; ShaderRegistry shader_registry_; - Renderer renderer_; // Window info tracking AppInfo window_info_; diff --git a/engine/src/hellfire/core/Project.cpp b/engine/src/hellfire/core/Project.cpp index 4fa8c99..61d415b 100644 --- a/engine/src/hellfire/core/Project.cpp +++ b/engine/src/hellfire/core/Project.cpp @@ -12,6 +12,7 @@ #include "json.hpp" #include "hellfire/assets/AssetManager.h" #include "hellfire/assets/importers/AssetImportManager.h" +#include "hellfire/graphics/renderer/Renderer.h" #include "hellfire/scene/Scene.h" #include "hellfire/serializers/ProjectSerializer.h" #include "hellfire/utilities/ServiceLocator.h" @@ -160,6 +161,7 @@ namespace hellfire { void Project::initialize_managers() { ServiceLocator::unregister_service(); ServiceLocator::unregister_service(); + ServiceLocator::unregister_service(); // Initialize managers with project root context auto registry_path = project_root_path_ / "settings/assetregistry.json"; @@ -170,6 +172,10 @@ namespace hellfire { asset_manager_ = std::make_unique(*asset_registry_.get()); ServiceLocator::register_service(asset_manager_.get()); + scene_renderer_ = std::make_unique(); + scene_renderer_->init(); // Make sure OpenGL is initialized! + ServiceLocator::register_service(scene_renderer_.get()); + AssetImportManager import_manager(*asset_registry_, *asset_manager_, project_root_path_); import_manager.import_all_pending(); asset_registry_->save(); diff --git a/engine/src/hellfire/core/Project.h b/engine/src/hellfire/core/Project.h index 20d893d..a6af537 100644 --- a/engine/src/hellfire/core/Project.h +++ b/engine/src/hellfire/core/Project.h @@ -8,6 +8,7 @@ #include "hellfire/assets/AssetManager.h" #include "hellfire/assets/AssetRegistry.h" +#include "hellfire/graphics/renderer/Renderer.h" #include "hellfire/scene/SceneManager.h" namespace hellfire { @@ -59,6 +60,7 @@ namespace hellfire { std::unique_ptr scene_manager_ = nullptr; std::unique_ptr asset_registry_ = nullptr; std::unique_ptr asset_manager_ = nullptr; + std::unique_ptr scene_renderer_ = nullptr; std::vector recent_scenes_; diff --git a/engine/src/hellfire/graphics/Mesh.cpp b/engine/src/hellfire/graphics/Mesh.cpp index 68dbbb4..cfb0b8c 100644 --- a/engine/src/hellfire/graphics/Mesh.cpp +++ b/engine/src/hellfire/graphics/Mesh.cpp @@ -13,6 +13,12 @@ namespace hellfire { create_mesh(); } + Mesh::Mesh(const std::vector &vertices, const std::vector &indices, bool defer_build) : vertices(vertices), indices(indices), index_count_(0) { + if (!defer_build) { + create_mesh(); + } + } + void Mesh::bind() const { vao_->bind(); } diff --git a/engine/src/hellfire/graphics/Mesh.h b/engine/src/hellfire/graphics/Mesh.h index 97fd619..860e5da 100644 --- a/engine/src/hellfire/graphics/Mesh.h +++ b/engine/src/hellfire/graphics/Mesh.h @@ -9,8 +9,13 @@ namespace hellfire { class Mesh { public: Mesh(); + Mesh(const std::vector &vertices, const std::vector &indices); + Mesh(const std::vector &vertices, + const std::vector &indices, + bool defer_build); + void cleanup(); void bind() const; @@ -18,6 +23,7 @@ namespace hellfire { void unbind() const; void build(); + bool is_built() const { return vao_ != nullptr; } // mesh data std::vector vertices; @@ -32,9 +38,9 @@ namespace hellfire { int get_index_count() const; private: - std::unique_ptr vao_; - std::unique_ptr vbo_; - std::unique_ptr ibo_; + std::unique_ptr vao_ = nullptr; + std::unique_ptr vbo_ = nullptr; + std::unique_ptr ibo_ = nullptr; int index_count_; diff --git a/engine/src/hellfire/serializers/TextureSerializer.h b/engine/src/hellfire/serializers/TextureSerializer.h index 984cb5e..6edd595 100644 --- a/engine/src/hellfire/serializers/TextureSerializer.h +++ b/engine/src/hellfire/serializers/TextureSerializer.h @@ -20,7 +20,7 @@ namespace hellfire { // Filtering enum class FilterMode { NEAREST, LINEAR, TRILINEAR }; - FilterMode filter = FilterMode::TRILINEAR; + FilterMode filter = FilterMode::LINEAR; // Wrapping enum class WrapMode { REPEAT, CLAMP, MIRROR };