diff options
Diffstat (limited to 'src/core')
| -rw-r--r-- | src/core/application.cpp | 276 | ||||
| -rw-r--r-- | src/core/application.h | 8 |
2 files changed, 184 insertions, 100 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp index f9e52a4..ae8a5d9 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -13,6 +13,7 @@ #include <ImGuizmo.h> #include "platform/vulkan/vulkan_renderer.h" +#include "platform/opengl/opengl_renderer.h" #ifdef __APPLE__ #include "platform/metal/metal_context.h" @@ -60,12 +61,12 @@ namespace Donut { while (m_running) { - if (m_vulkan_renderer) + if (m_renderer) { glfwPollEvents(); // before the ImGui frame so input is current if (!m_minimized) - m_vulkan_renderer->draw_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f), - [this] { build_vulkan_ui(); }); + m_renderer->draw_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f), + [this] { build_ui(); }); } else { @@ -79,86 +80,138 @@ namespace Donut } } - auto Application::build_vulkan_ui() -> void + // One-time ImGui theme: rounded, roomy, dark with a warm accretion-disk accent. + static auto apply_donut_style() -> void { - ImGui::Begin("Donut - Vulkan backend"); - ImGui::Text("Geodesic black hole through Vulkan (MoltenVK)."); - ImGui::Text("%.1f FPS", ImGui::GetIO().Framerate); - ImGui::Separator(); + ImGuiStyle& s = ImGui::GetStyle(); + s.WindowRounding = 7.0f; s.ChildRounding = 5.0f; s.FrameRounding = 4.0f; + s.GrabRounding = 4.0f; s.PopupRounding = 4.0f; s.ScrollbarRounding = 5.0f; s.TabRounding = 4.0f; + s.WindowPadding = ImVec2(12, 12); s.FramePadding = ImVec2(9, 5); + s.ItemSpacing = ImVec2(9, 8); s.ItemInnerSpacing = ImVec2(7, 5); + s.WindowBorderSize = 0.0f; s.FrameBorderSize = 0.0f; s.WindowTitleAlign = ImVec2(0.02f, 0.5f); - if (m_vulkan_renderer) - { - bool scene = m_vulkan_renderer->is_scene_mode(); - ImGui::Text("View"); - if (ImGui::RadioButton("Black hole", !scene)) m_vulkan_renderer->set_scene_mode(false); - ImGui::SameLine(); - if (ImGui::RadioButton("Scene", scene)) m_vulkan_renderer->set_scene_mode(true); - ImGui::Separator(); + const ImVec4 amber = ImVec4(0.98f, 0.62f, 0.20f, 1.00f); + const ImVec4 amberHi = ImVec4(1.00f, 0.73f, 0.36f, 1.00f); + ImVec4* c = s.Colors; + c[ImGuiCol_WindowBg] = ImVec4(0.07f, 0.08f, 0.10f, 0.97f); + c[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.11f, 0.13f, 0.55f); + c[ImGuiCol_PopupBg] = ImVec4(0.09f, 0.10f, 0.12f, 0.98f); + c[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f); + c[ImGuiCol_TitleBgActive] = ImVec4(0.13f, 0.14f, 0.17f, 1.00f); + c[ImGuiCol_Text] = ImVec4(0.90f, 0.91f, 0.93f, 1.00f); + c[ImGuiCol_TextDisabled] = ImVec4(0.48f, 0.50f, 0.54f, 1.00f); + c[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f); + c[ImGuiCol_FrameBgHovered] = ImVec4(0.22f, 0.24f, 0.28f, 1.00f); + c[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.28f, 0.33f, 1.00f); + c[ImGuiCol_Header] = ImVec4(0.17f, 0.19f, 0.23f, 1.00f); + c[ImGuiCol_HeaderHovered] = ImVec4(0.24f, 0.27f, 0.32f, 1.00f); + c[ImGuiCol_HeaderActive] = ImVec4(0.28f, 0.31f, 0.37f, 1.00f); + c[ImGuiCol_Button] = ImVec4(0.20f, 0.22f, 0.26f, 1.00f); + c[ImGuiCol_ButtonHovered] = ImVec4(0.27f, 0.30f, 0.35f, 1.00f); + c[ImGuiCol_ButtonActive] = amber; + c[ImGuiCol_SliderGrab] = amber; + c[ImGuiCol_SliderGrabActive]= amberHi; + c[ImGuiCol_CheckMark] = amberHi; + c[ImGuiCol_Separator] = ImVec4(0.20f, 0.22f, 0.26f, 1.00f); + c[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f); + } - if (scene) - { - ImGui::TextDisabled("Drag to orbit, scroll to zoom. HDRI skybox behind."); - ImGui::Separator(); + auto Application::build_ui() -> void + { + static bool styled = false; + if (!styled) { apply_donut_style(); styled = true; } + if (!m_renderer) return; + RendererBackend& r = *m_renderer; - auto& objs = m_vulkan_renderer->scene_objects(); - static int sel = 0; - if (sel >= (int)objs.size()) sel = (int)objs.size() - 1; + // Full-viewport dock space with a pass-through centre, so the panel can be + // docked to any edge while the render shows through the middle. + ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode); - ImGui::Text("Spheres (%d)", (int)objs.size()); - if (ImGui::Button("Add") && objs.size() < 64) - { - SceneObject o; - o.position = glm::vec3(((int)objs.size() % 5) * 5.0f - 10.0f, 2.0f, ((int)objs.size() / 5) * 5.0f); - o.radius = 1.5f; - o.color = glm::vec3(0.35f + 0.12f * (objs.size() % 5), 0.55f, 0.9f - 0.12f * (objs.size() % 4)); - objs.push_back(o); - sel = (int)objs.size() - 1; - } - ImGui::SameLine(); - if (ImGui::Button("Delete") && !objs.empty()) - { - objs.erase(objs.begin() + sel); - if (sel >= (int)objs.size()) sel = (int)objs.size() - 1; - } + ImGui::SetNextWindowSize(ImVec2(340, 580), ImGuiCond_FirstUseEver); + ImGui::Begin("Donut \xc2\xb7 Black Hole"); - ImGui::BeginChild("obj_list", ImVec2(0, 90), true); - for (int i = 0; i < (int)objs.size(); ++i) - { - std::string label = "Sphere " + std::to_string(i); - if (ImGui::Selectable(label.c_str(), sel == i)) sel = i; - } - ImGui::EndChild(); + // --- header: device + performance ----------------------------------- + ImGuiIO& io = ImGui::GetIO(); + ImGui::TextDisabled("%s", r.device_name().empty() ? "GPU" : r.device_name().c_str()); + ImGui::Text("%.0f FPS", io.Framerate); + ImGui::SameLine(); ImGui::TextDisabled("%.1f ms/frame", io.Framerate > 0 ? 1000.0f / io.Framerate : 0.0f); + ImGui::Spacing(); - m_vulkan_renderer->set_selected_object(sel); - if (sel >= 0 && sel < (int)objs.size()) - { - SceneObject& o = objs[sel]; - ImGui::DragFloat3("Position", &o.position.x, 0.1f); - ImGui::DragFloat("Radius", &o.radius, 0.05f, 0.1f, 20.0f); - ImGui::ColorEdit3("Color", &o.color.x); - } + if (ImGui::CollapsingHeader("Renderer", ImGuiTreeNodeFlags_DefaultOpen)) + { + auto& s = SettingsManager::get_settings(); + const char* apis[] = { "OpenGL", "Vulkan" }; + int cur = (s.graphics.render_api == "Vulkan") ? 1 : 0; + if (ImGui::Combo("Graphics API", &cur, apis, 2)) + { + s.graphics.render_api = apis[cur]; + SettingsManager::save_settings(); } + const char* running = (RendererAPI::get_api() == RendererAPI::API::Vulkan) ? "Vulkan" : "OpenGL"; + if (s.graphics.render_api != running) + ImGui::TextColored(ImVec4(0.98f, 0.62f, 0.20f, 1.0f), "Restart to apply (%s running)", running); else + ImGui::TextDisabled("Active backend: %s", running); + } + + ImGui::Spacing(); + bool scene = r.is_scene_mode(); + ImGui::TextDisabled("VIEW"); + if (ImGui::RadioButton("Black hole", !scene)) r.set_scene_mode(false); + ImGui::SameLine(); + if (ImGui::RadioButton("Scene", scene)) r.set_scene_mode(true); + ImGui::Separator(); + + if (scene) + { + build_scene_ui(); + } + else + { + BlackHoleParams& bh = r.black_hole_params(); + + if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) { - bool free_fly = m_vulkan_renderer->is_free_fly(); - ImGui::Text("Camera"); - if (ImGui::RadioButton("Orbital", !free_fly)) m_vulkan_renderer->set_free_fly(false); + bool ff = r.is_free_fly(); + if (ImGui::RadioButton("Orbital", !ff)) r.set_free_fly(false); ImGui::SameLine(); - if (ImGui::RadioButton("Free-fly", free_fly)) m_vulkan_renderer->set_free_fly(true); - ImGui::TextDisabled(free_fly ? "WASD move | Q/E down-up | Shift boost | drag to look" - : "Drag to orbit | scroll to zoom"); - ImGui::Separator(); + if (ImGui::RadioButton("Free-fly", ff)) r.set_free_fly(true); + ImGui::SameLine(); + if (ImGui::Button("Reset view")) r.reset_camera(); + ImGui::SliderFloat("FOV", &bh.fov_degrees, 20.0f, 90.0f, "%.0f\xc2\xb0"); + ImGui::TextDisabled(ff ? "WASD move | Q/E down-up | Shift boost | drag look" + : "Drag to orbit | scroll to zoom"); + } + + if (ImGui::CollapsingHeader("Accretion disk", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::SliderFloat("Temperature", &bh.temperature, 2000.0f, 15000.0f, "%.0f K"); + ImGui::SliderFloat("Brightness", &bh.brightness, 0.0f, 3.0f, "%.2f"); + ImGui::SliderFloat("Inner radius", &bh.disk_inner_rs, 3.0f, 12.0f, "%.1f r_s"); + if (bh.disk_outer_rs < bh.disk_inner_rs + 0.5f) bh.disk_outer_rs = bh.disk_inner_rs + 0.5f; + ImGui::SliderFloat("Outer radius", &bh.disk_outer_rs, bh.disk_inner_rs + 0.5f, 25.0f, "%.1f r_s"); + ImGui::SliderFloat("Turbulence", &bh.turbulence, 0.0f, 2.0f, "%.2f"); + ImGui::TextDisabled("Inner edge is the ISCO (3 r_s). Novikov-Thorne profile."); + } + if (ImGui::CollapsingHeader("Quality")) + { + ImGui::SliderInt("Integration steps", &bh.quality_steps, 2000, 15000); + ImGui::TextDisabled("Deeper lensing at higher cost. Settled frame is 4x supersampled."); + } + + if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen)) + { auto& hdri = HDRIManager::get(); - std::string current = m_vulkan_renderer->current_hdri(); + std::string current = r.current_hdri(); std::string preview = hdri.get_hdri_name(current); - if (ImGui::BeginCombo("HDRI", preview.c_str())) + if (ImGui::BeginCombo("Starfield", preview.c_str())) { for (const auto& path : hdri.get_available_hdri()) { bool selected = (path == current); if (ImGui::Selectable(hdri.get_hdri_name(path).c_str(), selected)) - m_vulkan_renderer->set_hdri(path); + r.set_hdri(path); if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); @@ -170,6 +223,52 @@ namespace Donut ImGui::End(); } + auto Application::build_scene_ui() -> void + { + RendererBackend& r = *m_renderer; + if (!ImGui::CollapsingHeader("Objects", ImGuiTreeNodeFlags_DefaultOpen)) + return; + + ImGui::TextDisabled("Drag to orbit, scroll to zoom. HDRI skybox behind."); + auto& objs = r.scene_objects(); + static int sel = 0; + if (sel >= (int)objs.size()) sel = (int)objs.size() - 1; + + ImGui::Text("Spheres (%d)", (int)objs.size()); + if (ImGui::Button("Add") && objs.size() < 64) + { + SceneObject o; + o.position = glm::vec3(((int)objs.size() % 5) * 5.0f - 10.0f, 2.0f, ((int)objs.size() / 5) * 5.0f); + o.radius = 1.5f; + o.color = glm::vec3(0.35f + 0.12f * (objs.size() % 5), 0.55f, 0.9f - 0.12f * (objs.size() % 4)); + objs.push_back(o); + sel = (int)objs.size() - 1; + } + ImGui::SameLine(); + if (ImGui::Button("Delete") && !objs.empty()) + { + objs.erase(objs.begin() + sel); + if (sel >= (int)objs.size()) sel = (int)objs.size() - 1; + } + + ImGui::BeginChild("obj_list", ImVec2(0, 90), true); + for (int i = 0; i < (int)objs.size(); ++i) + { + std::string label = "Sphere " + std::to_string(i); + if (ImGui::Selectable(label.c_str(), sel == i)) sel = i; + } + ImGui::EndChild(); + + r.set_selected_object(sel); + if (sel >= 0 && sel < (int)objs.size()) + { + SceneObject& o = objs[sel]; + ImGui::DragFloat3("Position", &o.position.x, 0.1f); + ImGui::DragFloat("Radius", &o.radius, 0.05f, 0.1f, 20.0f); + ImGui::ColorEdit3("Color", &o.color.x); + } + } + auto Application::close() -> void { m_running = false; @@ -192,9 +291,9 @@ namespace Donut else m_minimized = false; - if (m_vulkan_renderer) + if (m_renderer) { - m_vulkan_renderer->on_resize(e.get_width(), e.get_height()); + m_renderer->resize(e.get_width(), e.get_height()); } else { @@ -237,44 +336,29 @@ namespace Donut auto Application::on_init() -> void { - // (Logger, settings and API selection happen in the constructor.) + // (Logger, settings and API selection happen in the constructor.) Both + // backends run the SAME application through the RendererBackend interface, + // so the app behaves identically regardless of the active graphics API. + int w = 0, h = 0; + glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h); if (RendererAPI::get_api() == RendererAPI::API::Vulkan) - { - int w = 0, h = 0; - glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h); - m_vulkan_renderer = create_scope<VulkanRenderer>(); - if (!m_vulkan_renderer->init(m_window->get_native_window(), w, h)) - DONUT_ERROR("Vulkan renderer initialization failed"); - else - m_vulkan_renderer->init_im_gui(); - // Scene rendering hooks in here in the B-3 phase; the Vulkan path now - // clears, presents, and draws the ImGui UI. - return; - } - - Renderer::init(); - m_window->init_im_gui(); - - Renderer::on_window_resize(1280, 720); - RenderCommand::set_face_culling(false); - - m_engine = create_scope<Engine>(); - m_engine->set_window_dimensions(1280, 720); + m_renderer = create_scope<VulkanRenderer>(); + else + m_renderer = create_scope<OpenGLRenderer>(); - m_state_manager = create_scope<StateManager>(); - m_state_manager->register_state("Config", create_scope<ConfigState>()); - m_state_manager->register_state("Simulation", create_scope<SimulationState>()); - m_state_manager->register_state("WorldBuilder", create_scope<WorldBuilderState>()); - m_state_manager->switch_to_state("Config"); + if (!m_renderer->init(m_window->get_native_window(), w, h)) + DONUT_ERROR("Renderer initialization failed"); + else + m_renderer->init_ui(); } auto Application::on_shutdown() -> void { - if (m_vulkan_renderer) + if (m_renderer) { - m_vulkan_renderer->shutdown(); - m_vulkan_renderer.reset(); + m_renderer->shutdown(); + m_renderer.reset(); } else { diff --git a/src/core/application.h b/src/core/application.h index 06c4e94..4c377f0 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -7,11 +7,10 @@ #include "log.h" #include "engine/engine.h" +#include "rendering/renderer_backend.h" namespace Donut { - class VulkanRenderer; // live-window Vulkan backend (platform/vulkan) - class Application { public: @@ -34,13 +33,14 @@ namespace Donut auto on_render() -> void; auto on_event(Event& event) -> void; auto setup_docking_layout() -> void; - auto build_vulkan_ui() -> void; // ImGui UI built each frame on the Vulkan path + auto build_ui() -> void; // shared ImGui panel, drives the active RendererBackend + auto build_scene_ui() -> void; // scene-view (object list) section of the UI private: Scope<StateManager> m_state_manager; Scope<Window> m_window; Scope<Engine> m_engine; - Scope<VulkanRenderer> m_vulkan_renderer; // non-null only when the Vulkan API is selected + Scope<RendererBackend> m_renderer; // active backend (non-null once a backend runs the app) bool m_running; bool m_minimized; |
