diff --git a/assets/shaders/deferred/vk_geometry_indirect.frag b/assets/shaders/deferred/vk_geometry_indirect.frag new file mode 100644 index 0000000..76fbb56 --- /dev/null +++ b/assets/shaders/deferred/vk_geometry_indirect.frag @@ -0,0 +1,62 @@ +#version 450 + +/** + * Deferred Geometry Pass - GPU-Driven Fragment Shader + * + * v1: shares the default material set (set 1) across every indirect draw. + * Per-object material overrides will return when bindless materials land. + */ + +layout(location = 0) in vec3 v_WorldPos; +layout(location = 1) in vec3 v_Normal; +layout(location = 2) in vec2 v_TexCoord; +layout(location = 3) in vec4 v_Tangent; +layout(location = 4) flat in uint v_ObjectId; + +layout(set = 1, binding = 0) uniform sampler2D u_AlbedoMap; +layout(set = 1, binding = 1) uniform sampler2D u_NormalMap; +layout(set = 1, binding = 2) uniform sampler2D u_ARMMap; + +layout(location = 0) out vec4 gAlbedoMetallic; +layout(location = 1) out vec4 gNormalRoughness; +layout(location = 2) out vec4 gEmissionID; +layout(location = 3) out vec2 gVelocity; + +vec2 encode_octahedron(vec3 n) { + n /= (abs(n.x) + abs(n.y) + abs(n.z)); + if (n.z < 0.0) { + n.xy = (1.0 - abs(n.yx)) * vec2( + n.x >= 0.0 ? 1.0 : -1.0, + n.y >= 0.0 ? 1.0 : -1.0 + ); + } + return n.xy * 0.5 + 0.5; +} + +vec3 apply_normal_map(vec3 normal_sample, vec3 N, vec4 T) { + vec3 ts_normal = normal_sample * 2.0 - 1.0; + vec3 T3 = normalize(T.xyz); + vec3 N3 = normalize(N); + T3 = normalize(T3 - dot(T3, N3) * N3); + vec3 B = cross(N3, T3) * T.w; + mat3 TBN = mat3(T3, B, N3); + return normalize(TBN * ts_normal); +} + +void main() { + vec3 albedo_sample = texture(u_AlbedoMap, v_TexCoord).rgb; + vec3 normal_sample = texture(u_NormalMap, v_TexCoord).rgb; + vec3 arm_sample = texture(u_ARMMap, v_TexCoord).rgb; + + float ao = arm_sample.r; + float roughness = arm_sample.g; + float metallic = arm_sample.b; + + vec3 world_normal = apply_normal_map(normal_sample, v_Normal, v_Tangent); + vec2 encoded_normal = encode_octahedron(world_normal); + + gAlbedoMetallic = vec4(albedo_sample, metallic); + gNormalRoughness = vec4(encoded_normal, roughness, ao); + gEmissionID = vec4(0.0, 0.0, 0.0, 0.0); + gVelocity = vec2(0.0, 0.0); +} diff --git a/assets/shaders/deferred/vk_geometry_indirect.vert b/assets/shaders/deferred/vk_geometry_indirect.vert new file mode 100644 index 0000000..e13cfdf --- /dev/null +++ b/assets/shaders/deferred/vk_geometry_indirect.vert @@ -0,0 +1,61 @@ +#version 450 + +/** + * Deferred Geometry Pass - GPU-Driven Vertex Shader + * + * Same outputs as vk_geometry.vert, but reads the per-object model matrix + * from a storage buffer indexed by gl_BaseInstance (set by the cull shader's + * generated indirect draw command). + */ + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec2 a_TexCoord; +layout(location = 3) in vec4 a_Tangent; +layout(location = 4) in ivec4 a_BoneIds; +layout(location = 5) in vec4 a_BoneWeights; + +// Camera UBO (set 0, binding 0) +layout(set = 0, binding = 0) uniform CameraUBO { + mat4 view; + mat4 projection; + mat4 view_projection; + vec4 camera_position; +} camera; + +// Per-object data SSBO (set 2, binding 0). Layout must match GPUObjectData. +struct GPUObjectData { + mat4 model; + mat4 inverse_transpose_model; + vec4 bounding_sphere; + uint mesh_index; + uint material_index; + uint flags; + uint _padding; +}; + +layout(std430, set = 2, binding = 0) readonly buffer ObjectBuffer { + GPUObjectData objects[]; +}; + +// Outputs to fragment shader +layout(location = 0) out vec3 v_WorldPos; +layout(location = 1) out vec3 v_Normal; +layout(location = 2) out vec2 v_TexCoord; +layout(location = 3) out vec4 v_Tangent; +layout(location = 4) flat out uint v_ObjectId; + +void main() { + GPUObjectData obj = objects[gl_BaseInstance]; + + vec4 worldPos = obj.model * vec4(a_Position, 1.0); + v_WorldPos = worldPos.xyz; + + mat3 normalMatrix = mat3(obj.inverse_transpose_model); + v_Normal = normalize(normalMatrix * a_Normal); + v_TexCoord = a_TexCoord; + v_Tangent = vec4(normalize(normalMatrix * a_Tangent.xyz), a_Tangent.w); + v_ObjectId = gl_BaseInstance; + + gl_Position = camera.view_projection * worldPos; +} diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index fda8bb4..6e424a4 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -62,6 +62,7 @@ set(ENGINE_SOURCES renderer/debug_renderer.cpp renderer/cinematic_camera.cpp renderer/gpu_scene.cpp + renderer/gpu_cull_pass.cpp # Audio audio/audio_engine.cpp diff --git a/engine/renderer/deferred_renderer.cpp b/engine/renderer/deferred_renderer.cpp index 57d5800..cafde02 100644 --- a/engine/renderer/deferred_renderer.cpp +++ b/engine/renderer/deferred_renderer.cpp @@ -1302,6 +1302,51 @@ void DeferredRenderer::create_pipelines() { m_geometry_pipeline = m_device.create_graphics_pipeline(desc); } + // ========================================================================= + // Geometry Pipeline (GPU-driven / indirect) + // ========================================================================= + { + // Object SSBO descriptor set layout (set 2) + rhi::DescriptorSetLayoutDesc obj_layout_desc; + obj_layout_desc.bindings.push_back( + rhi::DescriptorBinding::storage_buffer(0, rhi::ShaderStage::Vertex)); + obj_layout_desc.debug_name = "ObjectSSBOLayout"; + m_object_set_layout = m_device.create_descriptor_set_layout(obj_layout_desc); + + // Pipeline layout: set 0 (camera), set 1 (material), set 2 (object SSBO). + // No push constants — model matrix comes from the SSBO. + rhi::PipelineLayoutDesc pl_desc; + pl_desc.set_layouts.push_back(m_camera_layout.get()); + pl_desc.set_layouts.push_back(m_material_layout.get()); + pl_desc.set_layouts.push_back(m_object_set_layout.get()); + m_geometry_layout_indirect = m_device.create_pipeline_layout(pl_desc); + + auto vs = m_device.create_shader_from_file( + "assets/shaders/deferred/vk_geometry_indirect.vert", rhi::ShaderStage::Vertex, + "GeometryIndirectVert"); + auto fs = m_device.create_shader_from_file( + "assets/shaders/deferred/vk_geometry_indirect.frag", rhi::ShaderStage::Fragment, + "GeometryIndirectFrag"); + + if (vs && fs) { + rhi::GraphicsPipelineDesc desc; + desc.vertex_shader = vs.get(); + desc.fragment_shader = fs.get(); + desc.vertex_layout = rhi::VertexInputLayout::standard_vertex(); + desc.topology = rhi::PrimitiveTopology::TriangleList; + desc.rasterization = rhi::RasterizationState::default_state(); + desc.depth_stencil = rhi::DepthStencilState::default_state(); + desc.blend = rhi::BlendState::disabled(4); + desc.multisample = {}; + desc.layout = m_geometry_layout_indirect.get(); + desc.color_attachment_formats = {rhi::Format::RGBA16_FLOAT, rhi::Format::RGBA16_FLOAT, + rhi::Format::RGBA16_FLOAT, rhi::Format::RG16_FLOAT}; + desc.depth_attachment_format = rhi::Format::D32_FLOAT; + desc.debug_name = "GeometryIndirectPipeline"; + m_geometry_pipeline_indirect = m_device.create_graphics_pipeline(desc); + } + } + // ========================================================================= // Lighting Pipeline // ========================================================================= diff --git a/engine/renderer/deferred_renderer.hpp b/engine/renderer/deferred_renderer.hpp index 752b734..63131eb 100644 --- a/engine/renderer/deferred_renderer.hpp +++ b/engine/renderer/deferred_renderer.hpp @@ -404,6 +404,18 @@ class DeferredRenderer { return m_geometry_layout.get(); } + // GPU-driven indirect rendering accessors. + [[nodiscard]] const rhi::PipelineLayout* get_geometry_layout_indirect() const { + return m_geometry_layout_indirect.get(); + } + [[nodiscard]] const rhi::Pipeline* get_geometry_pipeline_indirect() const { + return m_geometry_pipeline_indirect.get(); + } + [[nodiscard]] const rhi::DescriptorSetLayout* get_object_set_layout() const { + return m_object_set_layout.get(); + } + [[nodiscard]] const rhi::DescriptorSet* get_camera_set() const { return m_camera_set.get(); } + private: // Debug accessors (returning Views) [[nodiscard]] rhi::TextureView* get_gbuffer_albedo() const { @@ -473,6 +485,10 @@ class DeferredRenderer { // Pipelines std::unique_ptr m_geometry_pipeline; std::unique_ptr m_geometry_layout; + // GPU-driven variant: reads model matrix from object SSBO via gl_BaseInstance. + std::unique_ptr m_geometry_pipeline_indirect; + std::unique_ptr m_geometry_layout_indirect; + std::unique_ptr m_object_set_layout; std::unique_ptr m_lighting_pipeline; std::unique_ptr m_lighting_layout; std::unique_ptr m_composite_pipeline; diff --git a/engine/renderer/gpu_cull_pass.cpp b/engine/renderer/gpu_cull_pass.cpp new file mode 100644 index 0000000..fcc6164 --- /dev/null +++ b/engine/renderer/gpu_cull_pass.cpp @@ -0,0 +1,163 @@ +#include "gpu_cull_pass.hpp" + +#include "engine/core/log.hpp" +#include "engine/renderer/gpu_scene.hpp" +#include "engine/rhi/rhi_command_list.hpp" + +namespace hz { + +namespace { + +// Extract 6 frustum planes (left, right, bottom, top, near, far) from a row-major +// view-projection matrix using Gribb-Hartmann. GLM stores matrices column-major, +// so we read the rows by indexing the columns. +std::array extract_frustum_planes(const glm::mat4& vp) { + auto row = [&](int i) { + return glm::vec4(vp[0][i], vp[1][i], vp[2][i], vp[3][i]); + }; + const glm::vec4 r0 = row(0); + const glm::vec4 r1 = row(1); + const glm::vec4 r2 = row(2); + const glm::vec4 r3 = row(3); + + std::array planes{ + r3 + r0, // Left + r3 - r0, // Right + r3 + r1, // Bottom + r3 - r1, // Top + r3 + r2, // Near + r3 - r2, // Far + }; + + for (auto& p : planes) { + const float len = glm::length(glm::vec3(p)); + if (len > 0.0f) p /= len; + } + return planes; +} + +} // namespace + +GpuCullPass::GpuCullPass(rhi::Device& device, const GPUScene& scene) : m_device(device) { + // Descriptor set layout: 4 storage buffers (object, mesh_info, draw_cmd, draw_count) + { + rhi::DescriptorSetLayoutDesc desc; + desc.bindings.push_back(rhi::DescriptorBinding::storage_buffer(0, rhi::ShaderStage::Compute)); + desc.bindings.push_back(rhi::DescriptorBinding::storage_buffer(1, rhi::ShaderStage::Compute)); + desc.bindings.push_back(rhi::DescriptorBinding::storage_buffer(2, rhi::ShaderStage::Compute)); + desc.bindings.push_back(rhi::DescriptorBinding::storage_buffer(3, rhi::ShaderStage::Compute)); + desc.debug_name = "GpuCullSetLayout"; + m_set_layout = device.create_descriptor_set_layout(desc); + } + + // Pipeline layout with push constants + { + rhi::PipelineLayoutDesc desc; + desc.set_layouts = {m_set_layout.get()}; + desc.push_constant_ranges.push_back( + {rhi::ShaderStage::Compute, 0, sizeof(PushConstants)}); + desc.debug_name = "GpuCullPipelineLayout"; + m_pipeline_layout = device.create_pipeline_layout(desc); + } + + // Compute pipeline + auto cs = device.create_shader_from_file("assets/shaders/compute/cull.comp", + rhi::ShaderStage::Compute, "GpuCullCS"); + if (!cs) { + HZ_LOG_ERROR("GpuCullPass: failed to compile cull.comp"); + return; + } + + rhi::ComputePipelineDesc pdesc{}; + pdesc.compute_shader = cs.get(); + pdesc.layout = m_pipeline_layout.get(); + pdesc.debug_name = "GpuCullPipeline"; + m_pipeline = device.create_compute_pipeline(pdesc); + + // Descriptor pool + set + { + rhi::DescriptorPoolDesc desc{}; + desc.pool_sizes = {{rhi::DescriptorType::StorageBuffer, 4}}; + desc.max_sets = 1; + desc.debug_name = "GpuCullPool"; + m_pool = device.create_descriptor_pool(desc); + } + m_set = m_pool->allocate(*m_set_layout); + + rhi::DescriptorWrite writes[4]{ + rhi::DescriptorWrite::storage_buffer(0, *scene.object_buffer()), + rhi::DescriptorWrite::storage_buffer(1, *scene.mesh_info_buffer()), + rhi::DescriptorWrite::storage_buffer(2, *scene.draw_command_buffer()), + rhi::DescriptorWrite::storage_buffer(3, *scene.draw_count_buffer()), + }; + m_set->write({writes, 4}); + + HZ_LOG_INFO("GpuCullPass initialized"); +} + +GpuCullPass::~GpuCullPass() = default; + +void GpuCullPass::execute(rhi::CommandList& cmd, const glm::mat4& view, + const glm::mat4& projection, const GPUScene& scene) { + if (!m_pipeline || scene.object_count() == 0) return; + + HZ_DEBUG_MARKER(cmd, "GPU Cull"); + + // 1. Transition count buffer to CopyDest, then clear to 0. + // Source state may be Undefined (first frame) or IndirectArgument (subsequent frames). + rhi::BufferBarrier to_copy{}; + to_copy.buffer = scene.draw_count_buffer(); + to_copy.old_state = m_count_in_indirect_state ? rhi::ResourceState::IndirectArgument + : rhi::ResourceState::Undefined; + to_copy.new_state = rhi::ResourceState::CopyDest; + cmd.barrier(to_copy); + + cmd.clear_buffer(*scene.draw_count_buffer(), 0, sizeof(u32), 0u); + + // 2. Barrier: CopyDest -> UnorderedAccess (compute reads/writes count atomically) + rhi::BufferBarrier to_uav{}; + to_uav.buffer = scene.draw_count_buffer(); + to_uav.old_state = rhi::ResourceState::CopyDest; + to_uav.new_state = rhi::ResourceState::UnorderedAccess; + cmd.barrier(to_uav); + + // Also transition draw command buffer to UnorderedAccess if it was previously IndirectArgument. + if (m_cmd_in_indirect_state) { + rhi::BufferBarrier to_uav_cmd{}; + to_uav_cmd.buffer = scene.draw_command_buffer(); + to_uav_cmd.old_state = rhi::ResourceState::IndirectArgument; + to_uav_cmd.new_state = rhi::ResourceState::UnorderedAccess; + cmd.barrier(to_uav_cmd); + } + + // 3. Bind pipeline + descriptor set + cmd.bind_pipeline(*m_pipeline); + cmd.bind_descriptor_set(*m_pipeline_layout, 0, *m_set); + + // 4. Push constants (view-proj + planes + count) + PushConstants pc{}; + pc.view_proj = projection * view; + auto planes = extract_frustum_planes(pc.view_proj); + for (int i = 0; i < 6; ++i) pc.frustum_planes[i] = planes[i]; + pc.object_count = scene.object_count(); + cmd.push_constants(*m_pipeline_layout, rhi::ShaderStage::Compute, 0, sizeof(pc), &pc); + + // 5. Dispatch (64 threads per group, ceil-div) + const u32 group_count = (pc.object_count + 63) / 64; + cmd.dispatch(group_count, 1, 1); + + // 6. Barriers: compute writes -> indirect reads (for the upcoming draw_indexed_indirect_count) + rhi::BufferBarrier post_barriers[2]{}; + post_barriers[0].buffer = scene.draw_command_buffer(); + post_barriers[0].old_state = rhi::ResourceState::UnorderedAccess; + post_barriers[0].new_state = rhi::ResourceState::IndirectArgument; + post_barriers[1].buffer = scene.draw_count_buffer(); + post_barriers[1].old_state = rhi::ResourceState::UnorderedAccess; + post_barriers[1].new_state = rhi::ResourceState::IndirectArgument; + cmd.barriers({post_barriers, 2}); + + m_count_in_indirect_state = true; + m_cmd_in_indirect_state = true; +} + +} // namespace hz diff --git a/engine/renderer/gpu_cull_pass.hpp b/engine/renderer/gpu_cull_pass.hpp new file mode 100644 index 0000000..0eebae4 --- /dev/null +++ b/engine/renderer/gpu_cull_pass.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "engine/core/types.hpp" +#include "engine/rhi/rhi_descriptor.hpp" +#include "engine/rhi/rhi_device.hpp" +#include "engine/rhi/rhi_pipeline.hpp" + +#include + +#include + +namespace hz { + +namespace rhi { +class CommandList; +} + +class GPUScene; + +class GpuCullPass { +public: + GpuCullPass(rhi::Device& device, const GPUScene& scene); + ~GpuCullPass(); + + HZ_NON_COPYABLE(GpuCullPass); + HZ_NON_MOVABLE(GpuCullPass); + + void execute(rhi::CommandList& cmd, const glm::mat4& view, const glm::mat4& projection, + const GPUScene& scene); + + [[nodiscard]] bool is_valid() const noexcept { return m_pipeline != nullptr; } + +private: + struct PushConstants { + glm::mat4 view_proj; + glm::vec4 frustum_planes[6]; + u32 object_count; + u32 _pad[3]; + }; + + rhi::Device& m_device; + std::unique_ptr m_set_layout; + std::unique_ptr m_pipeline_layout; + std::unique_ptr m_pipeline; + std::unique_ptr m_pool; + std::unique_ptr m_set; + + // Tracks whether scene buffers are currently in IndirectArgument state from a prior frame. + bool m_count_in_indirect_state{false}; + bool m_cmd_in_indirect_state{false}; +}; + +} // namespace hz diff --git a/engine/renderer/gpu_scene.cpp b/engine/renderer/gpu_scene.cpp index f1646df..2795e54 100644 --- a/engine/renderer/gpu_scene.cpp +++ b/engine/renderer/gpu_scene.cpp @@ -192,6 +192,20 @@ u32 GPUScene::add_object(const glm::mat4& transform, u32 mesh_index, u32 materia return object_index; } +std::unique_ptr +GPUScene::create_object_descriptor_set(const rhi::DescriptorSetLayout& layout) { + if (!m_object_set_pool) { + rhi::DescriptorPoolDesc desc{}; + desc.pool_sizes = {{rhi::DescriptorType::StorageBuffer, 4}}; + desc.max_sets = 4; + desc.debug_name = "GPUScene_ObjectSetPool"; + m_object_set_pool = m_device.create_descriptor_pool(desc); + } + auto set = m_object_set_pool->allocate(layout); + set->write_storage_buffer(0, *m_object_buffer); + return set; +} + void GPUScene::end_frame() { if (m_objects.empty()) { return; diff --git a/engine/renderer/gpu_scene.hpp b/engine/renderer/gpu_scene.hpp index 67e25c5..9909297 100644 --- a/engine/renderer/gpu_scene.hpp +++ b/engine/renderer/gpu_scene.hpp @@ -186,6 +186,13 @@ class GPUScene { [[nodiscard]] u32 object_count() const { return static_cast(m_objects.size()); } [[nodiscard]] u32 mesh_count() const { return static_cast(m_mesh_infos.size()); } + /** + * @brief Allocate a single-binding descriptor set holding the object SSBO at binding 0. + * Caller owns the resulting set. Layout must contain one storage_buffer binding at 0. + */ + [[nodiscard]] std::unique_ptr + create_object_descriptor_set(const rhi::DescriptorSetLayout& layout); + [[nodiscard]] const GPUSceneConfig& config() const { return m_config; } private: @@ -217,6 +224,9 @@ class GPUScene { // Mesh pointer to index mapping (for deduplication) std::unordered_map m_mesh_index_map; + + // Pool for allocating object-SSBO descriptor sets. + std::unique_ptr m_object_set_pool; }; } // namespace hz diff --git a/engine/renderer/mesh.cpp b/engine/renderer/mesh.cpp index 3d4a10b..45054d4 100644 --- a/engine/renderer/mesh.cpp +++ b/engine/renderer/mesh.cpp @@ -15,6 +15,27 @@ namespace hz { Mesh::Mesh(std::vector vertices, std::vector indices) : m_vertices(std::move(vertices)), m_indices(std::move(indices)) {} +glm::vec4 Mesh::bounding_sphere() const { + if (m_bounds_cached) return m_bounding_sphere; + if (m_vertices.empty()) { + m_bounding_sphere = glm::vec4(0.0f); + m_bounds_cached = true; + return m_bounding_sphere; + } + // Centroid then max-distance radius. Cheap and good enough for frustum culling. + glm::vec3 center(0.0f); + for (const auto& v : m_vertices) center += v.position; + center /= static_cast(m_vertices.size()); + float r2 = 0.0f; + for (const auto& v : m_vertices) { + const glm::vec3 d = v.position - center; + r2 = std::max(r2, glm::dot(d, d)); + } + m_bounding_sphere = glm::vec4(center, std::sqrt(r2)); + m_bounds_cached = true; + return m_bounding_sphere; +} + // ============================================================================ // GPU Resource Management // ============================================================================ diff --git a/engine/renderer/mesh.hpp b/engine/renderer/mesh.hpp index 3fe8995..326627f 100644 --- a/engine/renderer/mesh.hpp +++ b/engine/renderer/mesh.hpp @@ -131,6 +131,17 @@ class Mesh { [[nodiscard]] rhi::Buffer* vertex_buffer() const { return m_vertex_buffer.get(); } [[nodiscard]] rhi::Buffer* index_buffer() const { return m_index_buffer.get(); } + // ========================================================================= + // GPU-Driven Rendering + // ========================================================================= + + /// Local-space bounding sphere (xyz=center, w=radius). Computed once on first access. + [[nodiscard]] glm::vec4 bounding_sphere() const; + + /// Index assigned by GPUScene::register_mesh; UINT32_MAX if unregistered. + [[nodiscard]] u32 gpu_mesh_index() const { return m_gpu_mesh_index; } + void set_gpu_mesh_index(u32 idx) const { m_gpu_mesh_index = idx; } + private: std::vector m_vertices; std::vector m_indices; @@ -138,6 +149,11 @@ class Mesh { // GPU buffers (created by upload_to_gpu) std::unique_ptr m_vertex_buffer; std::unique_ptr m_index_buffer; + + // GPU-driven rendering: lazy-cached bounds and registry index. + mutable glm::vec4 m_bounding_sphere{0.0f}; + mutable bool m_bounds_cached{false}; + mutable u32 m_gpu_mesh_index{UINT32_MAX}; }; } // namespace hz diff --git a/game/src/application.cpp b/game/src/application.cpp index 83a5229..c374ef7 100644 --- a/game/src/application.cpp +++ b/game/src/application.cpp @@ -66,6 +66,9 @@ bool Application::init_window() { m_imgui = std::make_unique(); m_imgui->init(*m_window); + // Apply professional dark editor theme + EditorUI::apply_dark_theme(); + return true; } @@ -94,6 +97,10 @@ bool Application::init_renderer() { m_debug_renderer = std::make_unique(); + // GPU-driven rendering: scene buffers + culling pass + m_gpu_scene = std::make_unique(*m_device); + m_cull_pass = std::make_unique(*m_device, *m_gpu_scene); + // 4. Initialize Synchronization primitives for (unsigned int i = 0; i < APP_MAX_FRAMES_IN_FLIGHT; ++i) { m_image_available_sems[i] = m_device->create_semaphore(); @@ -211,6 +218,7 @@ void Application::setup_scene_entities() { // Player auto player_entity = m_scene->create_entity(); { + m_scene->registry().emplace(player_entity, "Player Camera"); auto& tc = m_scene->registry().emplace(player_entity); tc.position = glm::vec3(0.0f, 2.0f, 6.0f); tc.rotation = glm::vec3(-10.0f, 0.0f, 0.0f); @@ -221,6 +229,7 @@ void Application::setup_scene_entities() { // Ground Plane if (m_plane_handle.is_valid()) { auto plane = m_scene->create_entity(); + m_scene->registry().emplace(plane, "Ground Plane"); auto& tc = m_scene->registry().emplace(plane); tc.position = glm::vec3(0.0f, -1.0f, 0.0f); tc.scale = glm::vec3(1.0f); @@ -243,6 +252,7 @@ void Application::setup_scene_entities() { // Test Cube if (m_cube_handle.is_valid()) { auto cube = m_scene->create_entity(); + m_scene->registry().emplace(cube, "Test Cube"); auto& tc = m_scene->registry().emplace(cube); tc.position = glm::vec3(-2.0f, 10.0f, 0.0f); // Higher up to fall @@ -265,6 +275,7 @@ void Application::setup_scene_entities() { // Test Sphere if (m_sphere_handle.is_valid()) { auto sphere = m_scene->create_entity(); + m_scene->registry().emplace(sphere, "Test Sphere"); auto& tc = m_scene->registry().emplace(sphere); tc.position = glm::vec3(2.0f, 10.0f, 0.0f); // Higher up @@ -287,6 +298,7 @@ void Application::setup_scene_entities() { // Point Light auto light = m_scene->create_entity(); { + m_scene->registry().emplace(light, "Point Light"); auto& tc = m_scene->registry().emplace(light); tc.position = glm::vec3(0.0f, 3.0f, 2.0f); @@ -328,6 +340,84 @@ void Application::on_update(float dt) { m_player_system.update(*m_scene, *m_input, *m_window, dt); m_animation_system.update(*m_scene, dt); + // F1 toggle for editor + bool f1_now = glfwGetKey(m_window->native_handle(), GLFW_KEY_F1) == GLFW_PRESS; + if (f1_now && !m_f1_held) { + m_show_editor = !m_show_editor; + } + m_f1_held = f1_now; + + // F2 toggle for GPU-driven culling (A/B against legacy CPU draw loop) + bool f2_now = glfwGetKey(m_window->native_handle(), GLFW_KEY_F2) == GLFW_PRESS; + if (f2_now && !m_f2_held) { + m_use_gpu_culling = !m_use_gpu_culling; + HZ_LOG_INFO("GPU culling: {}", m_use_gpu_culling ? "ON" : "OFF"); + } + m_f2_held = f2_now; + + // Handle entity preset requests from editor + if (m_editor.should_add_cube() && m_cube_handle.is_valid()) { + auto e = m_scene->create_entity(); + m_scene->registry().emplace(e, "New Cube"); + auto& tc = m_scene->registry().emplace(e); + tc.position = glm::vec3(0.0f, 5.0f, 0.0f); + auto& mc = m_scene->registry().emplace(e); + mc.mesh_type = hz::MeshComponent::MeshType::Model; + mc.model = m_cube_handle; + mc.primitive_name = "cube"; + auto& rb = m_scene->registry().emplace(e); + rb.type = hz::RigidBodyComponent::BodyType::Dynamic; + rb.mass = 5.0f; + auto& bc = m_scene->registry().emplace(e); + bc.half_extents = glm::vec3(0.5f); + } + if (m_editor.should_add_sphere() && m_sphere_handle.is_valid()) { + auto e = m_scene->create_entity(); + m_scene->registry().emplace(e, "New Sphere"); + auto& tc = m_scene->registry().emplace(e); + tc.position = glm::vec3(0.0f, 5.0f, 0.0f); + auto& mc = m_scene->registry().emplace(e); + mc.mesh_type = hz::MeshComponent::MeshType::Model; + mc.model = m_sphere_handle; + mc.primitive_name = "sphere"; + auto& rb = m_scene->registry().emplace(e); + rb.type = hz::RigidBodyComponent::BodyType::Dynamic; + rb.mass = 5.0f; + auto& sc = m_scene->registry().emplace(e); + sc.radius = 1.0f; + } + if (m_editor.should_add_light()) { + auto e = m_scene->create_entity(); + m_scene->registry().emplace(e, "New Light"); + auto& tc = m_scene->registry().emplace(e); + tc.position = glm::vec3(0.0f, 3.0f, 0.0f); + auto& lc = m_scene->registry().emplace(e); + lc.type = hz::LightType::Point; + lc.intensity = 10.0f; + lc.range = 20.0f; + } + if (m_editor.should_add_camera()) { + auto e = m_scene->create_entity(); + m_scene->registry().emplace(e, "New Camera"); + auto& tc = m_scene->registry().emplace(e); + tc.position = glm::vec3(0.0f, 2.0f, 5.0f); + auto& cc = m_scene->registry().emplace(e); + cc.primary = false; + } + if (m_editor.should_add_empty()) { + auto e = m_scene->create_entity(); + m_scene->registry().emplace(e, "New Entity"); + m_scene->registry().emplace(e); + } + { + hz::Entity del_entity; + if (m_editor.should_delete_entity(del_entity)) { + if (m_scene->is_valid(del_entity)) { + m_scene->destroy_entity(del_entity); + } + } + } + if (m_input->is_action_just_pressed(hz::InputManager::ACTION_MENU)) { m_window->close(); } @@ -424,29 +514,79 @@ void Application::on_render([[maybe_unused]] float alpha) { m_renderer->end_shadow_pass(*cmd); } - // === Geometry Pass === - m_renderer->begin_geometry_pass(*cmd, camera); + // === GPU Scene submission + Cull === + if (m_use_gpu_culling && m_gpu_scene && m_cull_pass) { + m_gpu_scene->begin_frame(); + auto group = + m_scene->registry().group(entt::get); + for (auto entity : group) { + auto [tc, mc] = group.get(entity); + const hz::Mesh* mesh = resolve_mesh(mc); + if (!mesh || !mesh->is_uploaded()) continue; + // Lazy-register: every Mesh enters the GPUScene mega-buffer once. + if (mesh->gpu_mesh_index() == UINT32_MAX) { + const hz::u32 idx = m_gpu_scene->register_mesh(*mesh); + mesh->set_gpu_mesh_index(idx); + } + if (mesh->gpu_mesh_index() == UINT32_MAX) continue; // capacity exceeded + const hz::u32 flags = + static_cast(hz::GPUObjectFlags::Visible) | + (mc.cast_shadows ? static_cast(hz::GPUObjectFlags::CastShadow) : 0u); + m_gpu_scene->add_object(tc.get_transform(), mesh->gpu_mesh_index(), 0u, + mesh->bounding_sphere(), + static_cast(flags)); + } + m_gpu_scene->end_frame(); - // Bind default material set (Set 1) - if (m_default_material_set) { - cmd->bind_descriptor_set(*m_renderer->get_geometry_layout(), 1, *m_default_material_set); + const glm::mat4 view = camera.view_matrix(); + const hz::u32 fb_w = m_swapchain->width(); + const hz::u32 fb_h = m_swapchain->height(); + const float aspect = (fb_h > 0) ? static_cast(fb_w) / static_cast(fb_h) : 1.0f; + const glm::mat4 proj = camera.projection_matrix(aspect); + m_cull_pass->execute(*cmd, view, proj, *m_gpu_scene); } - // Render entities - { + // === Geometry Pass === + m_renderer->begin_geometry_pass(*cmd, camera); + + if (m_use_gpu_culling && m_gpu_scene && m_renderer->get_geometry_pipeline_indirect()) { + // GPU-driven path: single indirect draw using cull-shader output. + if (!m_object_set && m_renderer->get_object_set_layout()) { + m_object_set = + m_gpu_scene->create_object_descriptor_set(*m_renderer->get_object_set_layout()); + } + const auto* layout = m_renderer->get_geometry_layout_indirect(); + cmd->bind_pipeline(*m_renderer->get_geometry_pipeline_indirect()); + if (auto* cam = m_renderer->get_camera_set()) { + cmd->bind_descriptor_set(*layout, 0, *cam); + } + if (m_default_material_set) { + cmd->bind_descriptor_set(*layout, 1, *m_default_material_set); + } + if (m_object_set) { + cmd->bind_descriptor_set(*layout, 2, *m_object_set); + } + cmd->bind_vertex_buffer(0, *m_gpu_scene->vertex_buffer()); + cmd->bind_index_buffer(*m_gpu_scene->index_buffer(), 0, hz::rhi::IndexType::Uint32); + cmd->draw_indexed_indirect_count(*m_gpu_scene->draw_command_buffer(), 0, + *m_gpu_scene->draw_count_buffer(), 0, + m_gpu_scene->object_count(), + sizeof(hz::GPUDrawCommand)); + } else { + // Legacy CPU-driven path + if (m_default_material_set) { + cmd->bind_descriptor_set(*m_renderer->get_geometry_layout(), 1, + *m_default_material_set); + } auto group = m_scene->registry().group(entt::get); for (auto entity : group) { auto [tc, mc] = group.get(entity); - const hz::Mesh* mesh = resolve_mesh(mc); if (mesh) { - // Push Model Matrix (Vertex Stage, Offset 0) glm::mat4 model = tc.get_transform(); cmd->push_constants(*m_renderer->get_geometry_layout(), hz::rhi::ShaderStage::Vertex, model, 0); - - // Push Material Params (Fragment Stage, Offset 64) struct MatParams { glm::vec4 albedo; float roughness; @@ -455,11 +595,8 @@ void Application::on_render([[maybe_unused]] float alpha) { mat_params.albedo = glm::vec4(mc.albedo_color, 1.0f); mat_params.roughness = mc.roughness; mat_params.metallic = mc.metallic; - cmd->push_constants(*m_renderer->get_geometry_layout(), hz::rhi::ShaderStage::Fragment, mat_params, 64); - - // Draw mesh->draw(*cmd); } } @@ -482,9 +619,10 @@ void Application::on_render([[maybe_unused]] float alpha) { // === UI === m_imgui->begin_frame(); - ImGui::Begin("PBR Test (Vulkan)"); - ImGui::Text("Profiling: %.2f ms", 1000.0f / ImGui::GetIO().Framerate); - ImGui::End(); + if (m_show_editor) { + m_editor.draw(*m_scene, m_scene_settings, ImGui::GetIO().Framerate, + m_scene->entity_count(), m_renderer->get_stats()); + } m_imgui->end_frame(); cmd->end(); diff --git a/game/src/application.hpp b/game/src/application.hpp index 2a58484..d0ddc14 100644 --- a/game/src/application.hpp +++ b/game/src/application.hpp @@ -5,7 +5,9 @@ * @brief Main application class that orchestrates all game systems */ +#include "editor_ui.hpp" #include "game_config.hpp" +#include "scene_settings.hpp" #include "systems/animation_system.hpp" #include "systems/character_system.hpp" #include "systems/lifetime_system.hpp" @@ -27,6 +29,8 @@ #include #include #include +#include +#include #include #include #include @@ -85,6 +89,12 @@ class Application { std::unique_ptr m_assets; std::unique_ptr m_ibl; + // GPU-driven rendering + std::unique_ptr m_gpu_scene; + std::unique_ptr m_cull_pass; + std::unique_ptr m_object_set; + bool m_use_gpu_culling{true}; + // Game systems PlayerSystem m_player_system; PhysicsSystem m_physics_system; @@ -110,6 +120,13 @@ class Application { // Default Material Set std::unique_ptr m_default_material_set; + // Editor + EditorUI m_editor; + SceneSettings m_scene_settings; + bool m_show_editor{true}; + bool m_f1_held{false}; + bool m_f2_held{false}; + // UI state bool m_show_grid{false}; bool m_show_model{true}; diff --git a/game/src/editor_ui.cpp b/game/src/editor_ui.cpp index c7dba3f..53beffe 100644 --- a/game/src/editor_ui.cpp +++ b/game/src/editor_ui.cpp @@ -1,54 +1,139 @@ +/** + * @file editor_ui.cpp + * @brief Full Scene Hierarchy Editor & Property Inspector implementation + */ + #include "editor_ui.hpp" +#include +#include + #include namespace game { -void EditorUI::draw(hz::Scene& scene, SceneSettings& settings, float fps, size_t entity_count) { - // Get viewport size for positioning +// ============================================================================ +// Dark Theme +// ============================================================================ + +void EditorUI::apply_dark_theme() { + ImGuiStyle& style = ImGui::GetStyle(); + + // Rounding + style.WindowRounding = 4.0f; + style.ChildRounding = 4.0f; + style.FrameRounding = 3.0f; + style.PopupRounding = 4.0f; + style.ScrollbarRounding = 6.0f; + style.GrabRounding = 3.0f; + style.TabRounding = 4.0f; + + // Sizing + style.WindowPadding = ImVec2(10, 10); + style.FramePadding = ImVec2(6, 4); + style.ItemSpacing = ImVec2(8, 5); + style.ItemInnerSpacing = ImVec2(6, 4); + style.IndentSpacing = 18.0f; + style.ScrollbarSize = 14.0f; + style.GrabMinSize = 10.0f; + + // Borders + style.WindowBorderSize = 1.0f; + style.ChildBorderSize = 1.0f; + style.FrameBorderSize = 0.0f; + + // Colors — professional dark grey with blue accent + ImVec4* c = style.Colors; + + // Backgrounds + c[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.11f, 0.13f, 1.00f); + c[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f); + c[ImGuiCol_PopupBg] = ImVec4(0.12f, 0.12f, 0.14f, 0.96f); + + // Borders + c[ImGuiCol_Border] = ImVec4(0.22f, 0.22f, 0.26f, 1.00f); + c[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + + // Frame (input fields, sliders etc) + c[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.16f, 0.19f, 1.00f); + c[ImGuiCol_FrameBgHovered] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f); + c[ImGuiCol_FrameBgActive] = ImVec4(0.24f, 0.24f, 0.29f, 1.00f); + + // Title bar + c[ImGuiCol_TitleBg] = ImVec4(0.08f, 0.08f, 0.10f, 1.00f); + c[ImGuiCol_TitleBgActive] = ImVec4(0.10f, 0.10f, 0.13f, 1.00f); + c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.08f, 0.08f, 0.10f, 0.50f); + + // Menu bar + c[ImGuiCol_MenuBarBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f); + + // Scrollbar + c[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f); + c[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.25f, 0.30f, 1.00f); + c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.30f, 0.30f, 0.36f, 1.00f); + c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.35f, 0.35f, 0.42f, 1.00f); + + // Buttons (accent blue) + c[ImGuiCol_Button] = ImVec4(0.22f, 0.35f, 0.55f, 1.00f); + c[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.42f, 0.65f, 1.00f); + c[ImGuiCol_ButtonActive] = ImVec4(0.18f, 0.30f, 0.48f, 1.00f); + + // Header (collapsing headers, tree nodes) + c[ImGuiCol_Header] = ImVec4(0.18f, 0.18f, 0.22f, 1.00f); + c[ImGuiCol_HeaderHovered] = ImVec4(0.22f, 0.35f, 0.55f, 0.80f); + c[ImGuiCol_HeaderActive] = ImVec4(0.22f, 0.35f, 0.55f, 1.00f); + + // Separator + c[ImGuiCol_Separator] = ImVec4(0.22f, 0.22f, 0.26f, 1.00f); + c[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.45f, 0.65f, 0.78f); + c[ImGuiCol_SeparatorActive] = ImVec4(0.30f, 0.45f, 0.65f, 1.00f); + + // Resize grip + c[ImGuiCol_ResizeGrip] = ImVec4(0.22f, 0.35f, 0.55f, 0.25f); + c[ImGuiCol_ResizeGripHovered] = ImVec4(0.22f, 0.35f, 0.55f, 0.67f); + c[ImGuiCol_ResizeGripActive] = ImVec4(0.22f, 0.35f, 0.55f, 0.95f); + + // Tabs + c[ImGuiCol_Tab] = ImVec4(0.14f, 0.14f, 0.17f, 1.00f); + c[ImGuiCol_TabHovered] = ImVec4(0.22f, 0.35f, 0.55f, 0.80f); + c[ImGuiCol_TabActive] = ImVec4(0.22f, 0.35f, 0.55f, 1.00f); + c[ImGuiCol_TabUnfocused] = ImVec4(0.12f, 0.12f, 0.15f, 1.00f); + c[ImGuiCol_TabUnfocusedActive] = ImVec4(0.18f, 0.18f, 0.22f, 1.00f); + + // Checkboxes / slider grab + c[ImGuiCol_CheckMark] = ImVec4(0.40f, 0.65f, 1.00f, 1.00f); + c[ImGuiCol_SliderGrab] = ImVec4(0.30f, 0.50f, 0.78f, 1.00f); + c[ImGuiCol_SliderGrabActive] = ImVec4(0.40f, 0.60f, 0.88f, 1.00f); + + // Text + c[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.92f, 1.00f); + c[ImGuiCol_TextDisabled] = ImVec4(0.45f, 0.45f, 0.50f, 1.00f); + c[ImGuiCol_TextSelectedBg] = ImVec4(0.22f, 0.35f, 0.55f, 0.43f); + + // Drag/Drop + c[ImGuiCol_DragDropTarget] = ImVec4(0.40f, 0.65f, 1.00f, 0.90f); +} + +// ============================================================================ +// Main Draw +// ============================================================================ + +void EditorUI::draw(hz::Scene& scene, SceneSettings& settings, float fps, size_t entity_count, + const hz::RenderStats& render_stats) { ImGuiIO& io = ImGui::GetIO(); float display_w = io.DisplaySize.x; float display_h = io.DisplaySize.y; - // Panel dimensions - const float hierarchy_width = 220.0f; - const float inspector_width = 300.0f; - const float menu_bar_height = 20.0f; // Approximate + const float hierarchy_width = 240.0f; + const float inspector_width = 320.0f; + const float menu_bar_height = 20.0f; - // Menu bar at top - if (ImGui::BeginMainMenuBar()) { - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Save Scene (Todo)")) { - } - if (ImGui::MenuItem("Load Scene (Todo)")) { - } - ImGui::Separator(); - if (ImGui::MenuItem("Exit (Todo)")) { - } - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("View")) { - ImGui::MenuItem("Hierarchy", nullptr, &m_show_hierarchy); - ImGui::MenuItem("Inspector", nullptr, &m_show_inspector); - ImGui::MenuItem("Scene Settings", nullptr, &m_show_settings); - ImGui::MenuItem("Stats", nullptr, &m_show_stats); - ImGui::MenuItem("Console", nullptr, &m_show_console); - ImGui::MenuItem("Toolbar", nullptr, &m_show_toolbar); - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Help")) { - if (ImGui::MenuItem("Controls")) { - } - ImGui::EndMenu(); - } - ImGui::EndMainMenuBar(); - } + draw_menu_bar(); if (m_show_toolbar) { draw_toolbar(display_w, menu_bar_height); } - // Adjust height for toolbar if visible float top_offset = menu_bar_height + (m_show_toolbar ? 40.0f : 0.0f); if (m_show_hierarchy) { @@ -64,7 +149,7 @@ void EditorUI::draw(hz::Scene& scene, SceneSettings& settings, float fps, size_t } if (m_show_stats) { - draw_stats(fps, entity_count, display_w, top_offset); + draw_stats(fps, entity_count, render_stats, display_w, top_offset); } if (m_show_console) { @@ -72,36 +157,139 @@ void EditorUI::draw(hz::Scene& scene, SceneSettings& settings, float fps, size_t } } +// ============================================================================ +// Menu Bar +// ============================================================================ + +void EditorUI::draw_menu_bar() { + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Save Scene", "Ctrl+S")) { /* TODO */ + } + if (ImGui::MenuItem("Load Scene", "Ctrl+O")) { /* TODO */ + } + ImGui::Separator(); + if (ImGui::MenuItem("Exit", "Esc")) { /* TODO */ + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) { + if (ImGui::MenuItem("Undo", "Ctrl+Z", false, false)) { /* TODO */ + } + if (ImGui::MenuItem("Redo", "Ctrl+Y", false, false)) { /* TODO */ + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("View")) { + ImGui::MenuItem("Hierarchy", "H", &m_show_hierarchy); + ImGui::MenuItem("Inspector", "I", &m_show_inspector); + ImGui::MenuItem("Scene Settings", nullptr, &m_show_settings); + ImGui::MenuItem("Stats", nullptr, &m_show_stats); + ImGui::MenuItem("Console", "`", &m_show_console); + ImGui::MenuItem("Toolbar", nullptr, &m_show_toolbar); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Create")) { + if (ImGui::MenuItem("Empty Entity")) { + m_req_add_empty = true; + } + ImGui::Separator(); + if (ImGui::MenuItem("Cube")) { + m_req_add_cube = true; + } + if (ImGui::MenuItem("Sphere")) { + m_req_add_sphere = true; + } + ImGui::Separator(); + if (ImGui::MenuItem("Point Light")) { + m_req_add_light = true; + } + if (ImGui::MenuItem("Camera")) { + m_req_add_camera = true; + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Help")) { + if (ImGui::MenuItem("Controls")) { /* TODO */ + } + if (ImGui::MenuItem("About")) { /* TODO */ + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } +} + +// ============================================================================ +// Toolbar +// ============================================================================ + void EditorUI::draw_toolbar(float display_w, float menu_bar_height) { ImGui::SetNextWindowPos(ImVec2(0, menu_bar_height)); ImGui::SetNextWindowSize(ImVec2(display_w, 40.0f)); ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoScrollWithMouse; + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoBringToFrontOnFocus; - // Style for toolbar - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.09f, 0.09f, 0.11f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 6)); - if (ImGui::Begin("Toolbar", &m_show_toolbar, flags)) { - if (ImGui::Button("Save")) { /* Todo */ + if (ImGui::Begin("##Toolbar", nullptr, flags)) { + // File operations + if (ImGui::Button("Save")) { /* TODO */ } ImGui::SameLine(); - if (ImGui::Button("Load")) { /* Todo */ + if (ImGui::Button("Load")) { /* TODO */ } + ImGui::SameLine(); - ImGui::Separator(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + + // Entity creation + if (ImGui::Button("+ Cube")) { + m_req_add_cube = true; + } + ImGui::SameLine(); + if (ImGui::Button("+ Sphere")) { + m_req_add_sphere = true; + } + ImGui::SameLine(); + if (ImGui::Button("+ Light")) { + m_req_add_light = true; + } ImGui::SameLine(); - if (ImGui::Button("Add Cube")) { - m_add_cube_requested = true; + if (ImGui::Button("+ Camera")) { + m_req_add_camera = true; } ImGui::SameLine(); - if (ImGui::Button("Add Light")) { - m_add_light_requested = true; + if (ImGui::Button("+ Empty")) { + m_req_add_empty = true; } } ImGui::End(); + ImGui::PopStyleVar(); ImGui::PopStyleColor(); } +// ============================================================================ +// Entity Icon Helper +// ============================================================================ + +const char* EditorUI::entity_icon(const entt::registry& reg, hz::Entity entity) { + if (reg.any_of(entity)) + return "[C] "; + if (reg.any_of(entity)) + return "[L] "; + if (reg.any_of(entity)) + return "[M] "; + return " "; +} + +// ============================================================================ +// Hierarchy Panel +// ============================================================================ + void EditorUI::draw_hierarchy(hz::Scene& scene, float display_h, float top_offset, float width) { ImGui::SetNextWindowPos(ImVec2(0, top_offset), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(width, display_h - top_offset), ImGuiCond_Always); @@ -110,42 +298,133 @@ void EditorUI::draw_hierarchy(hz::Scene& scene, float display_h, float top_offse ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Hierarchy", &m_show_hierarchy, flags)) { - // iterate all entities - auto view = scene.registry().view(); - view.each([&](auto entity) { - std::string name = "Entity " + std::to_string(static_cast(entity)); + // --- Search bar --- + ImGui::PushItemWidth(-1); + ImGui::InputTextWithHint("##Search", "Search entities...", m_search_buffer, + sizeof(m_search_buffer)); + ImGui::PopItemWidth(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // --- Entity list --- + std::string search_lower; + if (m_search_buffer[0] != '\0') { + search_lower = m_search_buffer; + std::transform(search_lower.begin(), search_lower.end(), search_lower.begin(), + [](char c) { return static_cast(std::tolower(c)); }); + } + + bool any_visible = false; + + // Iterate all entities using the same pattern as the serializer + auto entity_view = scene.registry().view(); + entity_view.each([&](auto entity) { + if (!scene.registry().valid(entity)) + return; // continue in lambda + + // Get display name + std::string name; if (auto* tag = scene.registry().try_get(entity)) { name = tag->tag; + } else { + name = "Entity " + std::to_string(static_cast(entity)); + } + + // Filter by search + if (!search_lower.empty()) { + std::string name_lower = name; + std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(), + [](char c) { return static_cast(std::tolower(c)); }); + if (name_lower.find(search_lower) == std::string::npos) + return; // continue in lambda + } + + any_visible = true; + + // Icon prefix + const char* icon = entity_icon(scene.registry(), entity); + + // Build display string with icon + std::string display = std::string(icon) + name; + + // Tree node flags + ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_NoTreePushOnOpen | + ImGuiTreeNodeFlags_SpanAvailWidth; + if (m_selected_entity == entity) { + node_flags |= ImGuiTreeNodeFlags_Selected; } - bool is_selected = (m_selected_entity == entity); - if (ImGui::Selectable(name.c_str(), is_selected)) { + ImGui::PushID(static_cast(static_cast(entity))); + ImGui::TreeNodeEx(display.c_str(), node_flags); + + // Selection + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { m_selected_entity = entity; } - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Delete")) { - scene.destroy_entity(entity); + // Context menu + if (ImGui::BeginPopupContextItem("EntityContextMenu")) { + if (ImGui::MenuItem("Duplicate")) { + // TODO: implement entity duplication + } + ImGui::Separator(); + if (ImGui::MenuItem("Delete", "Del")) { + m_req_delete = true; + m_req_delete_target = entity; if (m_selected_entity == entity) { m_selected_entity = entt::null; } } ImGui::EndPopup(); } + + ImGui::PopID(); }); + if (!any_visible && m_search_buffer[0] != '\0') { + ImGui::TextDisabled("No matching entities"); + } + + ImGui::Spacing(); ImGui::Separator(); + ImGui::Spacing(); + // --- Create entity dropdown --- if (ImGui::Button("+ Add Entity", ImVec2(-1, 0))) { - auto new_entity = scene.create_entity(); - scene.registry().emplace(new_entity, "New Entity"); - scene.registry().emplace(new_entity); - m_selected_entity = new_entity; + ImGui::OpenPopup("AddEntityPopup"); + } + + if (ImGui::BeginPopup("AddEntityPopup")) { + if (ImGui::MenuItem("Empty Entity")) { + m_req_add_empty = true; + } + ImGui::Separator(); + if (ImGui::MenuItem("Cube")) { + m_req_add_cube = true; + } + if (ImGui::MenuItem("Sphere")) { + m_req_add_sphere = true; + } + ImGui::Separator(); + if (ImGui::MenuItem("Point Light")) { + m_req_add_light = true; + } + if (ImGui::MenuItem("Camera")) { + m_req_add_camera = true; + } + ImGui::EndPopup(); } } ImGui::End(); } +// ============================================================================ +// Inspector Panel +// ============================================================================ + void EditorUI::draw_inspector(hz::Scene& scene, float display_w, float display_h, float top_offset, float width) { ImGui::SetNextWindowPos(ImVec2(display_w - width, top_offset), ImGuiCond_Always); @@ -157,6 +436,7 @@ void EditorUI::draw_inspector(hz::Scene& scene, float display_w, float display_h if (ImGui::Begin("Inspector", &m_show_inspector, flags)) { if (!has_selection()) { ImGui::TextDisabled("No entity selected"); + ImGui::TextDisabled("Select an entity in the Hierarchy panel"); ImGui::End(); return; } @@ -164,144 +444,558 @@ void EditorUI::draw_inspector(hz::Scene& scene, float display_w, float display_h hz::Entity entity = m_selected_entity; if (!scene.registry().valid(entity)) { m_selected_entity = entt::null; + ImGui::TextDisabled("Selected entity no longer exists"); ImGui::End(); return; } - // Tag Component + // Entity header with ID + ImGui::Text("Entity #%u", static_cast(entity)); + ImGui::Separator(); + ImGui::Spacing(); + + // ------ TagComponent ------ if (auto* tag = scene.registry().try_get(entity)) { - if (ImGui::CollapsingHeader("Tag", ImGuiTreeNodeFlags_DefaultOpen)) { - char buffer[256]; - strncpy(buffer, tag->tag.c_str(), sizeof(buffer) - 1); - buffer[sizeof(buffer) - 1] = '\0'; - if (ImGui::InputText("Name", buffer, sizeof(buffer))) { - tag->tag = buffer; - } - } + draw_tag_section(*tag); } - // Transform Component + // ------ TransformComponent ------ if (auto* transform = scene.registry().try_get(entity)) { - if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::DragFloat3("Position", &transform->position.x, 0.1f); - ImGui::DragFloat3("Rotation", &transform->rotation.x, 1.0f); - ImGui::DragFloat3("Scale", &transform->scale.x, 0.1f, 0.01f, 100.0f); + bool remove = false; + if (begin_component_section("Transform", "##TransformSection", false, remove)) { + draw_transform_section(*transform); } } - // Mesh Component + // ------ MeshComponent ------ if (auto* mesh = scene.registry().try_get(entity)) { - if (ImGui::CollapsingHeader("Mesh", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("Mesh: %s", mesh->mesh_path.c_str()); - ImGui::Separator(); - ImGui::Text("Material"); - ImGui::ColorEdit3("Albedo", &mesh->albedo_color.x); - ImGui::SliderFloat("Metallic", &mesh->metallic, 0.0f, 1.0f); - ImGui::SliderFloat("Roughness", &mesh->roughness, 0.0f, 1.0f); + bool remove = false; + if (begin_component_section("Mesh", "##MeshSection", true, remove)) { + draw_mesh_section(*mesh); } + if (remove) + scene.registry().remove(entity); } - // Light Component + // ------ LightComponent ------ if (auto* light = scene.registry().try_get(entity)) { - if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::ColorEdit3("Color", &light->color.x); - ImGui::DragFloat("Intensity", &light->intensity, 0.1f, 0.0f, 100.0f); - ImGui::DragFloat("Range", &light->range, 0.5f, 0.0f, 500.0f); + bool remove = false; + if (begin_component_section("Light", "##LightSection", true, remove)) { + draw_light_section(*light); } + if (remove) + scene.registry().remove(entity); } - ImGui::Separator(); + // ------ CameraComponent ------ + if (auto* cam = scene.registry().try_get(entity)) { + bool remove = false; + if (begin_component_section("Camera", "##CameraSection", true, remove)) { + draw_camera_section(*cam); + } + if (remove) + scene.registry().remove(entity); + } - if (ImGui::Button("+ Add Component", ImVec2(-1, 0))) { - ImGui::OpenPopup("AddComponentPopup"); + // ------ RigidBodyComponent ------ + if (auto* rb = scene.registry().try_get(entity)) { + bool remove = false; + if (begin_component_section("Rigid Body", "##RBSection", true, remove)) { + draw_rigidbody_section(*rb); + } + if (remove) + scene.registry().remove(entity); } - if (ImGui::BeginPopup("AddComponentPopup")) { - if (!scene.registry().try_get(entity)) { - if (ImGui::MenuItem("Mesh Component")) { - scene.registry().emplace(entity); - } + // ------ BoxColliderComponent ------ + if (auto* bc = scene.registry().try_get(entity)) { + bool remove = false; + if (begin_component_section("Box Collider", "##BoxColSection", true, remove)) { + draw_box_collider_section(*bc); } - if (!scene.registry().try_get(entity)) { - if (ImGui::MenuItem("Light Component")) { - scene.registry().emplace(entity); - } + if (remove) + scene.registry().remove(entity); + } + + // ------ SphereColliderComponent ------ + if (auto* sc = scene.registry().try_get(entity)) { + bool remove = false; + if (begin_component_section("Sphere Collider", "##SphereColSection", true, remove)) { + draw_sphere_collider_section(*sc); } - ImGui::EndPopup(); + if (remove) + scene.registry().remove(entity); } + + // ------ CapsuleColliderComponent ------ + if (auto* cc = scene.registry().try_get(entity)) { + bool remove = false; + if (begin_component_section("Capsule Collider", "##CapsuleColSection", true, remove)) { + draw_capsule_collider_section(*cc); + } + if (remove) + scene.registry().remove(entity); + } + + // ------ IKTargetComponent ------ + if (auto* ik = scene.registry().try_get(entity)) { + bool remove = false; + if (begin_component_section("IK Target", "##IKSection", true, remove)) { + draw_ik_target_section(*ik); + } + if (remove) + scene.registry().remove(entity); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // --- Add Component button --- + draw_add_component_popup(scene, entity); } ImGui::End(); } -void EditorUI::draw_stats(float fps, size_t entity_count, float display_w, float top_offset) { - ImGui::SetNextWindowPos(ImVec2(230, top_offset + 10)); - if (ImGui::Begin("Stats", &m_show_stats, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("FPS: %.1f", fps); - ImGui::Text("Entities: %zu", entity_count); - ImGui::Text("Renderer: OpenGL"); - ImGui::Text("Backend: EnTT"); +// ============================================================================ +// Component Section Helpers +// ============================================================================ + +bool EditorUI::begin_component_section(const char* label, const char* id, bool can_remove, + bool& out_remove_requested) { + out_remove_requested = false; + + ImGui::PushID(id); + + // Collapsing header with settings gear via context menu + bool open = ImGui::CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen); + + // Right-click context menu for removal + if (can_remove && ImGui::BeginPopupContextItem("ComponentContextMenu")) { + if (ImGui::MenuItem("Remove Component")) { + out_remove_requested = true; + } + ImGui::EndPopup(); } - ImGui::End(); + + ImGui::PopID(); + return open; +} + +// ============================================================================ +// Tag Section +// ============================================================================ + +void EditorUI::draw_tag_section(hz::TagComponent& tag) { + char buffer[256]; + std::strncpy(buffer, tag.tag.c_str(), sizeof(buffer) - 1); + buffer[sizeof(buffer) - 1] = '\0'; + + ImGui::PushItemWidth(-1); + if (ImGui::InputText("##EntityName", buffer, sizeof(buffer), + ImGuiInputTextFlags_EnterReturnsTrue)) { + tag.tag = buffer; + } + // Also update on deactivation (user clicked away) + if (ImGui::IsItemDeactivatedAfterEdit()) { + tag.tag = buffer; + } + ImGui::PopItemWidth(); + + ImGui::Spacing(); +} + +// ============================================================================ +// Transform Section +// ============================================================================ + +void EditorUI::draw_transform_section(hz::TransformComponent& transform) { + // Position + ImGui::Text("Position"); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 20.0f); + if (ImGui::SmallButton("R##pos")) { + transform.position = glm::vec3(0.0f); + } + ImGui::DragFloat3("##Position", &transform.position.x, 0.1f); + + // Rotation + ImGui::Text("Rotation"); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 20.0f); + if (ImGui::SmallButton("R##rot")) { + transform.rotation = glm::vec3(0.0f); + } + ImGui::DragFloat3("##Rotation", &transform.rotation.x, 1.0f, -360.0f, 360.0f); + + // Scale + ImGui::Text("Scale"); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 20.0f); + if (ImGui::SmallButton("R##scl")) { + transform.scale = glm::vec3(1.0f); + } + ImGui::DragFloat3("##Scale", &transform.scale.x, 0.05f, 0.001f, 100.0f); +} + +// ============================================================================ +// Mesh Section +// ============================================================================ + +void EditorUI::draw_mesh_section(hz::MeshComponent& mesh) { + // Mesh type selector + const char* mesh_types[] = {"Primitive", "Model"}; + int current_type = static_cast(mesh.mesh_type); + if (ImGui::Combo("Type", ¤t_type, mesh_types, 2)) { + mesh.mesh_type = static_cast(current_type); + } + + if (mesh.mesh_type == hz::MeshComponent::MeshType::Primitive) { + // Primitive selector + const char* primitives[] = {"cube", "sphere", "plane"}; + int current_prim = 0; + if (mesh.primitive_name == "sphere") + current_prim = 1; + else if (mesh.primitive_name == "plane") + current_prim = 2; + + if (ImGui::Combo("Primitive", ¤t_prim, primitives, 3)) { + mesh.primitive_name = primitives[current_prim]; + mesh.mesh_path = mesh.primitive_name; + } + } else { + ImGui::Text("Model Handle: %u (gen %u)", mesh.model.index, mesh.model.generation); + } + + ImGui::Checkbox("Cast Shadows", &mesh.cast_shadows); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Text("Material"); + + ImGui::ColorEdit3("Albedo", &mesh.albedo_color.x); + ImGui::SliderFloat("Metallic", &mesh.metallic, 0.0f, 1.0f); + ImGui::SliderFloat("Roughness", &mesh.roughness, 0.0f, 1.0f); + + // Show texture paths if set + if (!mesh.albedo_path.empty()) { + ImGui::TextDisabled("Albedo Tex: %s", mesh.albedo_path.c_str()); + } + if (!mesh.normal_path.empty()) { + ImGui::TextDisabled("Normal Tex: %s", mesh.normal_path.c_str()); + } +} + +// ============================================================================ +// Light Section +// ============================================================================ + +void EditorUI::draw_light_section(hz::LightComponent& light) { + const char* light_types[] = {"Directional", "Point"}; + int current_type = static_cast(light.type); + if (ImGui::Combo("Type", ¤t_type, light_types, 2)) { + light.type = static_cast(current_type); + } + + ImGui::ColorEdit3("Color", &light.color.x); + ImGui::DragFloat("Intensity", &light.intensity, 0.1f, 0.0f, 100.0f); + + if (light.type == hz::LightType::Point) { + ImGui::DragFloat("Range", &light.range, 0.5f, 0.0f, 500.0f); + } +} + +// ============================================================================ +// Camera Section +// ============================================================================ + +void EditorUI::draw_camera_section(hz::CameraComponent& cam) { + ImGui::SliderFloat("FOV", &cam.fov, 10.0f, 120.0f, "%.0f deg"); + ImGui::DragFloat("Near Plane", &cam.near_plane, 0.01f, 0.001f, 10.0f, "%.3f"); + ImGui::DragFloat("Far Plane", &cam.far_plane, 10.0f, 10.0f, 10000.0f, "%.0f"); + ImGui::Checkbox("Primary", &cam.primary); +} + +// ============================================================================ +// Rigid Body Section +// ============================================================================ + +void EditorUI::draw_rigidbody_section(hz::RigidBodyComponent& rb) { + const char* body_types[] = {"Static", "Dynamic", "Kinematic"}; + int current_type = static_cast(rb.type); + if (ImGui::Combo("Body Type", ¤t_type, body_types, 3)) { + rb.type = static_cast(current_type); + } + + if (rb.type == hz::RigidBodyComponent::BodyType::Dynamic) { + ImGui::DragFloat("Mass", &rb.mass, 0.1f, 0.001f, 10000.0f, "%.2f kg"); + } + + ImGui::Checkbox("Fixed Rotation", &rb.fixed_rotation); + + // Runtime info + ImGui::TextDisabled("Physics body %s", rb.created ? "created" : "pending"); +} + +// ============================================================================ +// Box Collider Section +// ============================================================================ + +void EditorUI::draw_box_collider_section(hz::BoxColliderComponent& bc) { + ImGui::DragFloat3("Half Extents", &bc.half_extents.x, 0.05f, 0.001f, 100.0f); + ImGui::DragFloat3("Offset", &bc.offset.x, 0.05f); +} + +// ============================================================================ +// Sphere Collider Section +// ============================================================================ + +void EditorUI::draw_sphere_collider_section(hz::SphereColliderComponent& sc) { + ImGui::DragFloat("Radius", &sc.radius, 0.05f, 0.001f, 100.0f); + ImGui::DragFloat3("Offset", &sc.offset.x, 0.05f); +} + +// ============================================================================ +// Capsule Collider Section +// ============================================================================ + +void EditorUI::draw_capsule_collider_section(hz::CapsuleColliderComponent& cc) { + ImGui::DragFloat("Radius", &cc.radius, 0.05f, 0.001f, 100.0f); + ImGui::DragFloat("Half Height", &cc.half_height, 0.05f, 0.001f, 100.0f); + ImGui::DragFloat3("Offset", &cc.offset.x, 0.05f); + + // Visual info + float total_height = 2.0f * cc.half_height + 2.0f * cc.radius; + ImGui::TextDisabled("Total height: %.2f", static_cast(total_height)); } -void EditorUI::draw_scene_settings(SceneSettings& settings, float display_w, float display_h, - float top_offset, float width) { - // Position to the left of Inspector if space allows, otherwise floating - ImGui::SetNextWindowSize(ImVec2(300, 400), ImGuiCond_FirstUseEver); +// ============================================================================ +// IK Target Section +// ============================================================================ + +void EditorUI::draw_ik_target_section(hz::IKTargetComponent& ik) { + ImGui::Checkbox("Enabled", &ik.enabled); + + ImGui::DragInt("Root Bone", &ik.root_bone_id, 1, -1, 255); + ImGui::DragInt("Mid Bone", &ik.mid_bone_id, 1, -1, 255); + ImGui::DragInt("End Bone", &ik.end_bone_id, 1, -1, 255); + + ImGui::DragFloat3("Target Pos", &ik.target_position.x, 0.1f); + ImGui::DragFloat3("Pole Vector", &ik.pole_vector.x, 0.05f); + + ImGui::SliderFloat("Weight", &ik.weight, 0.0f, 1.0f); +} + +// ============================================================================ +// Add Component Popup +// ============================================================================ + +void EditorUI::draw_add_component_popup(hz::Scene& scene, hz::Entity entity) { + float button_width = ImGui::GetContentRegionAvail().x; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.32f, 0.50f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.40f, 0.62f, 1.0f)); + + if (ImGui::Button("+ Add Component", ImVec2(button_width, 28))) { + ImGui::OpenPopup("AddComponentPopup"); + } + + ImGui::PopStyleColor(2); + + if (ImGui::BeginPopup("AddComponentPopup")) { + ImGui::TextDisabled("--- Rendering ---"); + + if (!scene.registry().any_of(entity)) { + if (ImGui::MenuItem("Mesh")) { + scene.registry().emplace(entity); + } + } + if (!scene.registry().any_of(entity)) { + if (ImGui::MenuItem("Light")) { + scene.registry().emplace(entity); + } + } + if (!scene.registry().any_of(entity)) { + if (ImGui::MenuItem("Camera")) { + scene.registry().emplace(entity); + } + } + + ImGui::Separator(); + ImGui::TextDisabled("--- Physics ---"); + + if (!scene.registry().any_of(entity)) { + if (ImGui::MenuItem("Rigid Body")) { + scene.registry().emplace(entity); + } + } + if (!scene.registry().any_of(entity)) { + if (ImGui::MenuItem("Box Collider")) { + scene.registry().emplace(entity); + } + } + if (!scene.registry().any_of(entity)) { + if (ImGui::MenuItem("Sphere Collider")) { + scene.registry().emplace(entity); + } + } + if (!scene.registry().any_of(entity)) { + if (ImGui::MenuItem("Capsule Collider")) { + scene.registry().emplace(entity); + } + } + + ImGui::Separator(); + ImGui::TextDisabled("--- Animation ---"); + + if (!scene.registry().any_of(entity)) { + if (ImGui::MenuItem("IK Target")) { + scene.registry().emplace(entity); + } + } + + ImGui::EndPopup(); + } +} + +// ============================================================================ +// Scene Settings Panel +// ============================================================================ + +void EditorUI::draw_scene_settings(SceneSettings& settings, [[maybe_unused]] float display_w, + [[maybe_unused]] float display_h, + [[maybe_unused]] float top_offset, + [[maybe_unused]] float width) { + ImGui::SetNextWindowSize(ImVec2(320, 440), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Scene Settings", &m_show_settings)) { + // Environment if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::ColorEdit3("Clear Color", &settings.clear_color.x); ImGui::ColorEdit3("Ambient Color", &settings.ambient_color.x); ImGui::DragFloat("Ambient Intensity", &settings.ambient_intensity, 0.01f, 0.0f, 5.0f); - ImGui::Separator(); + ImGui::Spacing(); ImGui::Checkbox("Fog Enabled", &settings.fog_enabled); if (settings.fog_enabled) { ImGui::DragFloat("Fog Density", &settings.fog_density, 0.001f, 0.0f, 0.1f); - ImGui::ColorEdit3("Fog Color", - &settings.clear_color.x); // Fog usually matches clear color + ImGui::DragFloat("Fog Gradient", &settings.fog_gradient, 0.05f, 0.1f, 10.0f); } } + // Sun if (ImGui::CollapsingHeader("Directional Light", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::DragFloat3("Direction", &settings.sun_direction.x, 0.01f, -1.0f, 1.0f); ImGui::ColorEdit3("Sun Color", &settings.sun_color.x); ImGui::DragFloat("Sun Intensity", &settings.sun_intensity, 0.1f, 0.0f, 20.0f); } + // Post-processing if (ImGui::CollapsingHeader("Post Processing", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::DragFloat("Exposure", &settings.exposure, 0.05f, 0.0f, 10.0f); + + ImGui::Spacing(); ImGui::Checkbox("Bloom", &settings.bloom_enabled); if (settings.bloom_enabled) { ImGui::DragFloat("Bloom Intensity", &settings.bloom_intensity, 0.01f, 0.0f, 5.0f); ImGui::DragFloat("Bloom Threshold", &settings.bloom_threshold, 0.01f, 0.0f, 2.0f); } - ImGui::DragFloat("Exposure", &settings.exposure, 0.1f, 0.0f, 10.0f); } } ImGui::End(); } -void EditorUI::draw_console(float display_h, float top_offset, float width) { - // Bottom panel +// ============================================================================ +// Stats Panel +// ============================================================================ + +void EditorUI::draw_stats(float fps, size_t entity_count, const hz::RenderStats& stats, + [[maybe_unused]] float display_w, float top_offset) { + ImGui::SetNextWindowPos(ImVec2(250, top_offset + 10), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowBgAlpha(0.85f); + + if (ImGui::Begin("Stats", &m_show_stats, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing)) { + // General + ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Performance"); + ImGui::Text("FPS: %.1f", static_cast(fps)); + ImGui::Text("Frame: %.2f ms", static_cast(stats.total_frame_ms)); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Renderer + ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Renderer"); + ImGui::Text("Backend: Vulkan 1.3"); + ImGui::Text("ECS: EnTT"); + ImGui::Text("Entities: %zu", entity_count); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Draw stats + ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Draw Stats"); + ImGui::Text("Draw Calls: %u", stats.draw_calls); + ImGui::Text("Triangles: %u", stats.triangles); + ImGui::Text("Visible: %u", stats.visible_objects); + ImGui::Text("Culled: %u", stats.culled_objects); + ImGui::Text("Lights: %u", stats.active_lights); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Pass timings + ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Pass Timings"); + ImGui::Text("Geometry: %.2f ms", static_cast(stats.geometry_pass_ms)); + ImGui::Text("Shadows: %.2f ms", static_cast(stats.shadow_pass_ms)); + ImGui::Text("Lighting: %.2f ms", static_cast(stats.lighting_pass_ms)); + ImGui::Text("PostFX: %.2f ms", static_cast(stats.post_process_ms)); + } + ImGui::End(); +} + +// ============================================================================ +// Console Panel +// ============================================================================ + +void EditorUI::draw_console(float display_h, [[maybe_unused]] float top_offset, float width) { float console_height = 200.0f; ImGui::SetNextWindowPos(ImVec2(width, display_h - console_height), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(600, console_height), ImGuiCond_FirstUseEver); if (ImGui::Begin("Console", &m_show_console)) { + // Toolbar if (ImGui::Button("Clear")) { m_console_logs.clear(); } + ImGui::SameLine(); + ImGui::TextDisabled("(%zu messages)", m_console_logs.size()); + ImGui::Separator(); + // Log area ImGui::BeginChild("ScrollingRegion", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); + for (const auto& log : m_console_logs) { - ImGui::TextUnformatted(log.c_str()); + // Color code by prefix + if (log.find("[ERR") != std::string::npos || log.find("[FATAL") != std::string::npos) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::TextUnformatted(log.c_str()); + ImGui::PopStyleColor(); + } else if (log.find("[WARN") != std::string::npos) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.3f, 1.0f)); + ImGui::TextUnformatted(log.c_str()); + ImGui::PopStyleColor(); + } else { + ImGui::TextUnformatted(log.c_str()); + } } + if (m_scroll_to_bottom) { ImGui::SetScrollHereY(1.0f); m_scroll_to_bottom = false; } + ImGui::EndChild(); } ImGui::End(); diff --git a/game/src/editor_ui.hpp b/game/src/editor_ui.hpp index 88c35c1..98f93b5 100644 --- a/game/src/editor_ui.hpp +++ b/game/src/editor_ui.hpp @@ -2,10 +2,19 @@ /** * @file editor_ui.hpp - * @brief In-game ECS editor using ImGui + * @brief In-game Scene Hierarchy Editor & Property Inspector using ImGui + * + * Provides a Unity/Unreal-style editor overlay with: + * - Scene hierarchy tree with search and entity icons + * - Full property inspector for all component types + * - Entity creation presets and component management + * - Scene settings panel (environment, sun, post-processing) + * - Console log panel + * - Render stats overlay */ #include "engine/core/log.hpp" +#include "engine/renderer/deferred_renderer.hpp" #include "engine/scene/components.hpp" #include "engine/scene/scene.hpp" #include "scene_settings.hpp" @@ -18,7 +27,7 @@ namespace game { /** - * @brief Simple in-game editor for ECS manipulation + * @brief Full-featured in-game editor for ECS manipulation */ class EditorUI { public: @@ -26,67 +35,152 @@ class EditorUI { ~EditorUI() = default; /** - * @brief Draw the editor UI + * @brief Apply a professional dark theme to ImGui + * + * Call once after ImGui context creation. + */ + static void apply_dark_theme(); + + /** + * @brief Draw the entire editor UI * @param scene The Scene to edit * @param settings Scene settings to edit * @param fps Current FPS * @param entity_count Total entity count + * @param render_stats Current frame render stats */ - void draw(hz::Scene& scene, SceneSettings& settings, float fps, size_t entity_count); + void draw(hz::Scene& scene, SceneSettings& settings, float fps, size_t entity_count, + const hz::RenderStats& render_stats); /** - * @brief Add a log message to the console + * @brief Add a log message to the console panel */ void add_log(const std::string& message) { m_console_logs.push_back(message); - if (m_console_logs.size() > 100) + if (m_console_logs.size() > 200) m_console_logs.erase(m_console_logs.begin()); m_scroll_to_bottom = true; } - /** - * @brief Check if an entity is selected - */ - [[nodiscard]] bool has_selection() const { return m_selected_entity != hz::Entity{entt::null}; } + // ========================================================================= + // Selection + // ========================================================================= - /** - * @brief Get the selected entity - */ + [[nodiscard]] bool has_selection() const { return m_selected_entity != hz::Entity{entt::null}; } [[nodiscard]] hz::Entity selected_entity() const { return m_selected_entity; } + void clear_selection() { m_selected_entity = entt::null; } + + // ========================================================================= + // Entity Preset Requests (polled by Application each frame) + // ========================================================================= - /** - * @brief Check if "Add Cube" was clicked - */ [[nodiscard]] bool should_add_cube() { - bool result = m_add_cube_requested; - m_add_cube_requested = false; - return result; + bool r = m_req_add_cube; + m_req_add_cube = false; + return r; + } + [[nodiscard]] bool should_add_sphere() { + bool r = m_req_add_sphere; + m_req_add_sphere = false; + return r; + } + [[nodiscard]] bool should_add_light() { + bool r = m_req_add_light; + m_req_add_light = false; + return r; + } + [[nodiscard]] bool should_add_camera() { + bool r = m_req_add_camera; + m_req_add_camera = false; + return r; + } + [[nodiscard]] bool should_add_empty() { + bool r = m_req_add_empty; + m_req_add_empty = false; + return r; } /** - * @brief Check if "Add Light" was clicked + * @brief Check if a delete was requested (and get the entity to delete) */ - [[nodiscard]] bool should_add_light() { - bool result = m_add_light_requested; - m_add_light_requested = false; - return result; + [[nodiscard]] bool should_delete_entity(hz::Entity& out_entity) { + if (m_req_delete) { + out_entity = m_req_delete_target; + m_req_delete = false; + m_req_delete_target = entt::null; + return true; + } + return false; } private: - bool m_add_cube_requested{false}; - bool m_add_light_requested{false}; - void draw_hierarchy(hz::Scene& scene, float display_h, float menu_bar_height, float width); - void draw_inspector(hz::Scene& scene, float display_w, float display_h, float menu_bar_height, + // ========================================================================= + // Panel Drawing + // ========================================================================= + + void draw_menu_bar(); + void draw_toolbar(float display_w, float menu_bar_height); + void draw_hierarchy(hz::Scene& scene, float display_h, float top_offset, float width); + void draw_inspector(hz::Scene& scene, float display_w, float display_h, float top_offset, float width); void draw_scene_settings(SceneSettings& settings, float display_w, float display_h, - float menu_bar_height, float width); - void draw_stats(float fps, size_t entity_count, float display_w, float menu_bar_height); - void draw_toolbar(float display_w, float menu_bar_height); - void draw_console(float display_h, float menu_bar_height, float width); + float top_offset, float width); + void draw_stats(float fps, size_t entity_count, const hz::RenderStats& stats, float display_w, + float top_offset); + void draw_console(float display_h, float top_offset, float width); + + // ========================================================================= + // Inspector Helpers + // ========================================================================= + + void draw_tag_section(hz::TagComponent& tag); + void draw_transform_section(hz::TransformComponent& transform); + void draw_mesh_section(hz::MeshComponent& mesh); + void draw_light_section(hz::LightComponent& light); + void draw_camera_section(hz::CameraComponent& cam); + void draw_rigidbody_section(hz::RigidBodyComponent& rb); + void draw_box_collider_section(hz::BoxColliderComponent& bc); + void draw_sphere_collider_section(hz::SphereColliderComponent& sc); + void draw_capsule_collider_section(hz::CapsuleColliderComponent& cc); + void draw_ik_target_section(hz::IKTargetComponent& ik); + + /** + * @brief Draw a collapsing component header with a remove option + * @return true if the section is open + */ + bool begin_component_section(const char* label, const char* id, bool can_remove, + bool& out_remove_requested); + + void draw_add_component_popup(hz::Scene& scene, hz::Entity entity); + + // ========================================================================= + // Utility + // ========================================================================= + + /** @brief Get a display icon prefix for an entity based on its components */ + static const char* entity_icon(const entt::registry& reg, hz::Entity entity); + + // ========================================================================= + // State + // ========================================================================= hz::Entity m_selected_entity{entt::null}; - // Panel Visibility + // Preset requests + bool m_req_add_cube{false}; + bool m_req_add_sphere{false}; + bool m_req_add_light{false}; + bool m_req_add_camera{false}; + bool m_req_add_empty{false}; + + // Delete request (deferred) + bool m_req_delete{false}; + hz::Entity m_req_delete_target{entt::null}; + + // Search + char m_search_buffer[256]{}; + + // Panel visibility bool m_show_hierarchy{true}; bool m_show_inspector{true}; bool m_show_settings{true};