aboutsummaryrefslogtreecommitdiff
path: root/src/states
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-20 02:27:02 +0200
committerhachem <im@hachem.wtf>2026-08-20 02:40:07 +0200
commit5b361d81dbd2af0d0e99b9eb1adebea76ff05db0 (patch)
tree34db1df17e8c7248aeeb92156d635d31c2484e3f /src/states
parente3abaaf59777258ba06705135a0cafcb5918ad12 (diff)
[chore]: MASSIVE refactor + more vulkan bs
Diffstat (limited to 'src/states')
-rw-r--r--src/states/config_state.cpp307
-rw-r--r--src/states/config_state.h47
-rw-r--r--src/states/simulation_state.cpp386
-rw-r--r--src/states/simulation_state.h27
-rw-r--r--src/states/world_builder_state.cpp1113
-rw-r--r--src/states/world_builder_state.h96
6 files changed, 1976 insertions, 0 deletions
diff --git a/src/states/config_state.cpp b/src/states/config_state.cpp
new file mode 100644
index 0000000..d5fca87
--- /dev/null
+++ b/src/states/config_state.cpp
@@ -0,0 +1,307 @@
+#include "config_state.h"
+#include "rendering/renderer.h"
+#include "core/application.h"
+#include "core/theme_manager.h"
+#include "core/settings_manager.h"
+
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <glad/glad.h>
+
+namespace Donut
+{
+ auto ConfigState::on_enter() -> void
+ {
+ DONUT_INFO("Entering Config State");
+
+ 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_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;
+ }
+
+ auto ConfigState::on_exit() -> void
+ {
+ DONUT_INFO("Exiting Config State");
+ }
+
+ auto ConfigState::on_update(float delta_time) -> void
+ {
+ if (m_show_restart_message)
+ {
+ m_restart_message_timer += delta_time;
+ if (m_restart_message_timer > 3.0f)
+ {
+ m_show_restart_message = false;
+ m_restart_message_timer = 0.0f;
+ }
+ }
+ }
+
+ auto ConfigState::on_render() -> void
+ {
+ Renderer::set_clear_color({ 0.1f, 0.1f, 0.1f, 1.0f });
+ Renderer::clear();
+ }
+
+ auto ConfigState::on_event(Event& event) -> void
+ {
+ }
+
+ auto ConfigState::on_im_ui_render() -> void
+ {
+ ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver);
+ ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f),
+ ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
+
+ ImGui::Begin("Donut Configuration", nullptr, ImGuiWindowFlags_NoCollapse);
+
+ ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
+ ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "Donut Configuration");
+ ImGui::PopFont();
+ ImGui::Separator();
+
+ ImGui::Columns(2, "ConfigColumns", true);
+
+ float available_height = ImGui::GetWindowHeight() - 140;
+
+ ImGui::BeginChild("GraphicsSettings", ImVec2(0, available_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Graphics Settings");
+ ImGui::Separator();
+
+ ImGui::Text("Render API:");
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(requires restart)");
+
+ const char* api_names[] = { "OpenGL", "Vulkan" };
+ static int current_api = (int)m_selected_api - 1;
+
+ if (ImGui::BeginCombo("##RenderAPI", api_names[current_api]))
+ {
+ for (int i = 0; i < IM_ARRAYSIZE(api_names); i++)
+ {
+ const bool is_selected = (current_api == i);
+ if (ImGui::Selectable(api_names[i], is_selected))
+ {
+ current_api = i;
+ m_selected_api = (RendererAPI::API)(i + 1);
+ m_show_restart_message = true;
+ m_restart_message_timer = 0.0f;
+ }
+
+ if (is_selected)
+ ImGui::SetItemDefaultFocus();
+ }
+
+ ImGui::EndCombo();
+ }
+
+ ImGui::Text("Current API: ");
+ ImGui::SameLine();
+ const char* current_api_name = (Renderer::get_api() == RendererAPI::API::OpenGL) ? "OpenGL" : "Vulkan";
+ ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), current_api_name);
+
+ ImGui::Spacing();
+
+ ImGui::Checkbox("Enable VSync", &m_v_sync_enabled);
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(recommended)");
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Display");
+ ImGui::Separator();
+
+ ImGui::Text("Window Size: 1280x720");
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Fullscreen: Not implemented yet");
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "System Info");
+ ImGui::Separator();
+
+ ImGui::Text("OpenGL Version: %s", glGetString(GL_VERSION));
+ ImGui::Text("GPU: %s", glGetString(GL_RENDERER));
+ ImGui::Text("Vendor: %s", glGetString(GL_VENDOR));
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Performance");
+ ImGui::Separator();
+
+ ImGui::SliderInt("Target FPS", &m_target_fps, 30, 120, "%d FPS");
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Target frame rate for the simulation");
+
+ ImGui::Checkbox("Show FPS Counter", &m_show_fps);
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Display current FPS in the simulation");
+
+ ImGui::Checkbox("Show Performance Metrics", &m_show_performance_metrics);
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Show detailed performance information");
+
+ ImGui::Checkbox("Show Debug Info", &m_show_debug_info);
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Display debug information and statistics");
+
+ ImGui::Checkbox("Enable Anti-Aliasing", &m_enable_anti_aliasing);
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Enable anti-aliasing for smoother rendering");
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Theme");
+ ImGui::Separator();
+
+ const char* theme_names[] = { "Dark", "Light", "Blue" };
+ if (ImGui::Combo("UI Theme", &m_selected_theme, theme_names, IM_ARRAYSIZE(theme_names)))
+ ThemeManager::set_theme(static_cast<Theme>(m_selected_theme));
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Choose the application theme");
+
+ ImGui::EndChild();
+
+ ImGui::NextColumn();
+
+ ImGui::BeginChild("SimulationSettings", ImVec2(0, available_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation Settings");
+ ImGui::Separator();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Quality");
+ ImGui::Separator();
+
+ ImGui::SliderInt("Compute Height", &m_compute_height, 64, 2048, "%d px");
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Resolution of the compute shader. Higher values give better quality but lower performance.");
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Higher = better quality, lower performance");
+
+ ImGui::SliderInt("Max Steps (Moving)", &m_max_steps_moving, 1000, 60000, "%d");
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Maximum ray marching steps when camera is moving");
+
+ ImGui::SliderInt("Max Steps (Static)", &m_max_steps_static, 1000, 30000, "%d");
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Maximum ray marching steps when camera is stationary");
+
+ ImGui::SliderFloat("Early Exit Distance", &m_early_exit_distance, 1e11f, 1e13f, "%.2e");
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Distance at which ray marching stops to improve performance");
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Distance at which ray marching stops");
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics");
+ ImGui::Separator();
+
+ ImGui::Checkbox("Enable Gravity", &m_gravity_enabled);
+
+ ImGui::EndChild();
+
+ ImGui::Columns(1);
+
+ ImGui::Spacing();
+ ImGui::Separator();
+ ImGui::Spacing();
+
+ float button_width = (ImGui::GetWindowWidth() - 120) / 5.0f;
+
+ 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();
+
+ ImGui::SameLine();
+ if (ImGui::Button("Apply Settings", ImVec2(button_width, 35)))
+ apply_settings();
+
+ ImGui::SameLine();
+ if (ImGui::Button("Save Settings", ImVec2(button_width, 35)))
+ {
+ apply_settings();
+ DONUT_INFO("Settings saved manually");
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Exit", ImVec2(button_width, 35)))
+ Application::get().close();
+
+ if (m_show_restart_message)
+ {
+ ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10);
+ ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
+ ImGui::TextWrapped("Warning: Render API changed! Please restart the application for changes to take effect.");
+ ImGui::PopStyleColor();
+ }
+
+ ImGui::Spacing();
+ ImGui::Separator();
+ ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Donut Engine v1.0.0 | Black Hole Simulation");
+
+ ImGui::End();
+ }
+
+ auto ConfigState::apply_settings() -> void
+ {
+ SimulationSettings sim_settings;
+ sim_settings.target_fps = m_target_fps;
+ sim_settings.compute_height = m_compute_height;
+ sim_settings.max_steps_moving = m_max_steps_moving;
+ sim_settings.max_steps_static = m_max_steps_static;
+ sim_settings.early_exit_distance = m_early_exit_distance;
+ sim_settings.gravity_enabled = m_gravity_enabled;
+
+ GraphicsSettings gfx_settings;
+ gfx_settings.render_api = (m_selected_api == RendererAPI::API::Vulkan) ? "Vulkan" : "OpenGL";
+ gfx_settings.v_sync_enabled = m_v_sync_enabled;
+ gfx_settings.show_fps = m_show_fps;
+ gfx_settings.show_performance_metrics = m_show_performance_metrics;
+ gfx_settings.show_debug_info = m_show_debug_info;
+ gfx_settings.enable_anti_aliasing = m_enable_anti_aliasing;
+ gfx_settings.selected_theme = (m_selected_theme == 1) ? "Light" :
+ (m_selected_theme == 2) ? "Blue" : "Dark";
+
+ SettingsManager::set_simulation_settings(sim_settings);
+ SettingsManager::set_graphics_settings(gfx_settings);
+
+ RendererAPI::set_api(m_selected_api);
+ DONUT_INFO("Settings applied and saved");
+ }
+
+ auto ConfigState::reset_to_defaults() -> void
+ {
+ m_selected_api = RendererAPI::API::OpenGL;
+ m_target_fps = 60;
+ m_compute_height = 512;
+ m_max_steps_moving = 30000;
+ m_max_steps_static = 15000;
+ m_early_exit_distance = 5e12f;
+ m_gravity_enabled = true;
+ m_v_sync_enabled = true;
+ m_show_fps = true;
+ m_show_performance_metrics = true;
+ m_show_debug_info = false;
+ m_enable_anti_aliasing = true;
+ m_selected_theme = 0;
+
+ apply_settings();
+ DONUT_INFO("Settings reset to defaults and saved");
+ }
+};
diff --git a/src/states/config_state.h b/src/states/config_state.h
new file mode 100644
index 0000000..e64889b
--- /dev/null
+++ b/src/states/config_state.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "core/state.h"
+#include "core/event.h"
+#include "core/log.h"
+#include "rendering/renderer.h"
+
+namespace Donut
+{
+ class ConfigState
+ : public State
+ {
+ public:
+ ~ConfigState() = default;
+
+ auto on_enter() -> void override;
+ auto on_exit() -> void override;
+ auto on_update(float delta_time) -> void override;
+ auto on_render() -> void override;
+ auto on_im_ui_render() -> void override;
+ auto on_event(Event& event) -> void override;
+
+ private:
+ auto apply_settings() -> void;
+ auto reset_to_defaults() -> void;
+
+ private:
+ RendererAPI::API m_selected_api = RendererAPI::API::OpenGL;
+ bool m_show_restart_message = false;
+ float m_restart_message_timer = 0.0f;
+
+ int m_target_fps = 60;
+ int m_compute_height = 512;
+ int m_max_steps_moving = 30000;
+ int m_max_steps_static = 15000;
+ float m_early_exit_distance = 5e12f;
+ bool m_gravity_enabled = true;
+
+ bool m_v_sync_enabled = true;
+ bool m_show_fps = true;
+ bool m_show_performance_metrics = true;
+ bool m_show_debug_info = false;
+ bool m_enable_anti_aliasing = true;
+
+ int m_selected_theme = 0; // 0=Dark, 1=Light, 2=Blue
+ };
+}; \ No newline at end of file
diff --git a/src/states/simulation_state.cpp b/src/states/simulation_state.cpp
new file mode 100644
index 0000000..06955ff
--- /dev/null
+++ b/src/states/simulation_state.cpp
@@ -0,0 +1,386 @@
+#include "simulation_state.h"
+#include "rendering/renderer.h"
+#include "core/application.h"
+#include "core/hdri_manager.h"
+#include "core/window.h"
+#include "core/event.h"
+#include "core/settings_manager.h"
+
+#include <imgui.h>
+#include <GLFW/glfw3.h>
+#include <chrono>
+#include <iomanip>
+#include <sstream>
+#include <vector>
+
+namespace Donut
+{
+ auto SimulationState::on_enter() -> void
+ {
+ DONUT_INFO("Entering Simulation State");
+
+ const auto& settings = SettingsManager::get_settings_const();
+ auto& engine = Application::get().get_engine();
+
+ engine.set_target_fps(settings.simulation.target_fps);
+ engine.set_compute_height(settings.simulation.compute_height);
+ engine.set_max_steps_moving(settings.simulation.max_steps_moving);
+ engine.set_max_steps_static(settings.simulation.max_steps_static);
+ engine.set_early_exit_distance(settings.simulation.early_exit_distance);
+ engine.get_gravity() = settings.simulation.gravity_enabled;
+
+ engine.set_disk_thickness(settings.simulation.disk_thickness);
+ engine.set_disk_density(settings.simulation.disk_density);
+ engine.set_rotation_speed(settings.simulation.rotation_speed);
+ engine.set_blur_strength(settings.simulation.blur_strength);
+ engine.set_glow_intensity(settings.simulation.glow_intensity);
+
+ engine.update_compute_dimensions();
+ m_initialized = true;
+ }
+
+ auto SimulationState::on_exit() -> void
+ {
+ DONUT_INFO("Exiting Simulation State");
+ }
+
+ auto SimulationState::on_update(float delta_time) -> void
+ {
+ auto& engine = Application::get().get_engine();
+ engine.update_performance(delta_time);
+ engine.update_window_dimensions();
+ engine.update_physics(delta_time);
+
+ if (engine.get_camera().is_dragging())
+ {
+ GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
+ double xpos, ypos;
+ glfwGetCursorPos(window, &xpos, &ypos);
+ engine.get_camera().process_orbital_mouse_move(xpos, ypos);
+ }
+ }
+
+ auto SimulationState::on_render() -> void
+ {
+ auto& engine = Application::get().get_engine();
+ RenderCommand::set_clear_color(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
+ RenderCommand::clear();
+ RenderCommand::set_viewport(0, 0, static_cast<uint32_t>(engine.get_width()), static_cast<uint32_t>(engine.get_height()));
+
+ engine.dispatch_compute(engine.get_camera());
+ engine.draw_blur_pass();
+ }
+
+ auto SimulationState::on_event(Event& event) -> void
+ {
+ auto& engine = Application::get().get_engine();
+
+ if (event.get_event_type() == EventType::MouseButtonPressed)
+ {
+ MouseButtonPressedEvent& e = (MouseButtonPressedEvent&)event;
+ int button = e.get_mouse_button();
+
+ GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
+ Camera& camera = engine.get_camera();
+ double last_x = camera.get_last_x();
+ double last_y = camera.get_last_y();
+ glfwGetCursorPos(window, &last_x, &last_y);
+
+ camera.process_orbital_mouse_button(button, GLFW_PRESS, 0);
+ }
+ else if (event.get_event_type() == EventType::MouseButtonReleased)
+ {
+ MouseButtonReleasedEvent& e = (MouseButtonReleasedEvent&)event;
+ int button = e.get_mouse_button();
+ engine.get_camera().process_orbital_mouse_button(button, GLFW_RELEASE, 0);
+ }
+
+ if (event.get_event_type() == EventType::MouseScrolled)
+ {
+ MouseScrolledEvent& e = (MouseScrolledEvent&)event;
+ engine.get_camera().process_orbital_scroll(e.get_x_offset(), e.get_y_offset());
+ }
+
+ if (event.get_event_type() == EventType::KeyPressed)
+ {
+ KeyPressedEvent& e = (KeyPressedEvent&)event;
+ if (e.get_key_code() == GLFW_KEY_G)
+ {
+ engine.get_gravity() = !engine.get_gravity();
+ DONUT_INFO("Gravity turned {}", engine.get_gravity() ? "ON" : "OFF");
+ }
+ }
+ }
+
+ auto SimulationState::on_im_ui_render() -> void
+ {
+ auto& engine = Application::get().get_engine();
+
+ ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver);
+ ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 420, 20), ImGuiCond_FirstUseEver);
+
+ ImGui::Begin("Simulation Controls", nullptr, ImGuiWindowFlags_NoCollapse);
+
+ ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "Black Hole Simulation");
+ ImGui::SameLine();
+ if (ImGui::Button("Back to Config"))
+ Application::get().get_state_manager().switch_to_state("Config");
+ ImGui::SameLine();
+ if (ImGui::Button("Save Settings"))
+ {
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.target_fps = engine.get_target_fps();
+ settings.compute_height = engine.get_compute_height();
+ settings.max_steps_moving = engine.get_max_steps_moving();
+ settings.max_steps_static = engine.get_max_steps_static();
+ settings.early_exit_distance = engine.get_early_exit_distance();
+ settings.gravity_enabled = engine.get_gravity();
+ SettingsManager::set_simulation_settings(settings);
+ DONUT_INFO("Simulation settings saved manually");
+ }
+
+ ImGui::Separator();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Performance");
+ ImGui::Separator();
+
+ ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
+ ImGui::Text("Frame Time: %.3f ms", 1000.0f / ImGui::GetIO().Framerate);
+ ImGui::Text("Engine FPS: %.1f", engine.get_current_fps());
+
+ int target_fps = engine.get_target_fps();
+ if (ImGui::SliderInt("Target FPS", &target_fps, 30, 120))
+ {
+ engine.set_target_fps(target_fps);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.target_fps = target_fps;
+ SettingsManager::set_simulation_settings(settings);
+ }
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation Info");
+ ImGui::Separator();
+
+ ImGui::Text("Resolution: %dx%d", engine.get_width(), engine.get_height());
+ ImGui::Text("Compute Resolution: %dx%d", engine.get_compute_width(), engine.get_compute_height());
+ ImGui::Text("Objects: %zu", engine.get_objects().size());
+
+ if (ImGui::Button("Print Object Info"))
+ engine.print_object_info();
+
+ int compute_height = engine.get_compute_height();
+ if (ImGui::SliderInt("Compute Height", &compute_height, 64, 2048))
+ {
+ engine.set_compute_height(compute_height);
+ engine.update_compute_dimensions();
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.compute_height = compute_height;
+ SettingsManager::set_simulation_settings(settings);
+ }
+
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Compute Width: %d (auto-calculated)", engine.get_compute_width());
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Quality Settings");
+ ImGui::Separator();
+
+ int max_steps_moving = engine.get_max_steps_moving();
+ if (ImGui::SliderInt("Max Steps (Moving)", &max_steps_moving, 1000, 60000))
+ {
+ engine.set_max_steps_moving(max_steps_moving);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.max_steps_moving = max_steps_moving;
+ SettingsManager::set_simulation_settings(settings);
+ }
+ int max_steps_static = engine.get_max_steps_static();
+ if (ImGui::SliderInt("Max Steps (Static)", &max_steps_static, 1000, 30000))
+ {
+ engine.set_max_steps_static(max_steps_static);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.max_steps_static = max_steps_static;
+ SettingsManager::set_simulation_settings(settings);
+ }
+ float early_exit_distance = engine.get_early_exit_distance();
+ if (ImGui::SliderFloat("Early Exit Distance", &early_exit_distance, 1e11f, 1e13f, "%.2e"))
+ {
+ engine.set_early_exit_distance(early_exit_distance);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.early_exit_distance = early_exit_distance;
+ SettingsManager::set_simulation_settings(settings);
+ }
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics");
+ ImGui::Separator();
+
+ bool& gravity = engine.get_gravity();
+ if (ImGui::Checkbox("Gravity Enabled", &gravity))
+ {
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.gravity_enabled = gravity;
+ SettingsManager::set_simulation_settings(settings);
+ }
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Accretion Disk");
+ ImGui::Separator();
+
+ float disk_thickness = engine.get_disk_thickness();
+ if (ImGui::SliderFloat("Cloud Thickness", &disk_thickness, 0.1f, 2.0f, "%.2f"))
+ {
+ engine.set_disk_thickness(disk_thickness);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.disk_thickness = disk_thickness;
+ SettingsManager::set_simulation_settings(settings);
+ }
+ ImGui::TextDisabled("Thickness relative to Schwarzschild radius");
+
+ float disk_density = engine.get_disk_density();
+ if (ImGui::SliderFloat("Cloud Density", &disk_density, 0.1f, 3.0f, "%.2f"))
+ {
+ engine.set_disk_density(disk_density);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.disk_density = disk_density;
+ SettingsManager::set_simulation_settings(settings);
+ }
+ ImGui::TextDisabled("Overall density multiplier");
+
+ float rotation_speed = engine.get_rotation_speed();
+ if (ImGui::SliderFloat("Rotation Speed", &rotation_speed, 0.0f, 3.0f, "%.2f"))
+ {
+ engine.set_rotation_speed(rotation_speed);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.rotation_speed = rotation_speed;
+ SettingsManager::set_simulation_settings(settings);
+ }
+ ImGui::TextDisabled("Rotation speed multiplier (0 = no rotation)");
+
+ float blur_strength = engine.get_blur_strength();
+ if (ImGui::SliderFloat("Blur Strength", &blur_strength, 0.5f, 5.0f, "%.2f"))
+ {
+ engine.set_blur_strength(blur_strength);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.blur_strength = blur_strength;
+ SettingsManager::set_simulation_settings(settings);
+ }
+ ImGui::TextDisabled("Blur radius for glow effect");
+
+ float glow_intensity = engine.get_glow_intensity();
+ if (ImGui::SliderFloat("Glow Intensity", &glow_intensity, 0.1f, 3.0f, "%.2f"))
+ {
+ engine.set_glow_intensity(glow_intensity);
+ SimulationSettings settings = SettingsManager::get_settings_const().simulation;
+ settings.glow_intensity = glow_intensity;
+ SettingsManager::set_simulation_settings(settings);
+ }
+ ImGui::TextDisabled("Intensity of the glow effect");
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "HDRI Environment");
+ ImGui::Separator();
+
+ static int selected_hdri = 0;
+ ImGui::PushID("SimulationHDRI");
+ auto& hdri_manager = HDRIManager::get();
+ const auto& available_hdri = hdri_manager.get_available_hdri();
+
+ static std::vector<std::string> hdri_option_names;
+ static std::vector<const char*> hdri_options;
+
+ if (hdri_option_names.size() != available_hdri.size())
+ {
+ hdri_option_names.clear();
+ hdri_options.clear();
+
+ for (const auto& path : available_hdri)
+ {
+ hdri_option_names.push_back(hdri_manager.get_hdri_name(path));
+ hdri_options.push_back(hdri_option_names.back().c_str());
+ }
+ }
+
+ if (ImGui::Combo("HDRI Environment", &selected_hdri, hdri_options.data(), static_cast<int>(hdri_options.size())))
+ {
+ auto& hdri_manager = HDRIManager::get();
+ hdri_manager.set_current_hdri(available_hdri[selected_hdri]);
+ engine.set_hdri_environment(hdri_manager.get_current_hdri());
+ }
+ ImGui::TextDisabled("HDRI provides background and lighting for the simulation");
+ ImGui::PopID();
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Camera");
+ ImGui::Separator();
+
+ ImGui::Text("Position: (%.2e, %.2e, %.2e)",
+ engine.get_camera().get_orbital_position().x,
+ engine.get_camera().get_orbital_position().y,
+ engine.get_camera().get_orbital_position().z);
+ ImGui::Text("Radius: %.2e", engine.get_camera().get_orbital_radius());
+ ImGui::Text("Azimuth: %.2f", engine.get_camera().get_azimuth());
+ ImGui::Text("Elevation: %.2f", engine.get_camera().get_elevation());
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Export");
+ ImGui::Separator();
+
+ if (ImGui::Button("Export Frame (1080p)", ImVec2(-1, 30)))
+ {
+ auto& engine = Application::get().get_engine();
+ auto now = std::chrono::system_clock::now();
+ auto time_t = std::chrono::system_clock::to_time_t(now);
+ std::stringstream ss;
+ ss << "frame_1080p_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png";
+ engine.export_high_res_frame(ss.str(), 1920, 1080);
+ }
+
+ if (ImGui::Button("Export High-Res Frame (4K)", ImVec2(-1, 30)))
+ {
+ auto& engine = Application::get().get_engine();
+ auto now = std::chrono::system_clock::now();
+ auto time_t = std::chrono::system_clock::to_time_t(now);
+ std::stringstream ss;
+ ss << "high_res_frame_4k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png";
+ engine.export_high_res_frame(ss.str(), 4096, 3072);
+ }
+
+ if (ImGui::Button("Export High-Res Frame (8K)", ImVec2(-1, 30)))
+ {
+ auto& engine = Application::get().get_engine();
+ auto now = std::chrono::system_clock::now();
+ auto time_t = std::chrono::system_clock::to_time_t(now);
+ std::stringstream ss;
+ ss << "high_res_frame_8k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png";
+ engine.export_high_res_frame(ss.str(), 8192, 6144);
+ }
+
+ if (ImGui::Button("Export Ultra High-Res Frame (16K)", ImVec2(-1, 30)))
+ {
+ auto& engine = Application::get().get_engine();
+ auto now = std::chrono::system_clock::now();
+ auto time_t = std::chrono::system_clock::to_time_t(now);
+ std::stringstream ss;
+ ss << "high_res_frame_16k_" << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S") << ".png";
+ engine.export_high_res_frame(ss.str(), 16384, 12288);
+ }
+
+ ImGui::Spacing();
+
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Controls");
+ ImGui::Separator();
+
+ ImGui::Text("Left Mouse: Orbit camera");
+ ImGui::Text("Scroll: Zoom in/out");
+ ImGui::Text("G: Toggle gravity");
+ ImGui::Text("Right Mouse: Enable gravity (hold)");
+
+ ImGui::End();
+ }
+};
diff --git a/src/states/simulation_state.h b/src/states/simulation_state.h
new file mode 100644
index 0000000..625d19e
--- /dev/null
+++ b/src/states/simulation_state.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "core/state.h"
+#include "core/event.h"
+#include "core/log.h"
+#include "engine/engine.h"
+#include <vector>
+
+namespace Donut
+{
+ class SimulationState
+ : public State
+ {
+ public:
+ ~SimulationState() = default;
+
+ auto on_enter() -> void override;
+ auto on_exit() -> void override;
+ auto on_update(float delta_time) -> void override;
+ auto on_render() -> void override;
+ auto on_im_ui_render() -> void override;
+ auto on_event(Event& event) -> void override;
+
+ private:
+ bool m_initialized = false;
+ };
+};
diff --git a/src/states/world_builder_state.cpp b/src/states/world_builder_state.cpp
new file mode 100644
index 0000000..09283c3
--- /dev/null
+++ b/src/states/world_builder_state.cpp
@@ -0,0 +1,1113 @@
+#include "world_builder_state.h"
+
+#include "core/application.h"
+#include "core/window.h"
+#include "core/hdri_manager.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 <imgui.h>
+#include <ImGuizmo.h>
+#include <GLFW/glfw3.h>
+
+#include <glm/gtc/matrix_transform.hpp>
+#include <glm/gtc/type_ptr.hpp>
+
+#include <nlohmann/json.hpp>
+
+#include <numbers>
+#include <fstream>
+#include <sstream>
+#include <limits>
+#include <vector>
+
+namespace Donut
+{
+ auto WorldBuilderState::on_enter() -> void
+ {
+ 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<float>(std::numbers::pi) / 3.0f);
+ m_camera.update_orbital();
+
+ m_sphere_shader = Ref<Shader>(Shader::create("assets/shaders/Sphere.glsl"));
+ m_skybox_shader = Ref<Shader>(Shader::create("assets/shaders/Skybox.glsl"));
+ m_grid_shader = Ref<Shader>(Shader::create("assets/shaders/Grid.glsl"));
+
+ if (!m_sphere_shader)
+ DONUT_ERROR("Failed to create sphere shader");
+ if (!m_skybox_shader)
+ DONUT_ERROR("Failed to create skybox shader");
+ if (!m_grid_shader)
+ DONUT_ERROR("Failed to create grid shader");
+
+ initialize_sphere_geometry();
+ initialize_skybox_geometry();
+ initialize_grid_geometry();
+
+ auto& hdri_manager = HDRIManager::get();
+ m_hdri_environment = hdri_manager.get_current_hdri();
+ if (!m_hdri_environment)
+ {
+ hdri_manager.set_current_hdri("assets/hdri/HDR_blue_nebulae-1.hdr");
+ m_hdri_environment = hdri_manager.get_current_hdri();
+ if (!m_hdri_environment)
+ DONUT_WARN("Failed to load default HDRI for WorldBuilder, using fallback");
+ }
+
+ Material black_hole_material(glm::vec3(0.0f, 0.0f, 0.0f), 0.0f, 0.0f);
+ m_black_hole = Object(glm::vec3(0.0f, 0.0f, 0.0f), 2.0f, black_hole_material);
+ m_black_hole_initialized = true;
+
+ // Set default grid size
+ m_grid_size = 10.0f;
+
+ m_initialized = true;
+ }
+
+ auto WorldBuilderState::on_exit() -> void
+ {
+ DONUT_INFO("Exiting World Builder State");
+ }
+
+ auto WorldBuilderState::on_update(float delta_time) -> void
+ {
+ if (m_camera_dragging &&
+ !ImGuizmo::IsUsing())
+ {
+ GLFWwindow* window = static_cast<GLFWwindow*>(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<float>(std::numbers::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;
+ }
+ }
+
+ auto WorldBuilderState::on_render() -> void
+ {
+ auto& hdri_manager = HDRIManager::get();
+ m_hdri_environment = hdri_manager.get_current_hdri();
+
+ RenderCommand::set_clear_color(glm::vec4(0.1f, 0.1f, 0.1f, 1.0f));
+ RenderCommand::clear();
+
+ if (m_hdri_environment)
+ render_skybox();
+
+ if (m_show_grid)
+ render_grid();
+
+ render_scene();
+ }
+
+ auto WorldBuilderState::on_event(Event& event) -> void
+ {
+ EventDispatcher dispatcher(event);
+
+ dispatcher.dispatch<MouseButtonPressedEvent>([this](MouseButtonPressedEvent& e)
+ {
+ if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT)
+ {
+ if (ImGuizmo::IsUsing() ||
+ ImGuizmo::IsOver())
+ return false;
+
+ GLFWwindow* window = static_cast<GLFWwindow*>(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 * static_cast<float>(xpos)) / static_cast<float>(width) - 1.0f;
+ float ndc_y = 1.0f - (2.0f * static_cast<float>(ypos)) / static_cast<float>(height);
+
+ glm::vec4 rayStart_NDC(ndc_x, ndc_y, -1.0f, 1.0f);
+ glm::vec4 rayEnd_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 rayStart_World = inv_vp * rayStart_NDC;
+ glm::vec4 rayEnd_World = inv_vp * rayEnd_NDC;
+
+ rayStart_World /= rayStart_World.w;
+ rayEnd_World /= rayEnd_World.w;
+
+ glm::vec3 ray_dir = glm::normalize(glm::vec3(rayEnd_World - rayStart_World));
+ glm::vec3 ray_origin = glm::vec3(rayStart_World);
+
+ float closest_distance = std::numeric_limits<float>::max();
+ int closest_object_index = -1;
+
+ if (m_black_hole_initialized)
+ {
+ glm::vec3 oc = ray_origin - m_black_hole.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) - m_black_hole.m_radius * m_black_hole.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 = -2;
+ }
+ else if (t2 > 0 && t2 < closest_distance)
+ {
+ closest_distance = t2;
+ closest_object_index = -2;
+ }
+ }
+ }
+
+ 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<int>(i);
+ }
+ else if (t2 > 0 && t2 < closest_distance)
+ {
+ closest_distance = t2;
+ closest_object_index = static_cast<int>(i);
+ }
+ }
+ }
+
+ if (closest_object_index >= 0)
+ {
+ m_selected_object_index = closest_object_index;
+ DONUT_INFO("Selected object {}", closest_object_index);
+ return true;
+ }
+ else if (closest_object_index == -2)
+ {
+ m_selected_object_index = -1;
+ DONUT_INFO("Black hole clicked (not selectable)");
+ return true;
+ }
+ else
+ {
+ m_selected_object_index = -1;
+ m_camera_dragging = true;
+ m_last_mouse_pos = glm::vec2(xpos, ypos);
+ }
+
+ return true;
+ }
+ return false;
+ });
+
+ dispatcher.dispatch<MouseButtonReleasedEvent>([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<WindowResizeEvent>([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<float>(new_width) / static_cast<float>(new_height);
+ m_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f);
+ }
+ return false;
+ });
+
+ dispatcher.dispatch<MouseScrolledEvent>([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<KeyPressedEvent>([this](KeyPressedEvent& e)
+ {
+ if (m_selected_object_index >= 0 &&
+ m_selected_object_index < static_cast<int>(m_scene.objs.size()))
+ {
+ switch (e.get_key_code())
+ {
+ case GLFW_KEY_M:
+ m_gizmo_operation = ImGuizmo::TRANSLATE;
+ return true;
+ case GLFW_KEY_R:
+ m_gizmo_operation = ImGuizmo::ROTATE;
+ return true;
+ case GLFW_KEY_S:
+ m_gizmo_operation = ImGuizmo::SCALE;
+ return true;
+ }
+ }
+ return false;
+ });
+ }
+
+ auto WorldBuilderState::on_im_ui_render() -> void
+ {
+ GLFWwindow* window = static_cast<GLFWwindow*>(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"))
+ {
+ ImGui::Text("Objects in scene: %zu/16 (+ 1 black hole)", 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<float>(std::numbers::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<float>(m_scene.objs.size());
+ m_camera.set_orbital_target(center);
+ m_camera.update_orbital();
+ }
+ }
+ }
+
+ ImGui::Spacing();
+
+ if (ImGui::CollapsingHeader("create Object"))
+ {
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "New Sphere");
+ ImGui::Separator();
+
+ ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Objects: %zu/16", m_scene.objs.size());
+
+ 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 (m_scene.objs.size() >= 16)
+ {
+ ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f);
+ ImGui::Button("add Sphere (Limit Reached)", ImVec2(ImGui::GetWindowWidth() - 20, 30));
+ ImGui::PopStyleVar();
+ }
+ else
+ {
+ if (ImGui::Button("add Sphere", ImVec2(ImGui::GetWindowWidth() - 20, 30)))
+ add_sphere();
+ }
+ }
+
+ ImGui::Spacing();
+
+ if (ImGui::CollapsingHeader("Scene Objects"))
+ {
+ if (m_black_hole_initialized)
+ {
+ ImGui::PushID(-1);
+
+ ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "Black Hole (Center)");
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Position: (0.00, 0.00, 0.00)");
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Radius: %.2f", m_black_hole.m_radius);
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Color: Black (with white outline)");
+ ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Cannot be removed or modified");
+
+ ImGui::PopID();
+ ImGui::Separator();
+ }
+
+ if (m_scene.objs.empty())
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No additional 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<int>(i));
+
+ bool is_selected = (m_selected_object_index == static_cast<int>(i));
+ if (ImGui::Selectable(("Sphere " + std::to_string(i)).c_str(), is_selected))
+ m_selected_object_index = static_cast<int>(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();
+ }
+ }
+ }
+
+ ImGui::Spacing();
+
+ if (ImGui::CollapsingHeader("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), "M = Move, R = Rotate, S = Scale");
+ }
+
+ ImGui::Spacing();
+
+ if (ImGui::CollapsingHeader("Selection Outline"))
+ {
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Outline Settings:");
+
+ ImGui::Text("Outline Color:");
+ ImGui::ColorEdit3("##OutlineColor", &m_outline_color.x);
+
+ ImGui::Text("Outline Width:");
+ ImGui::SliderFloat("##OutlineWidth", &m_outline_width, 0.01f, 0.9f, "%.2f");
+
+ ImGui::Spacing();
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "White rim around sphere silhouette");
+ }
+
+ ImGui::Spacing();
+
+ if (ImGui::CollapsingHeader("Grid Settings"))
+ {
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Grid Settings:");
+
+ ImGui::Text("Show Grid:");
+ ImGui::SameLine();
+ ImGui::Checkbox("##ShowGrid", &m_show_grid);
+
+ ImGui::Text("Grid Color:");
+ ImGui::ColorEdit3("##GridColor", &m_grid_color.x);
+
+ ImGui::Text("Grid Alpha:");
+ ImGui::SliderFloat("##GridAlpha", &m_grid_alpha, 0.0f, 1.0f, "%.2f");
+
+ ImGui::Text("Grid Size:");
+ ImGui::SliderFloat("##GridSize", &m_grid_size, 1.0f, 500.0f, "%.1f");
+
+ ImGui::Spacing();
+ if (ImGui::Button("Regenerate Grid", ImVec2(ImGui::GetWindowWidth() - 20, 25)))
+ initialize_grid_geometry();
+
+ ImGui::Spacing();
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Reference grid for spatial orientation");
+ }
+
+ ImGui::Spacing();
+
+ if (ImGui::CollapsingHeader("HDRI Environment", nullptr))
+ {
+ ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "HDRI Settings:");
+
+ static int selected_hdri = 0;
+ ImGui::PushID("WorldBuilderHDRI");
+ auto& hdri_manager = HDRIManager::get();
+ const auto& available_hdri = hdri_manager.get_available_hdri();
+
+ static std::vector<std::string> hdri_option_names;
+ static std::vector<const char*> hdri_options;
+
+ if (hdri_option_names.size() != available_hdri.size())
+ {
+ hdri_option_names.clear();
+ hdri_options.clear();
+
+ for (const auto& path : available_hdri)
+ {
+ hdri_option_names.push_back(hdri_manager.get_hdri_name(path));
+ hdri_options.push_back(hdri_option_names.back().c_str());
+ }
+ }
+
+ if (ImGui::Combo("HDRI Environment", &selected_hdri, hdri_options.data(), static_cast<int>(hdri_options.size())))
+ {
+ auto& hdri_manager = HDRIManager::get();
+ hdri_manager.set_current_hdri(available_hdri[selected_hdri]);
+ m_hdri_environment = hdri_manager.get_current_hdri();
+ }
+
+ ImGui::Spacing();
+ ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "HDRI provides background skybox and lighting for the scene");
+ ImGui::PopID();
+ }
+
+ 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("Start Simulation", ImVec2(ImGui::GetWindowWidth() - 20, 30)))
+ {
+ auto& engine = Application::get().get_engine();
+ engine.load_objects_from_scene(m_scene.objs);
+ Application::get().get_state_manager().switch_to_state("Simulation");
+ }
+
+ 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: M=Move, R=Rotate, S=Scale");
+
+ ImGui::End();
+
+ if (m_selected_object_index >= 0 &&
+ m_selected_object_index < static_cast<int>(m_scene.objs.size()) &&
+ m_selected_object_index != -2)
+ {
+ 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);
+ }
+ }
+ }
+
+ auto WorldBuilderState::add_sphere() -> void
+ {
+ if (m_scene.objs.size() >= 16)
+ {
+ DONUT_WARN("Cannot add more objects. Maximum of 16 objects reached.");
+ return;
+ }
+
+ 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_selected_object_index = static_cast<int>(m_scene.objs.size() - 1);
+
+ 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 and selected it");
+ }
+
+ auto WorldBuilderState::remove_selected_object() -> void
+ {
+ if (m_selected_object_index >= 0 && m_selected_object_index < static_cast<int>(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");
+ }
+ }
+
+ auto WorldBuilderState::clear_scene() -> void
+ {
+ m_scene.objs.clear();
+ m_selected_object_index = -1;
+ DONUT_INFO("Cleared scene (black hole remains at center)");
+ }
+
+ auto WorldBuilderState::save_scene() -> void
+ {
+ nlohmann::json scene_data;
+ scene_data["objects"] = nlohmann::json::array();
+
+ for (const auto& obj : m_scene.objs)
+ {
+ nlohmann::json sphere_data;
+ sphere_data["position"] =
+ {
+ obj.m_centre.x,
+ obj.m_centre.y,
+ obj.m_centre.z
+ };
+
+ sphere_data["color"] =
+ {
+ obj.m_material.m_color.x,
+ obj.m_material.m_color.y,
+ obj.m_material.m_color.z
+ };
+
+ sphere_data["radius"] = obj.m_radius;
+ sphere_data["specular"] = obj.m_material.m_specular;
+ sphere_data["emission"] = obj.m_material.m_emission;
+ scene_data["objects"].push_back(sphere_data);
+ }
+
+ std::ofstream file("Scene.json");
+ if (file.is_open())
+ {
+ file << scene_data.dump(4);
+ file.close();
+ DONUT_INFO("Scene saved to Scene.json (black hole always present at center)");
+ }
+ else
+ DONUT_ERROR("Failed to save scene");
+ }
+
+ auto WorldBuilderState::load_scene() -> void
+ {
+ std::ifstream file("Scene.json");
+ if (file.is_open())
+ {
+ m_scene.objs.clear();
+ m_selected_object_index = -1;
+
+ nlohmann::json scene_data;
+ file >> scene_data;
+
+ if (scene_data.contains("objects"))
+ {
+ for (const auto& sphere_data : scene_data["objects"])
+ {
+ glm::vec3 position;
+ glm::vec3 color;
+ float radius;
+ float specular;
+ float emission;
+
+ auto pos_array = sphere_data["position"];
+ position.x = pos_array[0].get<float>();
+ position.y = pos_array[1].get<float>();
+ position.z = pos_array[2].get<float>();
+
+ auto color_array = sphere_data["color"];
+ color.x = color_array[0].get<float>();
+ color.y = color_array[1].get<float>();
+ color.z = color_array[2].get<float>();
+
+ radius = sphere_data["radius"].get<float>();
+ specular = sphere_data["specular"].get<float>();
+ emission = sphere_data["emission"].get<float>();
+
+ Material material(color, specular, emission);
+ Object sphere(position, radius, material);
+ m_scene.objs.push_back(sphere);
+ }
+ }
+
+ file.close();
+ DONUT_INFO("Scene loaded from Scene.json (black hole remains at center)");
+ }
+ else
+ DONUT_ERROR("Failed to load scene");
+ }
+
+ auto WorldBuilderState::initialize_sphere_geometry() -> void
+ {
+ std::vector<float> vertices;
+ std::vector<uint32_t> indices;
+
+ const int segments = 32;
+ const int rings = 16;
+
+ for (int ring = 0; ring <= rings; ++ring)
+ {
+ float phi = static_cast<float>(std::numbers::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<float>(std::numbers::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>(VertexBuffer::create(vertices.data(), static_cast<uint32_t>(vertices.size() * sizeof(float))));
+ VertexBufferLayout layout;
+ layout.push<float>(3);
+ layout.push<float>(3);
+ vertex_buffer->set_layout(layout);
+
+ auto index_buffer = Ref<IndexBuffer>(IndexBuffer::create(indices.data(), static_cast<uint32_t>(indices.size())));
+
+ m_sphere_vao = Ref<VertexArray>(VertexArray::create());
+ m_sphere_vao->add_vertex_buffer(vertex_buffer);
+ m_sphere_vao->set_index_buffer(index_buffer);
+ }
+
+ auto WorldBuilderState::render_scene() -> void
+ {
+ GLFWwindow* window = static_cast<GLFWwindow*>(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();
+
+ 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();
+
+ m_sphere_shader->bind();
+ m_sphere_shader->set_mat4("u_ViewProjection", view_projection);
+ m_sphere_shader->set_float3("u_LightPos", light_pos);
+ m_sphere_shader->set_float3("u_CameraPos", camera_pos);
+
+ m_sphere_shader->set_float3("u_OutlineColor", m_outline_color);
+ m_sphere_shader->set_float("u_OutlineWidth", m_outline_width);
+
+ if (m_hdri_environment)
+ {
+ m_hdri_environment->bind(1);
+ m_sphere_shader->set_int("u_HDRIEnvironment", 1);
+ }
+
+ if (m_black_hole_initialized)
+ {
+ glm::mat4 black_hole_transform = glm::translate(glm::mat4(1.0f), m_black_hole.m_centre);
+ black_hole_transform = glm::scale(black_hole_transform, glm::vec3(m_black_hole.m_radius));
+
+ m_sphere_shader->set_mat4("u_Transform", black_hole_transform);
+ m_sphere_shader->set_int("u_IsSelected", 1);
+ m_sphere_shader->set_float3("u_Color", m_black_hole.m_material.m_color);
+ m_sphere_shader->set_float("u_Emission", m_black_hole.m_material.m_emission);
+ m_sphere_shader->set_float("u_Specular", m_black_hole.m_material.m_specular);
+ m_sphere_shader->set_float("u_OutlineWidth", 0.3f);
+
+ m_sphere_vao->bind();
+ RenderCommand::draw_indexed(m_sphere_vao);
+
+ m_sphere_shader->set_float("u_OutlineWidth", m_outline_width);
+ }
+
+ 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));
+
+ m_sphere_shader->set_mat4("u_Transform", transform);
+
+ bool is_selected = (m_selected_object_index == static_cast<int>(i));
+ m_sphere_shader->set_int("u_IsSelected", is_selected ? 1 : 0);
+ if (is_selected)
+ {
+ glm::vec3 highlight_color = obj.m_material.m_color * 1.5f;
+ highlight_color = glm::clamp(highlight_color, 0.0f, 1.0f);
+ m_sphere_shader->set_float3("u_Color", highlight_color);
+ m_sphere_shader->set_float("u_Emission", 0.2f);
+ }
+ else
+ {
+ m_sphere_shader->set_float3("u_Color", obj.m_material.m_color);
+ m_sphere_shader->set_float("u_Emission", obj.m_material.m_emission);
+ }
+
+ m_sphere_shader->set_float("u_Specular", obj.m_material.m_specular);
+
+ m_sphere_vao->bind();
+ RenderCommand::draw_indexed(m_sphere_vao);
+ }
+
+ RenderCommand::disable_depth_test();
+ }
+
+
+
+ auto WorldBuilderState::initialize_skybox_geometry() -> void
+ {
+ float skybox_vertices[] =
+ {
+ -1.0f, 1.0f, -1.0f,
+ -1.0f, -1.0f, -1.0f,
+ 1.0f, -1.0f, -1.0f,
+ 1.0f, -1.0f, -1.0f,
+ 1.0f, 1.0f, -1.0f,
+ -1.0f, 1.0f, -1.0f,
+
+ -1.0f, -1.0f, 1.0f,
+ -1.0f, -1.0f, -1.0f,
+ -1.0f, 1.0f, -1.0f,
+ -1.0f, 1.0f, -1.0f,
+ -1.0f, 1.0f, 1.0f,
+ -1.0f, -1.0f, 1.0f,
+
+ 1.0f, -1.0f, -1.0f,
+ 1.0f, -1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, -1.0f,
+ 1.0f, -1.0f, -1.0f,
+
+ -1.0f, -1.0f, 1.0f,
+ -1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f,
+ -1.0f, -1.0f, 1.0f,
+
+ -1.0f, 1.0f, -1.0f,
+ 1.0f, 1.0f, -1.0f,
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ -1.0f, 1.0f, 1.0f,
+ -1.0f, 1.0f, -1.0f,
+
+ -1.0f, -1.0f, -1.0f,
+ -1.0f, -1.0f, 1.0f,
+ 1.0f, -1.0f, -1.0f,
+ 1.0f, -1.0f, -1.0f,
+ -1.0f, -1.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f
+ };
+
+ auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(skybox_vertices, sizeof(skybox_vertices)));
+ VertexBufferLayout layout;
+ layout.push<float>(3);
+ vertex_buffer->set_layout(layout);
+
+ m_skybox_vao = Ref<VertexArray>(VertexArray::create());
+ m_skybox_vao->add_vertex_buffer(vertex_buffer);
+ }
+
+ auto WorldBuilderState::render_skybox() -> void
+ {
+ if (!m_skybox_shader || !m_skybox_vao || !m_hdri_environment)
+ return;
+
+ GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
+ int width, height;
+ glfwGetFramebufferSize(window, &width, &height);
+
+ RenderCommand::set_viewport(0, 0, width, height);
+ RenderCommand::disable_depth_test();
+
+ glm::mat4 view = m_camera.get_view_matrix();
+ glm::mat4 projection = m_camera.get_projection_matrix();
+
+ view = glm::mat4(glm::mat3(view));
+
+ m_skybox_shader->bind();
+ m_skybox_shader->set_mat4("u_View", view);
+ m_skybox_shader->set_mat4("u_Projection", projection);
+
+ m_hdri_environment->bind(0);
+ m_skybox_shader->set_int("u_Skybox", 0);
+
+ m_skybox_vao->bind();
+ RenderCommand::draw_arrays(36);
+
+ RenderCommand::enable_depth_test();
+ }
+
+ auto WorldBuilderState::initialize_grid_geometry() -> void
+ {
+ if (m_grid_vao)
+ {
+ m_grid_vao->unbind();
+ m_grid_vao.reset();
+ }
+
+ const float grid_size = 50.0f;
+ const int grid_lines = 101;
+ const float half_size = grid_size * 0.5f;
+ const float step = grid_size / (grid_lines - 1);
+
+ std::vector<float> vertices;
+ std::vector<uint32_t> indices;
+
+ for (int i = 0; i < grid_lines; ++i)
+ {
+ float z = -half_size + i * step;
+
+ vertices.push_back(-half_size);
+ vertices.push_back(0.0f);
+ vertices.push_back(z);
+
+ vertices.push_back(half_size);
+ vertices.push_back(0.0f);
+ vertices.push_back(z);
+
+ uint32_t base_index = static_cast<uint32_t>(vertices.size() / 3) - 2;
+ indices.push_back(base_index);
+ indices.push_back(base_index + 1);
+ }
+
+ for (int i = 0; i < grid_lines; ++i)
+ {
+ float x = -half_size + i * step;
+
+ vertices.push_back(x);
+ vertices.push_back(0.0f);
+ vertices.push_back(-half_size);
+
+ vertices.push_back(x);
+ vertices.push_back(0.0f);
+ vertices.push_back(half_size);
+
+ uint32_t base_index = static_cast<uint32_t>(vertices.size() / 3) - 2;
+ indices.push_back(base_index);
+ indices.push_back(base_index + 1);
+ }
+
+ auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(vertices.data(), static_cast<uint32_t>(vertices.size() * sizeof(float))));
+ VertexBufferLayout layout;
+ layout.push<float>(3);
+ vertex_buffer->set_layout(layout);
+
+ auto index_buffer = Ref<IndexBuffer>(IndexBuffer::create(indices.data(), static_cast<uint32_t>(indices.size())));
+
+ m_grid_vao = Ref<VertexArray>(VertexArray::create());
+ m_grid_vao->add_vertex_buffer(vertex_buffer);
+ m_grid_vao->set_index_buffer(index_buffer);
+
+ m_grid_vao->unbind();
+ }
+
+ auto WorldBuilderState::render_grid() -> void
+ {
+ if (!m_grid_shader || !m_grid_vao)
+ return;
+
+ GLFWwindow* window = static_cast<GLFWwindow*>(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();
+ RenderCommand::enable_blending();
+
+ glm::mat4 view = m_camera.get_view_matrix();
+ glm::mat4 projection = m_camera.get_projection_matrix();
+ glm::mat4 view_projection = projection * view;
+ glm::mat4 transform = glm::mat4(1.0f);
+
+ glm::vec3 camera_pos = m_camera.get_orbital_position();
+
+ m_grid_shader->bind();
+ m_grid_shader->set_mat4("u_ViewProjection", view_projection);
+ m_grid_shader->set_mat4("u_Transform", transform);
+ m_grid_shader->set_float3("u_GridColor", m_grid_color);
+ m_grid_shader->set_float("u_GridAlpha", m_grid_alpha);
+ m_grid_shader->set_float("u_GridSize", m_grid_size);
+ m_grid_shader->set_float3("u_CameraPos", camera_pos);
+
+ m_grid_vao->bind();
+ RenderCommand::draw_lines(m_grid_vao);
+
+ // Clean up state to prevent conflicts with scene rendering
+ m_grid_vao->unbind();
+ m_grid_shader->unbind();
+ RenderCommand::disable_blending();
+
+ // Ensure depth test is still enabled for scene rendering
+ RenderCommand::enable_depth_test();
+ }
+};
diff --git a/src/states/world_builder_state.h b/src/states/world_builder_state.h
new file mode 100644
index 0000000..8b4678a
--- /dev/null
+++ b/src/states/world_builder_state.h
@@ -0,0 +1,96 @@
+#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 <imgui.h>
+#include <ImGuizmo.h>
+
+#include <vector>
+#include <memory>
+
+namespace Donut
+{
+ class WorldBuilderState
+ : public State
+ {
+ public:
+ ~WorldBuilderState() = default;
+
+ auto on_enter() -> void override;
+ auto on_exit() -> void override;
+ auto on_update(float delta_time) -> void override;
+ auto on_render() -> void override;
+ auto on_im_ui_render() -> void override;
+ auto on_event(Event& event) -> void override;
+ private:
+ auto add_sphere() -> void;
+ auto remove_selected_object() -> void;
+ auto clear_scene() -> void;
+ auto save_scene() -> void;
+ auto load_scene() -> void;
+ auto render_scene() -> void;
+ auto initialize_sphere_geometry() -> void;
+ auto initialize_grid_geometry() -> void;
+ auto render_grid() -> void;
+ private:
+ Scene m_scene;
+ Camera m_camera;
+ bool m_initialized = false;
+
+ Ref<Shader> m_sphere_shader;
+ Ref<VertexArray> m_sphere_vao;
+ Ref<CubemapTexture> m_hdri_environment;
+
+ Ref<Shader> m_skybox_shader;
+ Ref<VertexArray> m_skybox_vao;
+
+ Ref<Shader> m_grid_shader;
+ Ref<VertexArray> m_grid_vao;
+
+ 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;
+ bool m_show_outline_controls = true;
+ bool m_show_grid = true;
+
+ glm::vec3 m_outline_color = glm::vec3(1.0f, 1.0f, 1.0f);
+ float m_outline_width = 0.25f;
+
+ glm::vec3 m_grid_color = glm::vec3(0.5f, 0.5f, 0.5f);
+ float m_grid_alpha = 0.5f;
+ float m_grid_size = 50.0f;
+
+ Object m_black_hole;
+ bool m_black_hole_initialized = false;
+
+ auto set_hdri_environment(Ref<CubemapTexture> hdri) -> void { m_hdri_environment = hdri; }
+ auto get_hdri_environment() const -> Ref<CubemapTexture> { return m_hdri_environment; }
+
+ auto initialize_skybox_geometry() -> void;
+ auto render_skybox() -> void;
+ };
+};