From d65233be9433a01c54e959aea72e87ff2e385937 Mon Sep 17 00:00:00 2001 From: hachem Date: Sun, 17 Aug 2025 22:38:29 +0200 Subject: feat: add world builder state --- .gitmodules | 3 + assets/shaders/sphere.glsl | 53 +++ ext/ImGuizmo | 1 + premake5.lua | 48 ++- src/core/application.cpp | 19 +- src/core/settings_manager.cpp | 2 + src/states/config_state.cpp | 38 ++- src/states/world_builder_state.cpp | 674 +++++++++++++++++++++++++++++++++++++ src/states/world_builder_state.h | 65 ++++ 9 files changed, 883 insertions(+), 20 deletions(-) create mode 100644 assets/shaders/sphere.glsl create mode 160000 ext/ImGuizmo create mode 100644 src/states/world_builder_state.cpp create mode 100644 src/states/world_builder_state.h diff --git a/.gitmodules b/.gitmodules index 27a424d..bd018a2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,3 +11,6 @@ [submodule "ext/toml11"] path = ext/toml11 url = https://github.com/ToruNiina/toml11 +[submodule "ext/ImGuizmo"] + path = ext/ImGuizmo + url = https://github.com/CedricGuillemet/ImGuizmo.git diff --git a/assets/shaders/sphere.glsl b/assets/shaders/sphere.glsl new file mode 100644 index 0000000..37757b5 --- /dev/null +++ b/assets/shaders/sphere.glsl @@ -0,0 +1,53 @@ +#type vertex + +#version 330 core + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; + +uniform mat4 u_ViewProjection; +uniform mat4 u_Transform; + +out vec3 v_Normal; +out vec3 v_WorldPos; + +void main() +{ + v_WorldPos = vec3(u_Transform * vec4(a_Position, 1.0)); + v_Normal = mat3(transpose(inverse(u_Transform))) * a_Normal; + gl_Position = u_ViewProjection * vec4(v_WorldPos, 1.0); +} + +#type fragment + +#version 330 core + +in vec3 v_Normal; +in vec3 v_WorldPos; + +uniform vec3 u_Color; +uniform float u_Specular; +uniform float u_Emission; +uniform vec3 u_LightPos; +uniform vec3 u_CameraPos; + +out vec4 o_FragColor; + +void main() +{ + vec3 normal = normalize(v_Normal); + vec3 lightDir = normalize(u_LightPos - v_WorldPos); + vec3 viewDir = normalize(u_CameraPos - v_WorldPos); + vec3 reflectDir = reflect(-lightDir, normal); + + float diff = max(dot(normal, lightDir), 0.0); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); + + vec3 ambient = u_Color * 0.1; + vec3 diffuse = u_Color * diff; + vec3 specular = vec3(u_Specular) * spec; + vec3 emission = u_Color * u_Emission; + + vec3 result = ambient + diffuse + specular + emission; + o_FragColor = vec4(result, 1.0); +} diff --git a/ext/ImGuizmo b/ext/ImGuizmo new file mode 160000 index 0000000..3e72562 --- /dev/null +++ b/ext/ImGuizmo @@ -0,0 +1 @@ +Subproject commit 3e725628fa64986c71ebc7c764621a8e7423fef4 diff --git a/premake5.lua b/premake5.lua index 25db23d..7e22bc9 100644 --- a/premake5.lua +++ b/premake5.lua @@ -29,6 +29,7 @@ IncludeDir["glfw"] = "ext/glfw/include" IncludeDir["glad"] = "ext/glad/include" IncludeDir["imgui"] = "ext/imgui" IncludeDir["imgui_backends"] = "ext/imgui/backends" +IncludeDir["imguizmo"] = "ext/ImGuizmo" IncludeDir["toml11"] = "ext/toml11/include" group "Dependencies" @@ -244,6 +245,49 @@ project "ImGui" runtime "Release" optimize "on" symbols "off" + +project "ImGuizmo" + kind "StaticLib" + language "C++" + staticruntime "on" + + targetdir ("bin/" .. outputdir) + objdir ("bin-int/" .. outputdir) + + files + { + "ext/ImGuizmo/ImGuizmo.cpp", + "ext/ImGuizmo/ImGuizmo.h" + } + + includedirs + { + "%{IncludeDir.imgui}", + "%{IncludeDir.imguizmo}" + } + + filter "system:windows" + systemversion "latest" + cppdialect "C++17" + + filter "system:linux" + pic "On" + systemversion "latest" + cppdialect "C++17" + + filter "configurations:Debug" + runtime "Debug" + symbols "on" + + filter "configurations:Release" + runtime "Release" + optimize "on" + + filter "configurations:Dist" + runtime "Release" + optimize "on" + symbols "off" + group "" project "Donut" @@ -285,6 +329,7 @@ project "Donut" "%{IncludeDir.glad}", "%{IncludeDir.imgui}", "%{IncludeDir.imgui_backends}", + "%{IncludeDir.imguizmo}", "%{IncludeDir.toml11}", } @@ -292,7 +337,8 @@ project "Donut" { "GLFW", "GLAD", - "ImGui" + "ImGui", + "ImGuizmo" } filter "system:windows" diff --git a/src/core/application.cpp b/src/core/application.cpp index 9d2278e..85c7645 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -5,9 +5,10 @@ #include "states/simulation_state.h" #include "states/config_state.h" +#include "states/world_builder_state.h" #include - +#include namespace Donut { @@ -88,6 +89,12 @@ namespace Donut event.handled = true; return true; } + else if (e.get_key_code() == GLFW_KEY_3) + { + m_state_manager->switch_to_state("WorldBuilder"); + event.handled = true; + return true; + } return true; }); @@ -112,8 +119,9 @@ namespace Donut RenderCommand::set_face_culling(false); m_state_manager = create_scope(); - m_state_manager->register_state("Config", create_scope()); - m_state_manager->register_state("Simulation", create_scope()); + m_state_manager->register_state("Config", create_scope()); + m_state_manager->register_state("Simulation", create_scope()); + m_state_manager->register_state("WorldBuilder", create_scope()); m_state_manager->switch_to_state("Config"); } @@ -139,8 +147,11 @@ namespace Donut void Application::on_render() { m_state_manager->render(); - m_window->begin_im_gui_frame(); + + ImGuizmo::BeginFrame(); + ImGuizmo::SetOrthographic(false); + setup_docking_layout(); m_state_manager->on_im_ui_render(); m_window->end_im_gui_frame(); diff --git a/src/core/settings_manager.cpp b/src/core/settings_manager.cpp index eb68794..289285c 100644 --- a/src/core/settings_manager.cpp +++ b/src/core/settings_manager.cpp @@ -138,7 +138,9 @@ namespace Donut DONUT_INFO("Settings saved to {}", file_path); } catch (const std::exception& e) + { DONUT_ERROR("Failed to save settings: {}", e.what()); + } } void SettingsManager::set_simulation_settings(const SimulationSettings& settings) diff --git a/src/states/config_state.cpp b/src/states/config_state.cpp index 1f1ef16..41fbaad 100644 --- a/src/states/config_state.cpp +++ b/src/states/config_state.cpp @@ -16,20 +16,20 @@ namespace Donut const auto& settings = SettingsManager::get_settings_const(); - m_selected_api = (settings.graphics.render_api == "Vulkan") ? RendererAPI::API::Vulkan : RendererAPI::API::OpenGL; - m_selected_theme = (settings.graphics.selected_theme == "Light") ? 1 : - (settings.graphics.selected_theme == "Blue") ? 2 : 0; - m_target_fps = settings.simulation.target_fps; - m_compute_height = settings.simulation.compute_height; - m_max_steps_moving = settings.simulation.max_steps_moving; - m_max_steps_static = settings.simulation.max_steps_static; - m_early_exit_distance = settings.simulation.early_exit_distance; - m_gravity_enabled = settings.simulation.gravity_enabled; - m_v_sync_enabled = settings.graphics.v_sync_enabled; - m_show_fps = settings.graphics.show_fps; + m_selected_api = (settings.graphics.render_api == "Vulkan") ? RendererAPI::API::Vulkan : RendererAPI::API::OpenGL; + m_selected_theme = (settings.graphics.selected_theme == "Light") ? 1 : + (settings.graphics.selected_theme == "Blue") ? 2 : 0; + m_target_fps = settings.simulation.target_fps; + m_compute_height = settings.simulation.compute_height; + m_max_steps_moving = settings.simulation.max_steps_moving; + m_max_steps_static = settings.simulation.max_steps_static; + m_early_exit_distance = settings.simulation.early_exit_distance; + m_gravity_enabled = settings.simulation.gravity_enabled; + m_v_sync_enabled = settings.graphics.v_sync_enabled; + m_show_fps = settings.graphics.show_fps; m_show_performance_metrics = settings.graphics.show_performance_metrics; - m_show_debug_info = settings.graphics.show_debug_info; - m_enable_anti_aliasing = settings.graphics.enable_anti_aliasing; + m_show_debug_info = settings.graphics.show_debug_info; + m_enable_anti_aliasing = settings.graphics.enable_anti_aliasing; } void ConfigState::on_exit() @@ -52,7 +52,7 @@ namespace Donut void ConfigState::on_render() { - Renderer::set_clear_color({ 0.05f, 0.05f, 0.1f, 1.0f }); + Renderer::set_clear_color({ 0.1f, 0.1f, 0.1f, 1.0f }); Renderer::clear(); } @@ -104,6 +104,7 @@ namespace Donut if (is_selected) ImGui::SetItemDefaultFocus(); } + ImGui::EndCombo(); } @@ -215,7 +216,7 @@ namespace Donut ImGui::Separator(); ImGui::Spacing(); - float button_width = (ImGui::GetWindowWidth() - 100) / 5.0f; + float button_width = (ImGui::GetWindowWidth() - 120) / 6.0f; if (ImGui::Button("Start Simulation", ImVec2(button_width, 35))) { @@ -223,6 +224,13 @@ namespace Donut Application::get().get_state_manager().switch_to_state("Simulation"); } + ImGui::SameLine(); + if (ImGui::Button("World Builder", ImVec2(button_width, 35))) + { + apply_settings(); + Application::get().get_state_manager().switch_to_state("WorldBuilder"); + } + ImGui::SameLine(); if (ImGui::Button("Reset to Defaults", ImVec2(button_width, 35))) reset_to_defaults(); diff --git a/src/states/world_builder_state.cpp b/src/states/world_builder_state.cpp new file mode 100644 index 0000000..16b7866 --- /dev/null +++ b/src/states/world_builder_state.cpp @@ -0,0 +1,674 @@ +#include "world_builder_state.h" + +#include "core/application.h" +#include "core/window.h" + +#include "rendering/renderer.h" +#include "rendering/shader.h" +#include "rendering/vertex_array.h" +#include "rendering/vertex_buffer.h" +#include "rendering/index_buffer.h" +#include "rendering/texture.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#define _USE_MATH_DEFINES +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +namespace Donut +{ + void WorldBuilderState::on_enter() + { + DONUT_INFO("Entering World Builder State"); + + ImGuizmo::Enable(true); + + m_camera.set_camera_mode(CameraMode::Orbital); + m_camera.set_orbital_target(glm::vec3(0.0f, 0.0f, 0.0f)); + m_camera.set_orbital_radius(15.0f); + m_camera.set_orbital_limits(2.0f, 200.0f); + m_camera.set_orbital_speed(0.01f); + m_camera.set_zoom_speed(2.0f); + m_camera.set_azimuth(0.0f); + m_camera.set_elevation(static_cast(M_PI) / 3.0f); + m_camera.update_orbital(); + + Material default_material(glm::vec3(0.8f, 0.2f, 0.2f), 0.5f, 0.0f); + Object sphere1(glm::vec3(-3.0f, 0.0f, 0.0f), 1.0f, default_material); + m_scene.objs.push_back(sphere1); + + Material material2(glm::vec3(0.2f, 0.8f, 0.2f), 0.3f, 0.0f); + Object sphere2(glm::vec3(3.0f, 0.0f, 0.0f), 1.5f, material2); + m_scene.objs.push_back(sphere2); + + Material material3(glm::vec3(0.2f, 0.2f, 0.8f), 0.7f, 0.0f); + Object sphere3(glm::vec3(0.0f, 2.0f, 0.0f), 0.8f, material3); + m_scene.objs.push_back(sphere3); + + m_initialized = true; + } + + void WorldBuilderState::on_exit() + { + DONUT_INFO("Exiting World Builder State"); + } + + void WorldBuilderState::on_update(float delta_time) + { + if (m_camera_dragging && + !ImGuizmo::IsUsing()) + { + GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); + double xpos, ypos; + glfwGetCursorPos(window, &xpos, &ypos); + + glm::vec2 current_mouse_pos(xpos, ypos); + glm::vec2 delta = current_mouse_pos - m_last_mouse_pos; + + float sensitivity = 0.005f; + float azimuth_delta = delta.x * sensitivity; + float elevation_delta = -delta.y * sensitivity; + + float new_azimuth = m_camera.get_azimuth() + azimuth_delta; + float new_elevation = m_camera.get_elevation() + elevation_delta; + + new_elevation = glm::clamp(new_elevation, 0.01f, static_cast(M_PI) - 0.01f); + + m_camera.set_azimuth(new_azimuth); + m_camera.set_elevation(new_elevation); + m_camera.update_orbital(); + + m_last_mouse_pos = current_mouse_pos; + } + } + + void WorldBuilderState::on_render() + { + RenderCommand::set_clear_color(glm::vec4(0.1f, 0.1f, 0.1f, 1.0f)); + RenderCommand::clear(); + render_scene(); + } + + void WorldBuilderState::on_event(Event& event) + { + EventDispatcher dispatcher(event); + + dispatcher.dispatch([this](MouseButtonPressedEvent& e) + { + if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT) + { + if (ImGuizmo::IsUsing() || + ImGuizmo::IsOver()) + return false; + + GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); + double xpos, ypos; + glfwGetCursorPos(window, &xpos, &ypos); + + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + float ndc_x = (2.0f * xpos) / width - 1.0f; + float ndc_y = 1.0f - (2.0f * ypos) / height; + + glm::vec4 ray_start_ndc(ndc_x, ndc_y, -1.0f, 1.0f); + glm::vec4 ray_end_ndc(ndc_x, ndc_y, 0.0f, 1.0f); + + glm::mat4 inv_vp = glm::inverse(m_camera.get_projection_matrix() * m_camera.get_view_matrix()); + glm::vec4 ray_start_world = inv_vp * ray_start_ndc; + glm::vec4 ray_end_world = inv_vp * ray_end_ndc; + + ray_start_world /= ray_start_world.w; + ray_end_world /= ray_end_world.w; + + glm::vec3 ray_dir = glm::normalize(glm::vec3(ray_end_world - ray_start_world)); + glm::vec3 ray_origin = glm::vec3(ray_start_world); + + float closest_distance = std::numeric_limits::max(); + int closest_object_index = -1; + + for (size_t i = 0; i < m_scene.objs.size(); ++i) + { + const Object& obj = m_scene.objs[i]; + + glm::vec3 oc = ray_origin - obj.m_centre; + float a = glm::dot(ray_dir, ray_dir); + float b = 2.0f * glm::dot(oc, ray_dir); + float c = glm::dot(oc, oc) - obj.m_radius * obj.m_radius; + float discriminant = b * b - 4 * a * c; + + if (discriminant > 0) + { + float t1 = (-b - sqrt(discriminant)) / (2.0f * a); + float t2 = (-b + sqrt(discriminant)) / (2.0f * a); + + if (t1 > 0 && t1 < closest_distance) + { + closest_distance = t1; + closest_object_index = static_cast(i); + } + else if (t2 > 0 && t2 < closest_distance) + { + closest_distance = t2; + closest_object_index = static_cast(i); + } + } + } + + if (closest_object_index >= 0) + { + m_selected_object_index = closest_object_index; + DONUT_INFO("Selected object {}", closest_object_index); + return true; + } + else + { + m_camera_dragging = true; + m_last_mouse_pos = glm::vec2(xpos, ypos); + } + + return true; + } + return false; + }); + + dispatcher.dispatch([this](MouseButtonReleasedEvent& e) + { + if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT) + { + if (!ImGuizmo::IsUsing()) + m_camera_dragging = false; + return true; + } + + return false; + }); + + dispatcher.dispatch([this](WindowResizeEvent& e) + { + int new_width = e.get_width(); + int new_height = e.get_height(); + if (new_width > 0 && new_height > 0) + { + float aspect = static_cast(new_width) / static_cast(new_height); + m_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); + } + return false; + }); + + dispatcher.dispatch([this](MouseScrolledEvent& e) + { + float zoom_speed = 0.1f; + float zoom_delta = e.get_y_offset() * zoom_speed; + + double current_radius = m_camera.get_orbital_radius(); + double new_radius = current_radius - zoom_delta * current_radius * 0.1f; + + double min_radius = 2.0f; + double max_radius = 200.0f; + new_radius = glm::clamp(new_radius, min_radius, max_radius); + + m_camera.set_orbital_radius(new_radius); + m_camera.update_orbital(); + + return true; + }); + + dispatcher.dispatch([this](KeyPressedEvent& e) + { + if (m_selected_object_index >= 0 && + m_selected_object_index < static_cast(m_scene.objs.size())) + { + switch (e.get_key_code()) + { + case GLFW_KEY_W: + m_gizmo_operation = ImGuizmo::TRANSLATE; + return true; + case GLFW_KEY_E: + m_gizmo_operation = ImGuizmo::ROTATE; + return true; + case GLFW_KEY_R: + m_gizmo_operation = ImGuizmo::SCALE; + return true; + } + } + return false; + }); + } + + void WorldBuilderState::on_im_ui_render() + { + GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + ImGuiViewport* vp = ImGui::GetMainViewport(); + ImGuizmo::SetRect(vp->Pos.x, vp->Pos.y, vp->Size.x, vp->Size.y); + + ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); + + ImGui::Begin("World Builder", nullptr, ImGuiWindowFlags_NoCollapse); + + ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "World Builder"); + ImGui::PopFont(); + ImGui::Separator(); + + if (ImGui::CollapsingHeader("Scene Info", &m_show_scene_info)) + { + ImGui::Text("Objects in scene: %zu", m_scene.objs.size()); + + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Camera Info:"); + + glm::vec3 camera_pos = m_camera.get_orbital_position(); + glm::vec3 target = m_camera.get_orbital_target(); + + ImGui::Text("Position: (%.2f, %.2f, %.2f)", camera_pos.x, camera_pos.y, camera_pos.z); + ImGui::Text("Target: (%.2f, %.2f, %.2f)", target.x, target.y, target.z); + ImGui::Text("Distance: %.2f", m_camera.get_orbital_radius()); + ImGui::Text("Azimuth: %.1f°", glm::degrees(m_camera.get_azimuth())); + ImGui::Text("Elevation: %.1f°", glm::degrees(m_camera.get_elevation())); + + ImGui::Separator(); + + if (ImGui::Button("Reset Camera")) + { + m_camera.set_orbital_radius(15.0f); + m_camera.set_azimuth(0.0f); + m_camera.set_elevation(static_cast(M_PI) / 3.0f); + m_camera.set_orbital_target(glm::vec3(0.0f, 0.0f, 0.0f)); + m_camera.update_orbital(); + } + + ImGui::SameLine(); + if (ImGui::Button("Clear Scene")) + clear_scene(); + + ImGui::SameLine(); + if (ImGui::Button("Focus on Objects")) + { + if (!m_scene.objs.empty()) + { + glm::vec3 center(0.0f); + for (const auto& obj : m_scene.objs) + center += obj.m_centre; + center /= static_cast(m_scene.objs.size()); + m_camera.set_orbital_target(center); + m_camera.update_orbital(); + } + } + } + + ImGui::Spacing(); + + if (ImGui::CollapsingHeader("Create Object", &m_show_object_creator)) + { + ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "New Sphere"); + ImGui::Separator(); + + ImGui::Text("Position:"); + ImGui::DragFloat3("##Position", &m_new_object_position.x, 0.1f); + + ImGui::Text("Radius:"); + ImGui::DragFloat("##Radius", &m_new_object_radius, 0.1f, 0.1f, 10.0f); + + ImGui::Text("Color:"); + ImGui::ColorEdit3("##Color", &m_new_object_color.x); + + ImGui::Text("Specular:"); + ImGui::SliderFloat("##Specular", &m_new_object_specular, 0.0f, 1.0f); + + ImGui::Text("Emission:"); + ImGui::SliderFloat("##Emission", &m_new_object_emission, 0.0f, 1.0f); + + ImGui::Spacing(); + + if (ImGui::Button("Add Sphere", ImVec2(ImGui::GetWindowWidth() - 20, 30))) + add_sphere(); + } + + ImGui::Spacing(); + + if (ImGui::CollapsingHeader("Scene Objects", &m_show_object_list)) + { + if (m_scene.objs.empty()) + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No objects in scene"); + else + { + for (size_t i = 0; i < m_scene.objs.size(); ++i) + { + Object& obj = m_scene.objs[i]; + + ImGui::PushID(static_cast(i)); + + bool is_selected = (m_selected_object_index == static_cast(i)); + if (ImGui::Selectable(("Sphere " + std::to_string(i)).c_str(), is_selected)) + m_selected_object_index = static_cast(i); + + if (is_selected) + { + ImGui::SameLine(); + if (ImGui::Button("Remove")) + { + remove_selected_object(); + } + + ImGui::Text("Position: (%.2f, %.2f, %.2f)", + obj.m_centre.x, obj.m_centre.y, obj.m_centre.z); + ImGui::Text("Radius: %.2f", obj.m_radius); + ImGui::Text("Color: (%.2f, %.2f, %.2f)", + obj.m_material.m_color.x, obj.m_material.m_color.y, obj.m_material.m_color.z); + } + + ImGui::PopID(); + } + } + } + + if (ImGui::CollapsingHeader("Gizmo Controls", &m_show_gizmo_controls)) + { + ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Gizmo Operation:"); + + if (ImGui::RadioButton("Translate", m_gizmo_operation == ImGuizmo::TRANSLATE)) + m_gizmo_operation = ImGuizmo::TRANSLATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", m_gizmo_operation == ImGuizmo::ROTATE)) + m_gizmo_operation = ImGuizmo::ROTATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Scale", m_gizmo_operation == ImGuizmo::SCALE)) + m_gizmo_operation = ImGuizmo::SCALE; + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Hotkeys:"); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "W = Translate, E = Rotate, R = Scale"); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + float button_width = (ImGui::GetWindowWidth() - 30) / 2.0f; + + if (ImGui::Button("Save Scene", ImVec2(button_width, 30))) + save_scene(); + + ImGui::SameLine(); + if (ImGui::Button("Load Scene", ImVec2(button_width, 30))) + load_scene(); + + ImGui::Spacing(); + + if (ImGui::Button("Back to Config", ImVec2(ImGui::GetWindowWidth() - 20, 30))) + Application::get().get_state_manager().switch_to_state("Config"); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Camera: Left mouse = rotate, Scroll = zoom"); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Gizmo: W=Translate, E=Rotate, R=Scale"); + + ImGui::End(); + + if (m_selected_object_index >= 0 && + m_selected_object_index < static_cast(m_scene.objs.size())) + { + glm::mat4 view = m_camera.get_view_matrix(); + glm::mat4 projection = m_camera.get_projection_matrix(); + + Object& selected_obj = m_scene.objs[m_selected_object_index]; + + glm::mat4 object_transform = glm::mat4(1.0f); + object_transform = glm::translate(object_transform, selected_obj.m_centre); + object_transform = glm::scale(object_transform, glm::vec3(selected_obj.m_radius)); + + DONUT_INFO("Object position: ({}, {}, {})", selected_obj.m_centre.x, selected_obj.m_centre.y, selected_obj.m_centre.z); + glm::vec3 camera_pos = m_camera.get_orbital_position(); + DONUT_INFO("Camera position: ({}, {}, {})", camera_pos.x, camera_pos.y, camera_pos.z); + + glm::vec3 view_translation = glm::vec3(view[3][0], view[3][1], view[3][2]); + DONUT_INFO("View translation: ({}, {}, {})", view_translation.x, view_translation.y, view_translation.z); + + ImGuizmo::SetDrawlist(ImGui::GetForegroundDrawList()); + if (ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), + m_gizmo_operation, m_gizmo_mode, + glm::value_ptr(object_transform), + nullptr, nullptr)) + { + glm::vec3 new_position = glm::vec3(object_transform[3][0], object_transform[3][1], object_transform[3][2]); + selected_obj.m_centre = new_position; + + glm::vec3 scale = glm::vec3( + glm::length(glm::vec3(object_transform[0])), + glm::length(glm::vec3(object_transform[1])), + glm::length(glm::vec3(object_transform[2])) + ); + selected_obj.m_radius = (scale.x + scale.y + scale.z) / 3.0f; + + DONUT_INFO("Matrix [3]: ({}, {}, {}, {})", object_transform[3][0], object_transform[3][1], object_transform[3][2], object_transform[3][3]); + DONUT_INFO("New position: ({}, {}, {})", new_position.x, new_position.y, new_position.z); + } + } + } + + void WorldBuilderState::add_sphere() + { + Material material(m_new_object_color, m_new_object_specular, m_new_object_emission); + Object sphere(m_new_object_position, m_new_object_radius, material); + m_scene.objs.push_back(sphere); + + m_new_object_position = glm::vec3(0.0f, 0.0f, 0.0f); + m_new_object_radius = 1.0f; + m_new_object_color = glm::vec3(1.0f, 1.0f, 1.0f); + m_new_object_specular = 0.5f; + m_new_object_emission = 0.0f; + + DONUT_INFO("Added sphere to scene"); + } + + void WorldBuilderState::remove_selected_object() + { + if (m_selected_object_index >= 0 && m_selected_object_index < static_cast(m_scene.objs.size())) + { + m_scene.objs.erase(m_scene.objs.begin() + m_selected_object_index); + m_selected_object_index = -1; + DONUT_INFO("Removed object from scene"); + } + } + + void WorldBuilderState::clear_scene() + { + m_scene.objs.clear(); + m_selected_object_index = -1; + DONUT_INFO("Cleared scene"); + } + + void WorldBuilderState::save_scene() + { + std::ofstream file("scene.txt"); + + if (file.is_open()) + { + file << m_scene.objs.size() << std::endl; + for (const auto& obj : m_scene.objs) + { + file << obj.m_centre.x << " " << obj.m_centre.y << " " << obj.m_centre.z << " "; + file << obj.m_radius << " "; + file << obj.m_material.m_color.x << " " << obj.m_material.m_color.y << " " << obj.m_material.m_color.z << " "; + file << obj.m_material.m_specular << " " << obj.m_material.m_emission << std::endl; + } + file.close(); + DONUT_INFO("Scene saved to scene.txt"); + } + else + DONUT_ERROR("Failed to save scene"); + } + + void WorldBuilderState::load_scene() + { + std::ifstream file("scene.txt"); + if (file.is_open()) + { + m_scene.objs.clear(); + m_selected_object_index = -1; + + size_t object_count; + file >> object_count; + + for (size_t i = 0; i < object_count; ++i) + { + glm::vec3 position; + float radius; + glm::vec3 color; + float specular, emission; + + file >> position.x >> position.y >> position.z; + file >> radius; + file >> color.x >> color.y >> color.z; + file >> specular >> emission; + + Material material(color, specular, emission); + Object sphere(position, radius, material); + m_scene.objs.push_back(sphere); + } + + file.close(); + DONUT_INFO("Scene loaded from scene.txt"); + } + else + DONUT_ERROR("Failed to load scene"); + } + + void WorldBuilderState::render_scene() + { + GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + RenderCommand::set_viewport(0, 0, width, height); + RenderCommand::enable_depth_test(); + + static Ref sphere_vao = nullptr; + static Ref sphere_shader = Ref(Shader::create("assets/shaders/sphere.glsl")); + + if (!sphere_vao) + { + std::vector vertices; + std::vector indices; + + const int segments = 32; + const int rings = 16; + + for (int ring = 0; ring <= rings; ++ring) + { + float phi = static_cast(M_PI) * ring / rings; + float sin_phi = sin(phi); + float cos_phi = cos(phi); + + for (int segment = 0; segment <= segments; ++segment) + { + float theta = 2.0f * static_cast(M_PI) * segment / segments; + float sin_theta = sin(theta); + float cos_theta = cos(theta); + + float x = cos_theta * sin_phi; + float y = cos_phi; + float z = sin_theta * sin_phi; + + float nx = x; + float ny = y; + float nz = z; + + vertices.push_back(x); + vertices.push_back(y); + vertices.push_back(z); + vertices.push_back(nx); + vertices.push_back(ny); + vertices.push_back(nz); + } + } + + for (int ring = 0; ring < rings; ++ring) + { + for (int segment = 0; segment < segments; ++segment) + { + uint32_t first = ring * (segments + 1) + segment; + uint32_t second = first + segments + 1; + + indices.push_back(first); + indices.push_back(second); + indices.push_back(first + 1); + + indices.push_back(second); + indices.push_back(second + 1); + indices.push_back(first + 1); + } + } + + auto vertex_buffer = Ref(VertexBuffer::create(vertices.data(), static_cast(vertices.size() * sizeof(float)))); + VertexBufferLayout layout; + layout.push(3); + layout.push(3); + vertex_buffer->set_layout(layout); + + auto index_buffer = Ref(IndexBuffer::create(indices.data(), static_cast(indices.size()))); + + sphere_vao = Ref(VertexArray::create()); + sphere_vao->add_vertex_buffer(vertex_buffer); + sphere_vao->set_index_buffer(index_buffer); + } + + glm::mat4 view = m_camera.get_view_matrix(); + glm::mat4 projection = m_camera.get_projection_matrix(); + glm::mat4 view_projection = projection * view; + + glm::vec3 light_pos = m_scene.m_light_pos; + glm::vec3 camera_pos = m_camera.get_orbital_position(); + + sphere_shader->bind(); + sphere_shader->set_mat4("u_ViewProjection", view_projection); + sphere_shader->set_float3("u_LightPos", light_pos); + sphere_shader->set_float3("u_CameraPos", camera_pos); + + for (size_t i = 0; i < m_scene.objs.size(); ++i) + { + const auto& obj = m_scene.objs[i]; + + glm::mat4 transform = glm::translate(glm::mat4(1.0f), obj.m_centre); + transform = glm::scale(transform, glm::vec3(obj.m_radius)); + + sphere_shader->set_mat4("u_Transform", transform); + + if (m_selected_object_index == static_cast(i)) + { + glm::vec3 highlight_color = obj.m_material.m_color * 1.5f; + highlight_color = glm::clamp(highlight_color, 0.0f, 1.0f); + sphere_shader->set_float3("u_Color", highlight_color); + sphere_shader->set_float("u_Emission", 0.2f); + } + else + { + sphere_shader->set_float3("u_Color", obj.m_material.m_color); + sphere_shader->set_float("u_Emission", obj.m_material.m_emission); + } + + sphere_shader->set_float("u_Specular", obj.m_material.m_specular); + + sphere_vao->bind(); + RenderCommand::draw_indexed(sphere_vao); + } + + RenderCommand::disable_depth_test(); + } +}; diff --git a/src/states/world_builder_state.h b/src/states/world_builder_state.h new file mode 100644 index 0000000..c587fbe --- /dev/null +++ b/src/states/world_builder_state.h @@ -0,0 +1,65 @@ +#pragma once + +#include "core/camera.h" +#include "core/state.h" +#include "core/event.h" +#include "core/log.h" + +#include "engine/scene.h" +#include "engine/object.h" + +#include "rendering/renderer.h" +#include "rendering/shader.h" +#include "rendering/vertex_array.h" + +#include +#include + +#include +#include + +namespace Donut +{ + class WorldBuilderState + : public State + { + public: + ~WorldBuilderState() = default; + + void on_enter() override; + void on_exit() override; + void on_update(float delta_time) override; + void on_render() override; + void on_im_ui_render() override; + void on_event(Event& event) override; + private: + void add_sphere(); + void remove_selected_object(); + void clear_scene(); + void save_scene(); + void load_scene(); + void render_scene(); + private: + Scene m_scene; + Camera m_camera; + bool m_initialized = false; + + glm::vec3 m_new_object_position = glm::vec3(0.0f, 0.0f, 0.0f); + float m_new_object_radius = 1.0f; + glm::vec3 m_new_object_color = glm::vec3(1.0f, 1.0f, 1.0f); + float m_new_object_specular = 0.5f; + float m_new_object_emission = 0.0f; + + int m_selected_object_index = -1; + bool m_camera_dragging = false; + glm::vec2 m_last_mouse_pos = glm::vec2(0.0f, 0.0f); + + ImGuizmo::OPERATION m_gizmo_operation = ImGuizmo::TRANSLATE; + ImGuizmo::MODE m_gizmo_mode = ImGuizmo::LOCAL; + + bool m_show_object_list = true; + bool m_show_object_creator = true; + bool m_show_scene_info = true; + bool m_show_gizmo_controls = true; + }; +}; -- cgit v1.3