diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 60e5da8..bd861d1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -130,6 +130,10 @@ add_executable (vulkan_engine scene/camera/mode_chase.cpp scene/camera/mode_fixed.h scene/camera/mode_fixed.cpp + scene/planet/cubesphere.h + scene/planet/cubesphere.cpp + scene/planet/planet_quadtree.h + scene/planet/planet_quadtree.cpp scene/planet/planet_system.h scene/planet/planet_system.cpp # compute diff --git a/src/core/assets/manager.cpp b/src/core/assets/manager.cpp index 131e9d0..3577560 100644 --- a/src/core/assets/manager.cpp +++ b/src/core/assets/manager.cpp @@ -587,7 +587,8 @@ std::pair AssetManager::loadImageFromAsset(std::string_vie std::shared_ptr AssetManager::createMesh(const std::string &name, std::span vertices, std::span indices, - std::shared_ptr material) + std::shared_ptr material, + bool build_bvh) { if (!_engine || !_engine->_resourceManager) return {}; if (name.empty()) return {}; @@ -631,9 +632,12 @@ std::shared_ptr AssetManager::createMesh(const std::string &name, surf.bounds = compute_bounds(vertices); mesh->surfaces.push_back(surf); - // Build CPU-side BVH for precise ray picking over this mesh. - // This uses the same mesh-local vertex/index data as the GPU upload. - mesh->bvh = build_mesh_bvh(*mesh, vertices, indices); + if (build_bvh) + { + // Build CPU-side BVH for precise ray picking over this mesh. + // This uses the same mesh-local vertex/index data as the GPU upload. + mesh->bvh = build_mesh_bvh(*mesh, vertices, indices); + } _meshCache.emplace(name, mesh); return mesh; @@ -709,3 +713,65 @@ bool AssetManager::removeMesh(const std::string &name) } return true; } + +bool AssetManager::removeMeshDeferred(const std::string &name, DeletionQueue &dq) +{ + auto it = _meshCache.find(name); + if (it == _meshCache.end()) return false; + + const std::shared_ptr mesh = it->second; + if (!mesh) return false; + + // Remove from cache immediately so callers won't retrieve a mesh we plan to destroy. + _meshCache.erase(it); + + if (_engine && _engine->_rayManager) + { + // Clean up BLAS cached for this mesh (if ray tracing is enabled). + // RayTracingManager defers actual AS destruction internally. + _engine->_rayManager->removeBLASForBuffer(mesh->meshBuffers.vertexBuffer.buffer); + } + + ResourceManager *rm = (_engine && _engine->_resourceManager) ? _engine->_resourceManager.get() : nullptr; + if (!rm) + { + return true; + } + + const AllocatedBuffer indexBuffer = mesh->meshBuffers.indexBuffer; + const AllocatedBuffer vertexBuffer = mesh->meshBuffers.vertexBuffer; + + std::optional materialBuffer; + auto itb = _meshMaterialBuffers.find(name); + if (itb != _meshMaterialBuffers.end()) + { + materialBuffer = itb->second; + _meshMaterialBuffers.erase(itb); + } + + std::vector ownedImages; + auto iti = _meshOwnedImages.find(name); + if (iti != _meshOwnedImages.end()) + { + ownedImages = std::move(iti->second); + _meshOwnedImages.erase(iti); + } + + dq.push_function([rm, indexBuffer, vertexBuffer, materialBuffer, ownedImages = std::move(ownedImages)]() mutable + { + if (indexBuffer.buffer) rm->destroy_buffer(indexBuffer); + if (vertexBuffer.buffer) rm->destroy_buffer(vertexBuffer); + + if (materialBuffer.has_value() && materialBuffer->buffer) + { + rm->destroy_buffer(*materialBuffer); + } + + for (const auto &img : ownedImages) + { + if (img.image) rm->destroy_image(img); + } + }); + + return true; +} diff --git a/src/core/assets/manager.h b/src/core/assets/manager.h index 42e0b7e..f62d321 100644 --- a/src/core/assets/manager.h +++ b/src/core/assets/manager.h @@ -107,11 +107,13 @@ public: std::shared_ptr createMesh(const std::string &name, std::span vertices, std::span indices, - std::shared_ptr material = {}); + std::shared_ptr material = {}, + bool build_bvh = true); std::shared_ptr getMesh(const std::string &name) const; bool removeMesh(const std::string &name); + bool removeMeshDeferred(const std::string &name, DeletionQueue &dq); // Convenience: create a PBR material from constants using engine default textures std::shared_ptr createMaterialFromConstants(const std::string &name, diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index fb6afb9..c873c63 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -1595,97 +1595,17 @@ namespace } } - // Scene debug bits - static void ui_scene(VulkanEngine *eng) + // Scene editor - spawn and delete instances + static void ui_scene_editor(VulkanEngine *eng) { - if (!eng) return; - const DrawContext &dc = eng->_context->getMainDrawContext(); - ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size()); - ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size()); + if (!eng || !eng->_sceneManager) + { + ImGui::TextUnformatted("SceneManager not available"); + return; + } + + SceneManager *sceneMgr = eng->_sceneManager.get(); PickingSystem *picking = eng->picking(); - if (picking) - { - bool use_id = picking->use_id_buffer_picking(); - if (ImGui::Checkbox("Use ID-buffer picking", &use_id)) - { - picking->set_use_id_buffer_picking(use_id); - } - ImGui::Text("Picking mode: %s", - use_id ? "ID buffer (async, 1-frame latency)" : "CPU raycast"); - - bool debug_bvh = picking->debug_draw_bvh(); - if (ImGui::Checkbox("Debug draw mesh BVH (last pick)", &debug_bvh)) - { - picking->set_debug_draw_bvh(debug_bvh); - } - } - else - { - ImGui::TextUnformatted("Picking system not available"); - } - - // Debug draw settings (engine-owned collector + render pass) - if (eng->_context && eng->_context->debug_draw) - { - DebugDrawSystem *dd = eng->_context->debug_draw; - auto &s = dd->settings(); - - bool enabled = s.enabled; - if (ImGui::Checkbox("Enable debug draw", &enabled)) - { - s.enabled = enabled; - } - if (s.enabled) - { - ImGui::SameLine(); - ImGui::Text("Commands: %zu", dd->command_count()); - - int seg = s.segments; - if (ImGui::SliderInt("Circle segments", &seg, 3, 128)) - { - s.segments = seg; - } - - bool depth_tested = s.show_depth_tested; - bool overlay = s.show_overlay; - if (ImGui::Checkbox("Depth-tested", &depth_tested)) - { - s.show_depth_tested = depth_tested; - } - ImGui::SameLine(); - if (ImGui::Checkbox("Overlay", &overlay)) - { - s.show_overlay = overlay; - } - - auto layer_checkbox = [&s](const char *label, DebugDrawLayer layer) { - const uint32_t bit = static_cast(layer); - bool on = (s.layer_mask & bit) != 0u; - if (ImGui::Checkbox(label, &on)) - { - if (on) s.layer_mask |= bit; - else s.layer_mask &= ~bit; - } - }; - - ImGui::TextUnformatted("Layers"); - layer_checkbox("Physics##dd_layer_physics", DebugDrawLayer::Physics); - ImGui::SameLine(); - layer_checkbox("Picking##dd_layer_picking", DebugDrawLayer::Picking); - ImGui::SameLine(); - layer_checkbox("Lights##dd_layer_lights", DebugDrawLayer::Lights); - layer_checkbox("Particles##dd_layer_particles", DebugDrawLayer::Particles); - ImGui::SameLine(); - layer_checkbox("Volumetrics##dd_layer_volumetrics", DebugDrawLayer::Volumetrics); - ImGui::SameLine(); - layer_checkbox("Misc##dd_layer_misc", DebugDrawLayer::Misc); - } - } - else - { - ImGui::TextUnformatted("Debug draw system not available"); - } - ImGui::Separator(); // Spawn glTF instances (runtime) ImGui::TextUnformatted("Spawn glTF instance"); @@ -1739,13 +1659,73 @@ namespace } } - // Point light editor - if (eng->_sceneManager) + ImGui::Separator(); + // Delete selected model/primitive (uses last pick if valid, otherwise hover) + static std::string deleteStatus; + if (ImGui::Button("Delete selected")) { - ImGui::Separator(); - ImGui::TextUnformatted("Point lights"); + deleteStatus.clear(); + const PickingSystem::PickInfo *pick = nullptr; + if (picking) + { + const auto &last = picking->last_pick(); + const auto &hover = picking->hover_pick(); + pick = last.valid ? &last : (hover.valid ? &hover : nullptr); + } + if (!pick || pick->ownerName.empty()) + { + deleteStatus = "No selection to delete."; + } + else if (pick->ownerType == RenderObject::OwnerType::MeshInstance) + { + bool ok = eng->_sceneManager->removeMeshInstance(pick->ownerName); + if (ok && picking) + { + picking->clear_owner_picks(RenderObject::OwnerType::MeshInstance, pick->ownerName); + } + deleteStatus = ok ? "Removed mesh instance: " + pick->ownerName + : "Mesh instance not found: " + pick->ownerName; + } + else if (pick->ownerType == RenderObject::OwnerType::GLTFInstance) + { + bool ok = eng->_sceneManager->removeGLTFInstance(pick->ownerName); + if (ok) + { + deleteStatus = "Removed glTF instance: " + pick->ownerName; + if (picking) + { + picking->clear_owner_picks(RenderObject::OwnerType::GLTFInstance, pick->ownerName); + } + } + else + { + deleteStatus = "glTF instance not found: " + pick->ownerName; + } + } + else + { + deleteStatus = "Cannot delete this object type (static scene)."; + } + } + if (!deleteStatus.empty()) + { + ImGui::TextUnformatted(deleteStatus.c_str()); + } + } - SceneManager *sceneMgr = eng->_sceneManager.get(); + // Lights editor (Point + Spot lights) + static void ui_lights(VulkanEngine *eng) + { + if (!eng || !eng->_sceneManager) + { + ImGui::TextUnformatted("SceneManager not available"); + return; + } + + SceneManager *sceneMgr = eng->_sceneManager.get(); + + // Point light editor + ImGui::TextUnformatted("Point lights"); const auto &lights = sceneMgr->getPointLights(); ImGui::Text("Active lights: %zu", lights.size()); @@ -1939,62 +1919,21 @@ namespace sceneMgr->clearSpotLights(); selectedSpot = -1; } + } + + // Picking & Gizmo - picking info and transform editor + static void ui_picking_gizmo(VulkanEngine *eng) + { + if (!eng || !eng->_sceneManager) + { + ImGui::TextUnformatted("SceneManager not available"); + return; } - ImGui::Separator(); - // Delete selected model/primitive (uses last pick if valid, otherwise hover) - static std::string deleteStatus; - if (ImGui::Button("Delete selected")) - { - deleteStatus.clear(); - const PickingSystem::PickInfo *pick = nullptr; - if (picking) - { - const auto &last = picking->last_pick(); - const auto &hover = picking->hover_pick(); - pick = last.valid ? &last : (hover.valid ? &hover : nullptr); - } - if (!pick || pick->ownerName.empty()) - { - deleteStatus = "No selection to delete."; - } - else if (pick->ownerType == RenderObject::OwnerType::MeshInstance) - { - bool ok = eng->_sceneManager->removeMeshInstance(pick->ownerName); - if (ok && picking) - { - picking->clear_owner_picks(RenderObject::OwnerType::MeshInstance, pick->ownerName); - } - deleteStatus = ok ? "Removed mesh instance: " + pick->ownerName - : "Mesh instance not found: " + pick->ownerName; - } - else if (pick->ownerType == RenderObject::OwnerType::GLTFInstance) - { - bool ok = eng->_sceneManager->removeGLTFInstance(pick->ownerName); - if (ok) - { - deleteStatus = "Removed glTF instance: " + pick->ownerName; - if (picking) - { - picking->clear_owner_picks(RenderObject::OwnerType::GLTFInstance, pick->ownerName); - } - } - else - { - deleteStatus = "glTF instance not found: " + pick->ownerName; - } - } - else - { - deleteStatus = "Cannot delete this object type (static scene)."; - } - } - if (!deleteStatus.empty()) - { - ImGui::TextUnformatted(deleteStatus.c_str()); - } - ImGui::Separator(); + SceneManager *sceneMgr = eng->_sceneManager.get(); + PickingSystem *picking = eng->picking(); + // Last pick info if (picking && picking->last_pick().valid) { const auto &last = picking->last_pick(); @@ -2075,14 +2014,6 @@ namespace ImGui::Separator(); ImGui::TextUnformatted("Object Gizmo (ImGuizmo)"); - if (!eng->_sceneManager) - { - ImGui::TextUnformatted("SceneManager not available"); - return; - } - - SceneManager *sceneMgr = eng->_sceneManager.get(); - // Choose a pick to edit: prefer last pick, then hover. PickingSystem::PickInfo *pick = nullptr; if (picking) @@ -2299,11 +2230,11 @@ namespace PlanetSystem::PlanetBody *earth = planets->get_body(PlanetSystem::BodyID::Earth); PlanetSystem::PlanetBody *moon = planets->get_body(PlanetSystem::BodyID::Moon); - if (earth) - { - ImGui::Separator(); + if (earth) + { + ImGui::Separator(); - bool vis = earth->visible; + bool vis = earth->visible; if (ImGui::Checkbox("Render Earth", &vis)) { earth->visible = vis; @@ -2322,20 +2253,103 @@ namespace look_at_world(scene->getMainCamera(), earth->center_world); } - if (ImGui::Button("Teleport: 1000 km orbit")) - { - scene->getMainCamera().position_world = - earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e6); - look_at_world(scene->getMainCamera(), earth->center_world); - } - ImGui::SameLine(); - if (ImGui::Button("Teleport: 10 km above surface")) - { - scene->getMainCamera().position_world = - earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e4); - look_at_world(scene->getMainCamera(), earth->center_world); - } - } + if (ImGui::Button("Teleport: 1000 km orbit")) + { + scene->getMainCamera().position_world = + earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e6); + look_at_world(scene->getMainCamera(), earth->center_world); + } + ImGui::SameLine(); + if (ImGui::Button("Teleport: 10 km above surface")) + { + scene->getMainCamera().position_world = + earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e4); + look_at_world(scene->getMainCamera(), earth->center_world); + } + + ImGui::Separator(); + if (ImGui::CollapsingHeader("Earth LOD / Perf", ImGuiTreeNodeFlags_DefaultOpen)) + { + auto settings = planets->earth_quadtree_settings(); + bool changed = false; + + int maxLevel = static_cast(settings.max_level); + if (ImGui::SliderInt("Max LOD level", &maxLevel, 0, 20)) + { + settings.max_level = static_cast(std::max(0, maxLevel)); + changed = true; + } + + if (ImGui::SliderFloat("Target SSE (px)", &settings.target_sse_px, 4.0f, 128.0f, "%.1f")) + { + settings.target_sse_px = std::max(settings.target_sse_px, 0.1f); + changed = true; + } + + int maxPatches = static_cast(settings.max_patches_visible); + if (ImGui::SliderInt("Max visible patches", &maxPatches, 64, 20000)) + { + settings.max_patches_visible = static_cast(std::max(6, maxPatches)); + changed = true; + } + + int createBudget = static_cast(planets->earth_patch_create_budget_per_frame()); + if (ImGui::SliderInt("Patch create budget/frame", &createBudget, 0, 512)) + { + planets->set_earth_patch_create_budget_per_frame(static_cast(std::max(0, createBudget))); + } + + float createBudgetMs = planets->earth_patch_create_budget_ms(); + if (ImGui::DragFloat("Patch create budget (ms)", &createBudgetMs, 0.25f, 0.0f, 50.0f, "%.2f")) + { + planets->set_earth_patch_create_budget_ms(std::max(0.0f, createBudgetMs)); + } + + int cacheMax = static_cast(planets->earth_patch_cache_max()); + if (ImGui::SliderInt("Patch cache max", &cacheMax, 0, 50000)) + { + planets->set_earth_patch_cache_max(static_cast(std::max(0, cacheMax))); + } + + if (ImGui::Checkbox("Frustum cull", &settings.frustum_cull)) changed = true; + if (ImGui::Checkbox("Horizon cull", &settings.horizon_cull)) changed = true; + + if (ImGui::Checkbox("RT guardrail (LOD floor)", &settings.rt_guardrail)) changed = true; + if (settings.rt_guardrail) + { + float maxEdge = static_cast(settings.max_patch_edge_rt_m); + if (ImGui::DragFloat("RT max patch edge (m)", &maxEdge, 100.0f, 0.0f, 200000.0f, "%.0f")) + { + settings.max_patch_edge_rt_m = static_cast(std::max(0.0f, maxEdge)); + changed = true; + } + + float maxAlt = static_cast(settings.rt_guardrail_max_altitude_m); + if (ImGui::DragFloat("RT max altitude (m)", &maxAlt, 1000.0f, 0.0f, 2.0e6f, "%.0f")) + { + settings.rt_guardrail_max_altitude_m = static_cast(std::max(0.0f, maxAlt)); + changed = true; + } + } + + if (changed) + { + planets->set_earth_quadtree_settings(settings); + } + + const PlanetSystem::EarthDebugStats &s = planets->earth_debug_stats(); + ImGui::Separator(); + ImGui::Text("Visible patches: %u (est. tris: %u)", s.visible_patches, s.estimated_triangles); + ImGui::Text("Cache size: %u (created this frame: %u)", s.patch_cache_size, s.created_patches); + ImGui::Text("Quadtree: max level used %u | visited %u | culled %u | budget-limited %u", + s.quadtree.max_level_used, + s.quadtree.nodes_visited, + s.quadtree.nodes_culled, + s.quadtree.splits_budget_limited); + ImGui::Text("CPU ms: quadtree %.2f | create %.2f | emit %.2f | total %.2f", + s.ms_quadtree, s.ms_patch_create, s.ms_emit, s.ms_total); + } + } if (moon) { @@ -2353,24 +2367,7 @@ namespace // Window visibility states for menu-bar toggles namespace { - struct DebugWindowStates - { - bool show_overview{false}; - bool show_window{false}; - bool show_background{false}; - bool show_particles{false}; - bool show_shadows{false}; - bool show_render_graph{false}; - bool show_pipelines{false}; - bool show_ibl{false}; - bool show_postfx{false}; - bool show_scene{false}; - bool show_camera{false}; - bool show_planets{false}; - bool show_async_assets{false}; - bool show_textures{false}; - }; - static DebugWindowStates g_debug_windows; + static bool g_show_debug_window = false; } // namespace void vk_engine_draw_debug_ui(VulkanEngine *eng) @@ -2384,23 +2381,7 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng) { if (ImGui::BeginMenu("View")) { - ImGui::MenuItem("Overview", nullptr, &g_debug_windows.show_overview); - ImGui::MenuItem("Window", nullptr, &g_debug_windows.show_window); - ImGui::Separator(); - ImGui::MenuItem("Scene", nullptr, &g_debug_windows.show_scene); - ImGui::MenuItem("Camera", nullptr, &g_debug_windows.show_camera); - ImGui::MenuItem("Planets", nullptr, &g_debug_windows.show_planets); - ImGui::MenuItem("Render Graph", nullptr, &g_debug_windows.show_render_graph); - ImGui::MenuItem("Pipelines", nullptr, &g_debug_windows.show_pipelines); - ImGui::Separator(); - ImGui::MenuItem("Shadows", nullptr, &g_debug_windows.show_shadows); - ImGui::MenuItem("IBL", nullptr, &g_debug_windows.show_ibl); - ImGui::MenuItem("PostFX", nullptr, &g_debug_windows.show_postfx); - ImGui::MenuItem("Background", nullptr, &g_debug_windows.show_background); - ImGui::Separator(); - ImGui::MenuItem("Particles", nullptr, &g_debug_windows.show_particles); - ImGui::MenuItem("Textures", nullptr, &g_debug_windows.show_textures); - ImGui::MenuItem("Async Assets", nullptr, &g_debug_windows.show_async_assets); + ImGui::MenuItem("Engine Debug", nullptr, &g_show_debug_window); ImGui::EndMenu(); } @@ -2414,129 +2395,112 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng) ImGui::EndMainMenuBar(); } - // Individual debug windows (only shown when toggled) - if (g_debug_windows.show_overview) + // Single consolidated debug window with tabs + if (g_show_debug_window) { - if (ImGui::Begin("Overview", &g_debug_windows.show_overview)) + ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Engine Debug", &g_show_debug_window)) { - ui_overview(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabBar("DebugTabs", ImGuiTabBarFlags_None)) + { + if (ImGui::BeginTabItem("Overview")) + { + ui_overview(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_window) - { - if (ImGui::Begin("Window Settings", &g_debug_windows.show_window)) - { - ui_window(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Scene Editor")) + { + ui_scene_editor(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_background) - { - if (ImGui::Begin("Background", &g_debug_windows.show_background)) - { - ui_background(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Lights")) + { + ui_lights(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_particles) - { - if (ImGui::Begin("Particles", &g_debug_windows.show_particles)) - { - ui_particles(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Picking & Gizmo")) + { + ui_picking_gizmo(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_shadows) - { - if (ImGui::Begin("Shadows", &g_debug_windows.show_shadows)) - { - ui_shadows(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Camera")) + { + ui_camera(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_render_graph) - { - if (ImGui::Begin("Render Graph", &g_debug_windows.show_render_graph)) - { - ui_render_graph(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Planets")) + { + ui_planets(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_pipelines) - { - if (ImGui::Begin("Pipelines", &g_debug_windows.show_pipelines)) - { - ui_pipelines(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Render Graph")) + { + ui_render_graph(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_ibl) - { - if (ImGui::Begin("IBL", &g_debug_windows.show_ibl)) - { - ui_ibl(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Pipelines")) + { + ui_pipelines(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_postfx) - { - if (ImGui::Begin("PostFX", &g_debug_windows.show_postfx)) - { - ui_postfx(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Shadows")) + { + ui_shadows(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_scene) - { - if (ImGui::Begin("Scene", &g_debug_windows.show_scene)) - { - ui_scene(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("IBL")) + { + ui_ibl(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_camera) - { - if (ImGui::Begin("Camera", &g_debug_windows.show_camera)) - { - ui_camera(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("PostFX")) + { + ui_postfx(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_planets) - { - if (ImGui::Begin("Planets", &g_debug_windows.show_planets)) - { - ui_planets(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Background")) + { + ui_background(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_async_assets) - { - if (ImGui::Begin("Async Assets", &g_debug_windows.show_async_assets)) - { - ui_async_assets(eng); - } - ImGui::End(); - } + if (ImGui::BeginTabItem("Particles")) + { + ui_particles(eng); + ImGui::EndTabItem(); + } - if (g_debug_windows.show_textures) - { - if (ImGui::Begin("Textures", &g_debug_windows.show_textures)) - { - ui_textures(eng); + if (ImGui::BeginTabItem("Window")) + { + ui_window(eng); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Textures")) + { + ui_textures(eng); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Async Assets")) + { + ui_async_assets(eng); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } } ImGui::End(); } diff --git a/src/scene/planet/cubesphere.cpp b/src/scene/planet/cubesphere.cpp new file mode 100644 index 0000000..b678263 --- /dev/null +++ b/src/scene/planet/cubesphere.cpp @@ -0,0 +1,266 @@ +#include "cubesphere.h" + +#include + +#include + +#include + +namespace planet +{ + glm::dvec3 cubesphere_unit_direction(CubeFace face, double u, double v) + { + // Convention: u increases right, v increases down (image space). + glm::dvec3 d(0.0); + switch (face) + { + case CubeFace::PosX: d = glm::dvec3(1.0, -v, -u); break; + case CubeFace::NegX: d = glm::dvec3(-1.0, -v, u); break; + case CubeFace::PosY: d = glm::dvec3(u, 1.0, v); break; + case CubeFace::NegY: d = glm::dvec3(u, -1.0, -v); break; + case CubeFace::PosZ: d = glm::dvec3(u, -v, 1.0); break; + case CubeFace::NegZ: d = glm::dvec3(-u, -v, -1.0); break; + } + + const double len2 = glm::dot(d, d); + if (len2 <= 0.0) + { + return glm::dvec3(0.0, 0.0, 1.0); + } + return d * (1.0 / std::sqrt(len2)); + } + + void cubesphere_tile_uv_bounds(uint32_t level, uint32_t x, uint32_t y, + double &out_u0, double &out_u1, + double &out_v0, double &out_v1) + { + const uint32_t tiles_u = (level < 31u) ? (1u << level) : 0u; + const double inv_tiles = (tiles_u > 0u) ? (1.0 / static_cast(tiles_u)) : 1.0; + + const double u0_01 = static_cast(x) * inv_tiles; + const double u1_01 = static_cast(x + 1u) * inv_tiles; + const double v0_01 = static_cast(y) * inv_tiles; + const double v1_01 = static_cast(y + 1u) * inv_tiles; + + out_u0 = u0_01 * 2.0 - 1.0; + out_u1 = u1_01 * 2.0 - 1.0; + out_v0 = v0_01 * 2.0 - 1.0; + out_v1 = v1_01 * 2.0 - 1.0; + } + + glm::dvec3 cubesphere_patch_center_direction(CubeFace face, uint32_t level, uint32_t x, uint32_t y) + { + double u0 = 0.0, u1 = 0.0, v0 = 0.0, v1 = 0.0; + cubesphere_tile_uv_bounds(level, x, y, u0, u1, v0, v1); + const double u_mid = 0.5 * (u0 + u1); + const double v_mid = 0.5 * (v0 + v1); + return cubesphere_unit_direction(face, u_mid, v_mid); + } + + WorldVec3 cubesphere_patch_center_world(const WorldVec3 ¢er_world, + double radius_m, + CubeFace face, + uint32_t level, + uint32_t x, + uint32_t y) + { + const glm::dvec3 dir = cubesphere_patch_center_direction(face, level, x, y); + return center_world + dir * radius_m; + } + + double cubesphere_patch_edge_m(double radius_m, uint32_t level) + { + // Each cube face spans 90 degrees. Use arc length per tile edge as a simple estimate. + const double face_arc_m = (glm::pi() * 0.5) * radius_m; + const uint32_t safe_level = (level < 30u) ? level : 30u; + const double tiles_per_axis = static_cast(1u << safe_level); + return face_arc_m / tiles_per_axis; + } + + double cubesphere_skirt_depth_m(double radius_m, uint32_t level) + { + const double edge_m = cubesphere_patch_edge_m(radius_m, level); + return glm::max(10.0, 0.02 * edge_m); + } + + void build_cubesphere_patch_mesh(CubeSpherePatchMesh &out, + const WorldVec3 ¢er_world, + double radius_m, + CubeFace face, + uint32_t level, + uint32_t x, + uint32_t y, + uint32_t resolution, + const glm::vec4 &vertex_color, + bool generate_tangents) + { + out.vertices.clear(); + out.indices.clear(); + out.patch_center_world = center_world; + + if (resolution < 2) + { + return; + } + + const double skirt_depth_m = cubesphere_skirt_depth_m(radius_m, level); + const double skirt_radius_m = glm::max(0.0, radius_m - skirt_depth_m); + + double u0 = 0.0, u1 = 0.0, v0 = 0.0, v1 = 0.0; + cubesphere_tile_uv_bounds(level, x, y, u0, u1, v0, v1); + + const glm::dvec3 patch_center_dir = cubesphere_patch_center_direction(face, level, x, y); + out.patch_center_world = center_world + patch_center_dir * radius_m; + + const uint32_t base_vertex_count = resolution * resolution; + const uint32_t skirt_vertex_count = 4u * resolution; + out.vertices.resize(static_cast(base_vertex_count) + static_cast(skirt_vertex_count)); + + const double inv = 1.0 / static_cast(resolution - 1u); + const double du = (u1 - u0) * inv; + const double dv = (v1 - v0) * inv; + for (uint32_t j = 0; j < resolution; ++j) + { + const float t = static_cast(static_cast(j) * inv); + const double v = v0 + dv * static_cast(j); + + for (uint32_t i = 0; i < resolution; ++i) + { + const float s = static_cast(static_cast(i) * inv); + const double u = u0 + du * static_cast(i); + + const glm::dvec3 unit_dir = cubesphere_unit_direction(face, u, v); + const glm::dvec3 delta_d = (unit_dir - patch_center_dir) * radius_m; + + Vertex vert{}; + vert.position = glm::vec3(static_cast(delta_d.x), + static_cast(delta_d.y), + static_cast(delta_d.z)); + vert.normal = glm::vec3(static_cast(unit_dir.x), + static_cast(unit_dir.y), + static_cast(unit_dir.z)); + vert.uv_x = s; + vert.uv_y = t; + vert.color = vertex_color; + vert.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + + const uint32_t idx = j * resolution + i; + out.vertices[idx] = vert; + } + } + + auto add_skirt_vertex = [&](uint32_t base_index, uint32_t skirt_index) + { + const glm::vec3 n = out.vertices[base_index].normal; + const glm::dvec3 unit_dir(static_cast(n.x), + static_cast(n.y), + static_cast(n.z)); + const glm::dvec3 delta_d = unit_dir * skirt_radius_m - patch_center_dir * radius_m; + + Vertex vert = out.vertices[base_index]; + vert.position = glm::vec3(static_cast(delta_d.x), + static_cast(delta_d.y), + static_cast(delta_d.z)); + vert.normal = glm::vec3(static_cast(unit_dir.x), + static_cast(unit_dir.y), + static_cast(unit_dir.z)); + out.vertices[skirt_index] = vert; + }; + + const uint32_t top_skirt_start = base_vertex_count + 0u * resolution; + const uint32_t right_skirt_start = base_vertex_count + 1u * resolution; + const uint32_t bottom_skirt_start = base_vertex_count + 2u * resolution; + const uint32_t left_skirt_start = base_vertex_count + 3u * resolution; + + // Top edge (j=0) + for (uint32_t i = 0; i < resolution; ++i) + { + add_skirt_vertex(0u * resolution + i, top_skirt_start + i); + } + // Right edge (i=resolution-1) + for (uint32_t j = 0; j < resolution; ++j) + { + add_skirt_vertex(j * resolution + (resolution - 1u), right_skirt_start + j); + } + // Bottom edge (j=resolution-1) + for (uint32_t i = 0; i < resolution; ++i) + { + add_skirt_vertex((resolution - 1u) * resolution + i, bottom_skirt_start + i); + } + // Left edge (i=0) + for (uint32_t j = 0; j < resolution; ++j) + { + add_skirt_vertex(j * resolution + 0u, left_skirt_start + j); + } + + const size_t grid_index_count = + static_cast(resolution - 1u) * static_cast(resolution - 1u) * 6u; + const size_t skirt_index_count = static_cast(4u) * static_cast(resolution - 1u) * 6u; + out.indices.reserve(grid_index_count + skirt_index_count); + + // Base grid indices + for (uint32_t j = 0; j + 1 < resolution; ++j) + { + for (uint32_t i = 0; i + 1 < resolution; ++i) + { + const uint32_t i0 = j * resolution + i; + const uint32_t i1 = i0 + 1; + const uint32_t i2 = i0 + resolution; + const uint32_t i3 = i2 + 1; + + // CCW winding when viewed from outside the sphere. + out.indices.push_back(i0); + out.indices.push_back(i1); + out.indices.push_back(i2); + + out.indices.push_back(i2); + out.indices.push_back(i1); + out.indices.push_back(i3); + } + } + + auto add_skirt_quads = [&](uint32_t base0, uint32_t base1, uint32_t skirt0, uint32_t skirt1) + { + out.indices.push_back(base0); + out.indices.push_back(base1); + out.indices.push_back(skirt0); + + out.indices.push_back(skirt0); + out.indices.push_back(base1); + out.indices.push_back(skirt1); + }; + + // Skirt indices: 4 edges, (N-1) segments each. + for (uint32_t i = 0; i + 1 < resolution; ++i) + { + // Top edge + add_skirt_quads(0u * resolution + i, + 0u * resolution + (i + 1u), + top_skirt_start + i, + top_skirt_start + (i + 1u)); + // Bottom edge + add_skirt_quads((resolution - 1u) * resolution + i, + (resolution - 1u) * resolution + (i + 1u), + bottom_skirt_start + i, + bottom_skirt_start + (i + 1u)); + } + for (uint32_t j = 0; j + 1 < resolution; ++j) + { + // Left edge + add_skirt_quads(j * resolution + 0u, + (j + 1u) * resolution + 0u, + left_skirt_start + j, + left_skirt_start + (j + 1u)); + // Right edge + add_skirt_quads(j * resolution + (resolution - 1u), + (j + 1u) * resolution + (resolution - 1u), + right_skirt_start + j, + right_skirt_start + (j + 1u)); + } + + if (generate_tangents) + { + geom::generate_tangents(out.vertices, out.indices); + } + } +} // namespace planet diff --git a/src/scene/planet/cubesphere.h b/src/scene/planet/cubesphere.h new file mode 100644 index 0000000..2c5b890 --- /dev/null +++ b/src/scene/planet/cubesphere.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +#include +#include + +#include + +namespace planet +{ + // Cube face ordering matches KTX/Vulkan cubemap face order: + // +X, -X, +Y, -Y, +Z, -Z + enum class CubeFace : uint8_t + { + PosX = 0, + NegX = 1, + PosY = 2, + NegY = 3, + PosZ = 4, + NegZ = 5, + }; + + // u,v are in [-1,+1] on the chosen face. Convention: + // - u increases to the right + // - v increases downward (image space) + glm::dvec3 cubesphere_unit_direction(CubeFace face, double u, double v); + + // Tile bounds on a face in cube-face parametric space: + // u,v in [-1,+1], where [0..1] maps to [-1..+1]. + void cubesphere_tile_uv_bounds(uint32_t level, uint32_t x, uint32_t y, + double &out_u0, double &out_u1, + double &out_v0, double &out_v1); + + glm::dvec3 cubesphere_patch_center_direction(CubeFace face, uint32_t level, uint32_t x, uint32_t y); + + WorldVec3 cubesphere_patch_center_world(const WorldVec3 ¢er_world, + double radius_m, + CubeFace face, + uint32_t level, + uint32_t x, + uint32_t y); + + // Approximate world-space tile edge length on the sphere surface. + double cubesphere_patch_edge_m(double radius_m, uint32_t level); + + // Skirt depth heuristic (meters). + double cubesphere_skirt_depth_m(double radius_m, uint32_t level); + + struct CubeSpherePatchMesh + { + std::vector vertices; + std::vector indices; + WorldVec3 patch_center_world{0.0, 0.0, 0.0}; + }; + + // Build a cube-sphere patch mesh with skirts. Vertex positions are relative to patch_center_world. + void build_cubesphere_patch_mesh(CubeSpherePatchMesh &out, + const WorldVec3 ¢er_world, + double radius_m, + CubeFace face, + uint32_t level, + uint32_t x, + uint32_t y, + uint32_t resolution, + const glm::vec4 &vertex_color, + bool generate_tangents = true); +} // namespace planet diff --git a/src/scene/planet/planet_quadtree.cpp b/src/scene/planet/planet_quadtree.cpp new file mode 100644 index 0000000..10c6c49 --- /dev/null +++ b/src/scene/planet/planet_quadtree.cpp @@ -0,0 +1,242 @@ +#include "planet_quadtree.h" + +#include +#include +#include +#include + +#include + +namespace planet +{ + namespace + { + struct Node + { + PatchKey key{}; + }; + + bool is_patch_visible_horizon(const WorldVec3 &body_center_world, + double radius_m, + const WorldVec3 &camera_world, + const glm::dvec3 &patch_center_dir, + double patch_edge_m) + { + const glm::dvec3 w = camera_world - body_center_world; + const double d = glm::length(w); + if (d <= radius_m || d <= 0.0) + { + return true; + } + + const glm::dvec3 w_dir = w / d; + const double cos_theta = glm::dot(patch_center_dir, w_dir); + + // Horizon angle: cos(theta_h) = R / d + const double cos_h = glm::clamp(radius_m / d, 0.0, 1.0); + const double sin_h = std::sqrt(glm::max(0.0, 1.0 - cos_h * cos_h)); + + // Expand horizon by patch angular radius to avoid culling near silhouettes. + const double half_diag_m = patch_edge_m * 0.7071067811865476; // sqrt(2)/2 + const double ang = glm::clamp(half_diag_m / radius_m, 0.0, glm::pi()); + const double cos_a = std::cos(ang); + const double sin_a = std::sin(ang); + + // Visible if theta <= theta_h + ang: + // cos(theta) >= cos(theta_h + ang) + const double cos_limit = cos_h * cos_a - sin_h * sin_a; + return cos_theta >= cos_limit; + } + + bool is_patch_visible_frustum(const glm::vec3 ¢er_local, float bound_radius_m, const glm::mat4 &viewproj) + { + if (!(bound_radius_m > 0.0f)) + { + bound_radius_m = 1.0f; + } + + // Conservative AABB-in-clip test for a cube around the patch center. + const std::array corners{ + glm::vec3{+1, +1, +1}, glm::vec3{+1, +1, -1}, glm::vec3{+1, -1, +1}, glm::vec3{+1, -1, -1}, + glm::vec3{-1, +1, +1}, glm::vec3{-1, +1, -1}, glm::vec3{-1, -1, +1}, glm::vec3{-1, -1, -1}, + }; + + glm::vec4 clip[8]; + for (int i = 0; i < 8; ++i) + { + const glm::vec3 p = center_local + corners[i] * bound_radius_m; + clip[i] = viewproj * glm::vec4(p, 1.0f); + } + + auto all_out = [&](auto pred) { + for (int i = 0; i < 8; ++i) + { + if (!pred(clip[i])) return false; + } + return true; + }; + + // Clip volume in Vulkan (ZO): -w<=x<=w, -w<=y<=w, 0<=z<=w + if (all_out([](const glm::vec4 &v) { return v.x < -v.w; })) return false; // left + if (all_out([](const glm::vec4 &v) { return v.x > v.w; })) return false; // right + if (all_out([](const glm::vec4 &v) { return v.y < -v.w; })) return false; // bottom + if (all_out([](const glm::vec4 &v) { return v.y > v.w; })) return false; // top + if (all_out([](const glm::vec4 &v) { return v.z < 0.0f; })) return false; // near (ZO) + if (all_out([](const glm::vec4 &v) { return v.z > v.w; })) return false; // far + + return true; + } + } // namespace + + void PlanetQuadtree::update(const WorldVec3 &body_center_world, + double radius_m, + const WorldVec3 &camera_world, + const WorldVec3 &origin_world, + const GPUSceneData &scene_data, + VkExtent2D logical_extent) + { + _visible_leaves.clear(); + _stats = {}; + + if (radius_m <= 0.0) + { + return; + } + + if (logical_extent.width == 0 || logical_extent.height == 0) + { + logical_extent = VkExtent2D{1920, 1080}; + } + + const bool rt_shadows_enabled = (scene_data.rtOptions.x != 0u) && (scene_data.rtOptions.z != 0u); + const double cam_alt_m = glm::max(0.0, glm::length(camera_world - body_center_world) - radius_m); + const bool rt_guardrail_active = + _settings.rt_guardrail && + rt_shadows_enabled && + (_settings.max_patch_edge_rt_m > 0.0) && + (cam_alt_m <= _settings.rt_guardrail_max_altitude_m); + + const float proj_y = scene_data.proj[1][1]; + const float proj_scale = std::abs(proj_y) * (static_cast(logical_extent.height) * 0.5f); + if (!(proj_scale > 0.0f)) + { + return; + } + + thread_local std::vector stack; + stack.clear(); + stack.reserve(256); + + const size_t max_visible_leaves = + (_settings.max_patches_visible > 0u) + ? static_cast(std::max(_settings.max_patches_visible, 6u)) + : std::numeric_limits::max(); + + auto push_root = [&](CubeFace face) + { + Node n{}; + n.key.face = face; + n.key.level = 0; + n.key.x = 0; + n.key.y = 0; + stack.push_back(n); + }; + + // Push in reverse order so pop_back visits in +X,-X,+Y,-Y,+Z,-Z order. + push_root(CubeFace::NegZ); + push_root(CubeFace::PosZ); + push_root(CubeFace::NegY); + push_root(CubeFace::PosY); + push_root(CubeFace::NegX); + push_root(CubeFace::PosX); + + while (!stack.empty()) + { + Node n = stack.back(); + stack.pop_back(); + _stats.nodes_visited++; + + const PatchKey &k = n.key; + + const double patch_edge_m = cubesphere_patch_edge_m(radius_m, k.level); + const glm::dvec3 patch_dir = cubesphere_patch_center_direction(k.face, k.level, k.x, k.y); + + if (_settings.horizon_cull) + { + if (!is_patch_visible_horizon(body_center_world, radius_m, camera_world, patch_dir, patch_edge_m)) + { + _stats.nodes_culled++; + continue; + } + } + + const WorldVec3 patch_center_world = + body_center_world + patch_dir * radius_m; + + if (_settings.frustum_cull) + { + const glm::vec3 patch_center_local = world_to_local(patch_center_world, origin_world); + const float bound_r = static_cast(patch_edge_m * 0.7071067811865476); + if (!is_patch_visible_frustum(patch_center_local, bound_r, scene_data.viewproj)) + { + _stats.nodes_culled++; + continue; + } + } + + const double dist_m = glm::max(1.0, glm::length(camera_world - patch_center_world)); + + // Screen-space error metric. + const double error_m = 0.5 * patch_edge_m; + const float sse_px = static_cast((error_m / dist_m) * static_cast(proj_scale)); + + bool refine = (k.level < _settings.max_level) && (sse_px > _settings.target_sse_px); + if (!refine && rt_guardrail_active && (k.level < _settings.max_level) && (patch_edge_m > _settings.max_patch_edge_rt_m)) + { + refine = true; + } + + if (refine) + { + // Budget check: splitting replaces this node with 4 children (adds +3 leaves minimum). + // Keep a stable upper bound on the final leaf count: leaves_so_far + stack.size() + 4. + const size_t min_leaves_if_split = _visible_leaves.size() + stack.size() + 4u; + if (min_leaves_if_split > max_visible_leaves) + { + refine = false; + _stats.splits_budget_limited++; + } + } + + if (refine) + { + // Child order: (0,0), (1,0), (0,1), (1,1) with y increasing downward. + const uint32_t cl = k.level + 1u; + const uint32_t cx = k.x * 2u; + const uint32_t cy = k.y * 2u; + + stack.push_back(Node{PatchKey{k.face, cl, cx + 1u, cy + 1u}}); + stack.push_back(Node{PatchKey{k.face, cl, cx + 0u, cy + 1u}}); + stack.push_back(Node{PatchKey{k.face, cl, cx + 1u, cy + 0u}}); + stack.push_back(Node{PatchKey{k.face, cl, cx + 0u, cy + 0u}}); + continue; + } + + _visible_leaves.push_back(k); + _stats.max_level_used = std::max(_stats.max_level_used, k.level); + } + + _stats.visible_leaves = static_cast(_visible_leaves.size()); + + // Keep deterministic order for stability (optional). + // DFS already stable; sort is useful when culling changes traversal. + std::sort(_visible_leaves.begin(), _visible_leaves.end(), + [](const PatchKey &a, const PatchKey &b) + { + if (a.face != b.face) return a.face < b.face; + if (a.level != b.level) return a.level < b.level; + if (a.x != b.x) return a.x < b.x; + return a.y < b.y; + }); + } +} // namespace planet diff --git a/src/scene/planet/planet_quadtree.h b/src/scene/planet/planet_quadtree.h new file mode 100644 index 0000000..6b67a1e --- /dev/null +++ b/src/scene/planet/planet_quadtree.h @@ -0,0 +1,82 @@ +#pragma once + +#include "cubesphere.h" + +#include +#include + +#include +#include +#include + +namespace planet +{ + struct PatchKey + { + CubeFace face = CubeFace::PosX; + uint32_t level = 0; + uint32_t x = 0; + uint32_t y = 0; + + friend bool operator==(const PatchKey &, const PatchKey &) = default; + }; + + struct PatchKeyHash + { + size_t operator()(const PatchKey &k) const noexcept + { + const uint64_t f = static_cast(k.face) & 0xFFull; + const uint64_t l = static_cast(k.level) & 0x3Full; + const uint64_t x = static_cast(k.x) & 0x1FFFFFull; + const uint64_t y = static_cast(k.y) & 0x1FFFFFull; + + // Simple stable packing: [face:8 | level:6 | x:21 | y:21] + const uint64_t packed = (f << 56) | (l << 50) | (x << 29) | (y << 8); + return std::hash{}(packed); + } + }; + + class PlanetQuadtree + { + public: + struct Settings + { + uint32_t max_level = 14; + float target_sse_px = 32.0f; // screen space error pixel + uint32_t max_patches_visible = 8192; + bool frustum_cull = true; + bool horizon_cull = true; + + // RT stability guardrail (only applied near-surface). + bool rt_guardrail = true; + double max_patch_edge_rt_m = 5000.0; + double rt_guardrail_max_altitude_m = 200000.0; + }; + + struct Stats + { + uint32_t visible_leaves = 0; + uint32_t max_level_used = 0; + uint32_t nodes_visited = 0; + uint32_t nodes_culled = 0; + uint32_t splits_budget_limited = 0; + }; + + void set_settings(const Settings &settings) { _settings = settings; } + const Settings &settings() const { return _settings; } + const Stats &stats() const { return _stats; } + const std::vector &visible_leaves() const { return _visible_leaves; } + + void update(const WorldVec3 &body_center_world, + double radius_m, + const WorldVec3 &camera_world, + const WorldVec3 &origin_world, + const GPUSceneData &scene_data, + VkExtent2D logical_extent); + + private: + Settings _settings{}; + Stats _stats{}; + std::vector _visible_leaves; + }; +} // namespace planet diff --git a/src/scene/planet/planet_system.cpp b/src/scene/planet/planet_system.cpp index 7a648cc..50aee44 100644 --- a/src/scene/planet/planet_system.cpp +++ b/src/scene/planet/planet_system.cpp @@ -1,19 +1,25 @@ #include "planet_system.h" #include +#include #include #include #include #include +#include #include #include #include +#include +#include +#include + namespace { - constexpr double kEarthRadiusM = 6378137.0; // WGS84 equatorial radius - constexpr double kMoonRadiusM = 1737400.0; // mean radius + constexpr double kEarthRadiusM = 6378137.0; // WGS84 equatorial radius + constexpr double kMoonRadiusM = 1737400.0; // mean radius constexpr double kMoonDistanceM = 384400000.0; // mean Earth-Moon distance GLTFMetallic_Roughness::MaterialConstants make_planet_constants() @@ -24,7 +30,16 @@ namespace c.metal_rough_factors = glm::vec4(0.0f, 1.0f, 0.0f, 0.0f); return c; } -} + + glm::vec4 debug_color_for_level(uint32_t level) + { + const float t = static_cast(level) * 0.37f; + const float r = 0.35f + 0.65f * std::sin(t + 0.0f); + const float g = 0.35f + 0.65f * std::sin(t + 2.1f); + const float b = 0.35f + 0.65f * std::sin(t + 4.2f); + return glm::vec4(r, g, b, 1.0f); + } +} // namespace void PlanetSystem::init(EngineContext *context) { @@ -72,25 +87,11 @@ void PlanetSystem::ensure_bodies_created() { AssetManager *assets = _context->assets; - // Earth: textured sphere (albedo only for now). + // Earth: cube-sphere quadtree patches (Milestones B2-B4). Material is shared. { - AssetManager::MeshCreateInfo ci{}; - ci.name = "Planet_EarthSphere"; - ci.geometry.type = AssetManager::MeshGeometryDesc::Type::Sphere; - ci.geometry.sectors = 64; - ci.geometry.stacks = 32; - - ci.material.kind = AssetManager::MeshMaterialDesc::Kind::Textured; - ci.material.options.albedoPath = "earth/earth_8k.jpg"; - ci.material.options.albedoSRGB = true; - ci.material.options.constants = make_planet_constants(); - ci.material.options.pass = MaterialPass::MainColor; - - earth.mesh = assets->createMesh(ci); - if (earth.mesh && !earth.mesh->surfaces.empty()) - { - earth.material = earth.mesh->surfaces[0].material; - } + GLTFMetallic_Roughness::MaterialConstants mc = make_planet_constants(); + mc.colorFactors = glm::vec4(1.0f); + earth.material = assets->createMaterialFromConstants("Planet_EarthMaterial", mc, MaterialPass::MainColor); } // Moon: constant albedo (no texture yet). @@ -113,6 +114,104 @@ void PlanetSystem::ensure_bodies_created() _bodies.push_back(std::move(moon)); } +std::shared_ptr PlanetSystem::get_or_create_earth_patch_mesh(const PlanetBody &earth, + const planet::PatchKey &key) +{ + auto it = _earth_patch_cache.find(key); + if (it != _earth_patch_cache.end()) + { + it->second.last_used_frame = _context ? _context->frameIndex : 0; + _earth_patch_lru.splice(_earth_patch_lru.begin(), _earth_patch_lru, it->second.lru_it); + return it->second.mesh; + } + + if (!_context || !_context->assets || !earth.material) + { + return {}; + } + + planet::CubeSpherePatchMesh mesh{}; + planet::build_cubesphere_patch_mesh(mesh, + earth.center_world, + earth.radius_m, + key.face, + key.level, + key.x, + key.y, + _earth_patch_resolution, + debug_color_for_level(key.level), + /*generate_tangents=*/false); + + const uint32_t face_i = static_cast(key.face); + const std::string name = + "Planet_EarthPatch_f" + std::to_string(face_i) + + "_L" + std::to_string(key.level) + + "_X" + std::to_string(key.x) + + "_Y" + std::to_string(key.y); + + std::shared_ptr out = + _context->assets->createMesh(name, mesh.vertices, mesh.indices, earth.material, /*build_bvh=*/false); + + EarthPatchCacheEntry entry{}; + entry.mesh = out; + entry.patch_center_dir = planet::cubesphere_patch_center_direction(key.face, key.level, key.x, key.y); + entry.last_used_frame = _context ? _context->frameIndex : 0; + _earth_patch_lru.push_front(key); + entry.lru_it = _earth_patch_lru.begin(); + _earth_patch_cache.emplace(key, std::move(entry)); + return out; +} + +void PlanetSystem::trim_earth_patch_cache() +{ + if (_earth_patch_cache_max == 0) + { + return; + } + + if (_earth_patch_cache.size() <= static_cast(_earth_patch_cache_max)) + { + return; + } + + if (!_context || !_context->assets) + { + return; + } + + AssetManager *assets = _context->assets; + FrameResources *frame = _context->currentFrame; + + while (_earth_patch_cache.size() > static_cast(_earth_patch_cache_max) && !_earth_patch_lru.empty()) + { + const planet::PatchKey key = _earth_patch_lru.back(); + _earth_patch_lru.pop_back(); + + auto it = _earth_patch_cache.find(key); + if (it == _earth_patch_cache.end()) + { + continue; + } + + std::shared_ptr mesh = std::move(it->second.mesh); + _earth_patch_cache.erase(it); + + if (!mesh) + { + continue; + } + + if (frame) + { + assets->removeMeshDeferred(mesh->name, frame->_deletionQueue); + } + else + { + assets->removeMesh(mesh->name); + } + } +} + void PlanetSystem::update_and_emit(const SceneManager &scene, DrawContext &draw_context) { if (!_enabled) @@ -124,8 +223,136 @@ void PlanetSystem::update_and_emit(const SceneManager &scene, DrawContext &draw_ const WorldVec3 origin_world = scene.get_world_origin(); - for (PlanetBody &b : _bodies) + // Earth: quadtree patches. { + using Clock = std::chrono::steady_clock; + + PlanetBody *earth = get_body(BodyID::Earth); + if (earth && earth->visible && earth->material && _context) + { + const Clock::time_point t0 = Clock::now(); + + _earth_quadtree.set_settings(_earth_quadtree_settings); + + const VkExtent2D logical_extent = _context->getLogicalRenderExtent(); + const WorldVec3 cam_world = scene.getMainCamera().position_world; + + const Clock::time_point t_q0 = Clock::now(); + _earth_quadtree.update(earth->center_world, + earth->radius_m, + cam_world, + origin_world, + scene.getSceneData(), + logical_extent); + const Clock::time_point t_q1 = Clock::now(); + + uint32_t created_patches = 0; + double ms_patch_create = 0.0; + const uint32_t max_create = _earth_patch_create_budget_per_frame; + const double max_create_ms = + (_earth_patch_create_budget_ms > 0.0f) ? static_cast(_earth_patch_create_budget_ms) : 0.0; + const uint32_t frame_index = _context->frameIndex; + + const Clock::time_point t_emit0 = Clock::now(); + for (const planet::PatchKey &k : _earth_quadtree.visible_leaves()) + { + EarthPatchCacheEntry *entry = nullptr; + { + auto it = _earth_patch_cache.find(k); + if (it != _earth_patch_cache.end()) + { + it->second.last_used_frame = frame_index; + _earth_patch_lru.splice(_earth_patch_lru.begin(), _earth_patch_lru, it->second.lru_it); + entry = &it->second; + } + else + { + const bool hit_count_budget = (max_create != 0u) && (created_patches >= max_create); + const bool hit_time_budget = (max_create_ms > 0.0) && (ms_patch_create >= max_create_ms); + if (!hit_count_budget && !hit_time_budget) + { + const Clock::time_point t_c0 = Clock::now(); + (void)get_or_create_earth_patch_mesh(*earth, k); + const Clock::time_point t_c1 = Clock::now(); + + created_patches++; + ms_patch_create += std::chrono::duration(t_c1 - t_c0).count(); + } + + auto it2 = _earth_patch_cache.find(k); + if (it2 != _earth_patch_cache.end()) + { + entry = &it2->second; + } + } + } + if (!entry || !entry->mesh || entry->mesh->surfaces.empty()) + { + continue; + } + + const std::shared_ptr &mesh = entry->mesh; + + const WorldVec3 patch_center_world = + earth->center_world + entry->patch_center_dir * earth->radius_m; + const glm::vec3 patch_center_local = world_to_local(patch_center_world, origin_world); + const glm::mat4 transform = glm::translate(glm::mat4(1.0f), patch_center_local); + + uint32_t surface_index = 0; + for (const GeoSurface &surf : mesh->surfaces) + { + RenderObject obj{}; + obj.indexCount = surf.count; + obj.firstIndex = surf.startIndex; + obj.indexBuffer = mesh->meshBuffers.indexBuffer.buffer; + obj.vertexBuffer = mesh->meshBuffers.vertexBuffer.buffer; + obj.vertexBufferAddress = mesh->meshBuffers.vertexBufferAddress; + obj.material = surf.material ? &surf.material->data : nullptr; + obj.bounds = surf.bounds; + obj.transform = transform; + // Planet terrain patches are not meaningful RT occluders; skip BLAS/TLAS builds. + obj.sourceMesh = nullptr; + obj.surfaceIndex = surface_index++; + obj.objectID = draw_context.nextID++; + obj.ownerType = RenderObject::OwnerType::MeshInstance; + obj.ownerName = earth->name; + + draw_context.OpaqueSurfaces.push_back(obj); + } + } + const Clock::time_point t_emit1 = Clock::now(); + + trim_earth_patch_cache(); + + const uint32_t visible_patches = static_cast(_earth_quadtree.visible_leaves().size()); + const uint32_t n = _earth_patch_resolution; + const uint32_t patch_tris = (n >= 2u) ? (2u * (n - 1u) * (n + 3u)) : 0u; + const uint32_t estimated_tris = patch_tris * visible_patches; + + _earth_debug_stats = {}; + _earth_debug_stats.quadtree = _earth_quadtree.stats(); + _earth_debug_stats.visible_patches = visible_patches; + _earth_debug_stats.created_patches = created_patches; + _earth_debug_stats.patch_cache_size = static_cast(_earth_patch_cache.size()); + _earth_debug_stats.estimated_triangles = estimated_tris; + _earth_debug_stats.ms_quadtree = static_cast(std::chrono::duration(t_q1 - t_q0).count()); + _earth_debug_stats.ms_patch_create = static_cast(ms_patch_create); + const double ms_emit_total = std::chrono::duration(t_emit1 - t_emit0).count(); + _earth_debug_stats.ms_emit = static_cast(std::max(0.0, ms_emit_total - ms_patch_create)); + _earth_debug_stats.ms_total = static_cast(std::chrono::duration(Clock::now() - t0).count()); + } + } + + // Other bodies (moon etc.): regular mesh instances. + for (size_t body_index = 0; body_index < _bodies.size(); ++body_index) + { + PlanetBody &b = _bodies[body_index]; + + if (body_index == static_cast(BodyID::Earth)) + { + continue; + } + if (!b.visible || !b.mesh || b.mesh->surfaces.empty()) { continue; diff --git a/src/scene/planet/planet_system.h b/src/scene/planet/planet_system.h index 3715270..476645b 100644 --- a/src/scene/planet/planet_system.h +++ b/src/scene/planet/planet_system.h @@ -1,10 +1,13 @@ #pragma once #include +#include #include +#include #include #include +#include #include class EngineContext; @@ -22,6 +25,19 @@ public: Moon = 1, }; + struct EarthDebugStats + { + planet::PlanetQuadtree::Stats quadtree{}; + uint32_t visible_patches = 0; + uint32_t created_patches = 0; + uint32_t patch_cache_size = 0; + uint32_t estimated_triangles = 0; + float ms_quadtree = 0.0f; + float ms_patch_create = 0.0f; + float ms_emit = 0.0f; + float ms_total = 0.0f; + }; + struct PlanetBody { std::string name; @@ -44,11 +60,44 @@ public: PlanetBody *get_body(BodyID id); const std::vector &bodies() const { return _bodies; } + const planet::PlanetQuadtree::Settings &earth_quadtree_settings() const { return _earth_quadtree_settings; } + void set_earth_quadtree_settings(const planet::PlanetQuadtree::Settings &settings) { _earth_quadtree_settings = settings; } + const EarthDebugStats &earth_debug_stats() const { return _earth_debug_stats; } + + uint32_t earth_patch_create_budget_per_frame() const { return _earth_patch_create_budget_per_frame; } + void set_earth_patch_create_budget_per_frame(uint32_t budget) { _earth_patch_create_budget_per_frame = budget; } + + float earth_patch_create_budget_ms() const { return _earth_patch_create_budget_ms; } + void set_earth_patch_create_budget_ms(float budget_ms) { _earth_patch_create_budget_ms = budget_ms; } + + uint32_t earth_patch_cache_max() const { return _earth_patch_cache_max; } + void set_earth_patch_cache_max(uint32_t max_patches) { _earth_patch_cache_max = max_patches; } + private: + struct EarthPatchCacheEntry + { + std::shared_ptr mesh; + WorldVec3 patch_center_dir{0.0, 0.0, 1.0}; + uint32_t last_used_frame = 0; + std::list::iterator lru_it; + }; + void ensure_bodies_created(); + std::shared_ptr get_or_create_earth_patch_mesh(const PlanetBody &earth, const planet::PatchKey &key); + void trim_earth_patch_cache(); EngineContext *_context = nullptr; bool _enabled = true; std::vector _bodies; -}; + // Earth cube-sphere quadtree (Milestone B4). + planet::PlanetQuadtree _earth_quadtree{}; + planet::PlanetQuadtree::Settings _earth_quadtree_settings{}; + EarthDebugStats _earth_debug_stats{}; + std::unordered_map _earth_patch_cache; + std::list _earth_patch_lru; + uint32_t _earth_patch_resolution = 33; + uint32_t _earth_patch_create_budget_per_frame = 16; + float _earth_patch_create_budget_ms = 2.0f; + uint32_t _earth_patch_cache_max = 2048; +}; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 6241870..a8b0865 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -312,11 +312,6 @@ void SceneManager::update_scene() } } - if (_planetSystem) - { - _planetSystem->update_and_emit(*this, mainDrawContext); - } - glm::mat4 view = mainCamera.getViewMatrix(_camera_position_local); // Use reversed infinite-Z projection (right-handed, -Z forward) to avoid far-plane clipping // on very large scenes. Vulkan clip space is 0..1 (GLM_FORCE_DEPTH_ZERO_TO_ONE) and requires Y flip. @@ -437,6 +432,11 @@ void SceneManager::update_scene() sceneData.rtParams = glm::vec4(ss.hybridRayNoLThreshold, ss.enabled ? 1.0f : 0.0f, 0.0f, 0.0f); } + if (_planetSystem) + { + _planetSystem->update_and_emit(*this, mainDrawContext); + } + // Fill punctual lights into GPUSceneData const uint32_t lightCount = static_cast(std::min(pointLights.size(), static_cast(kMaxPunctualLights))); for (uint32_t i = 0; i < lightCount; ++i) diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index bc6284a..ec39ff1 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -71,6 +71,7 @@ public: void update_scene(); Camera &getMainCamera() { return mainCamera; } + const Camera &getMainCamera() const { return mainCamera; } CameraRig &getCameraRig() { return cameraRig; } const CameraRig &getCameraRig() const { return cameraRig; }