ADD: planet quadtree
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -587,7 +587,8 @@ std::pair<AllocatedImage, bool> AssetManager::loadImageFromAsset(std::string_vie
|
||||
std::shared_ptr<MeshAsset> AssetManager::createMesh(const std::string &name,
|
||||
std::span<Vertex> vertices,
|
||||
std::span<uint32_t> indices,
|
||||
std::shared_ptr<GLTFMaterial> material)
|
||||
std::shared_ptr<GLTFMaterial> material,
|
||||
bool build_bvh)
|
||||
{
|
||||
if (!_engine || !_engine->_resourceManager) return {};
|
||||
if (name.empty()) return {};
|
||||
@@ -631,9 +632,12 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const std::string &name,
|
||||
surf.bounds = compute_bounds(vertices);
|
||||
mesh->surfaces.push_back(surf);
|
||||
|
||||
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<MeshAsset> 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<AllocatedBuffer> materialBuffer;
|
||||
auto itb = _meshMaterialBuffers.find(name);
|
||||
if (itb != _meshMaterialBuffers.end())
|
||||
{
|
||||
materialBuffer = itb->second;
|
||||
_meshMaterialBuffers.erase(itb);
|
||||
}
|
||||
|
||||
std::vector<AllocatedImage> 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;
|
||||
}
|
||||
|
||||
@@ -107,11 +107,13 @@ public:
|
||||
std::shared_ptr<MeshAsset> createMesh(const std::string &name,
|
||||
std::span<Vertex> vertices,
|
||||
std::span<uint32_t> indices,
|
||||
std::shared_ptr<GLTFMaterial> material = {});
|
||||
std::shared_ptr<GLTFMaterial> material = {},
|
||||
bool build_bvh = true);
|
||||
|
||||
std::shared_ptr<MeshAsset> 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<GLTFMaterial> createMaterialFromConstants(const std::string &name,
|
||||
|
||||
@@ -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<uint32_t>(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();
|
||||
ImGui::TextUnformatted("Point lights");
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
@@ -1941,60 +1921,19 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
// Delete selected model/primitive (uses last pick if valid, otherwise hover)
|
||||
static std::string deleteStatus;
|
||||
if (ImGui::Button("Delete selected"))
|
||||
// Picking & Gizmo - picking info and transform editor
|
||||
static void ui_picking_gizmo(VulkanEngine *eng)
|
||||
{
|
||||
deleteStatus.clear();
|
||||
const PickingSystem::PickInfo *pick = nullptr;
|
||||
if (picking)
|
||||
if (!eng || !eng->_sceneManager)
|
||||
{
|
||||
const auto &last = picking->last_pick();
|
||||
const auto &hover = picking->hover_pick();
|
||||
pick = last.valid ? &last : (hover.valid ? &hover : nullptr);
|
||||
ImGui::TextUnformatted("SceneManager not available");
|
||||
return;
|
||||
}
|
||||
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)
|
||||
@@ -2335,6 +2266,89 @@ namespace
|
||||
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<int>(settings.max_level);
|
||||
if (ImGui::SliderInt("Max LOD level", &maxLevel, 0, 20))
|
||||
{
|
||||
settings.max_level = static_cast<uint32_t>(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<int>(settings.max_patches_visible);
|
||||
if (ImGui::SliderInt("Max visible patches", &maxPatches, 64, 20000))
|
||||
{
|
||||
settings.max_patches_visible = static_cast<uint32_t>(std::max(6, maxPatches));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
int createBudget = static_cast<int>(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<uint32_t>(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<int>(planets->earth_patch_cache_max());
|
||||
if (ImGui::SliderInt("Patch cache max", &cacheMax, 0, 50000))
|
||||
{
|
||||
planets->set_earth_patch_cache_max(static_cast<uint32_t>(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<float>(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<double>(std::max(0.0f, maxEdge));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
float maxAlt = static_cast<float>(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<double>(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))
|
||||
{
|
||||
if (ImGui::BeginTabBar("DebugTabs", ImGuiTabBarFlags_None))
|
||||
{
|
||||
if (ImGui::BeginTabItem("Overview"))
|
||||
{
|
||||
ui_overview(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_window)
|
||||
if (ImGui::BeginTabItem("Scene Editor"))
|
||||
{
|
||||
if (ImGui::Begin("Window Settings", &g_debug_windows.show_window))
|
||||
{
|
||||
ui_window(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
ui_scene_editor(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_background)
|
||||
if (ImGui::BeginTabItem("Lights"))
|
||||
{
|
||||
if (ImGui::Begin("Background", &g_debug_windows.show_background))
|
||||
{
|
||||
ui_background(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
ui_lights(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_particles)
|
||||
if (ImGui::BeginTabItem("Picking & Gizmo"))
|
||||
{
|
||||
if (ImGui::Begin("Particles", &g_debug_windows.show_particles))
|
||||
{
|
||||
ui_particles(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
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 (g_debug_windows.show_render_graph)
|
||||
{
|
||||
if (ImGui::Begin("Render Graph", &g_debug_windows.show_render_graph))
|
||||
{
|
||||
ui_render_graph(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_pipelines)
|
||||
{
|
||||
if (ImGui::Begin("Pipelines", &g_debug_windows.show_pipelines))
|
||||
{
|
||||
ui_pipelines(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_ibl)
|
||||
{
|
||||
if (ImGui::Begin("IBL", &g_debug_windows.show_ibl))
|
||||
{
|
||||
ui_ibl(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_postfx)
|
||||
{
|
||||
if (ImGui::Begin("PostFX", &g_debug_windows.show_postfx))
|
||||
{
|
||||
ui_postfx(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_scene)
|
||||
{
|
||||
if (ImGui::Begin("Scene", &g_debug_windows.show_scene))
|
||||
{
|
||||
ui_scene(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_camera)
|
||||
{
|
||||
if (ImGui::Begin("Camera", &g_debug_windows.show_camera))
|
||||
if (ImGui::BeginTabItem("Camera"))
|
||||
{
|
||||
ui_camera(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_planets)
|
||||
{
|
||||
if (ImGui::Begin("Planets", &g_debug_windows.show_planets))
|
||||
if (ImGui::BeginTabItem("Planets"))
|
||||
{
|
||||
ui_planets(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_async_assets)
|
||||
if (ImGui::BeginTabItem("Render Graph"))
|
||||
{
|
||||
if (ImGui::Begin("Async Assets", &g_debug_windows.show_async_assets))
|
||||
{
|
||||
ui_async_assets(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
ui_render_graph(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_textures)
|
||||
if (ImGui::BeginTabItem("Pipelines"))
|
||||
{
|
||||
if (ImGui::Begin("Textures", &g_debug_windows.show_textures))
|
||||
ui_pipelines(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Shadows"))
|
||||
{
|
||||
ui_shadows(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("IBL"))
|
||||
{
|
||||
ui_ibl(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("PostFX"))
|
||||
{
|
||||
ui_postfx(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Background"))
|
||||
{
|
||||
ui_background(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Particles"))
|
||||
{
|
||||
ui_particles(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
266
src/scene/planet/cubesphere.cpp
Normal file
266
src/scene/planet/cubesphere.cpp
Normal file
@@ -0,0 +1,266 @@
|
||||
#include "cubesphere.h"
|
||||
|
||||
#include <scene/tangent_space.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
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<double>(tiles_u)) : 1.0;
|
||||
|
||||
const double u0_01 = static_cast<double>(x) * inv_tiles;
|
||||
const double u1_01 = static_cast<double>(x + 1u) * inv_tiles;
|
||||
const double v0_01 = static_cast<double>(y) * inv_tiles;
|
||||
const double v1_01 = static_cast<double>(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<double>() * 0.5) * radius_m;
|
||||
const uint32_t safe_level = (level < 30u) ? level : 30u;
|
||||
const double tiles_per_axis = static_cast<double>(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<size_t>(base_vertex_count) + static_cast<size_t>(skirt_vertex_count));
|
||||
|
||||
const double inv = 1.0 / static_cast<double>(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<float>(static_cast<double>(j) * inv);
|
||||
const double v = v0 + dv * static_cast<double>(j);
|
||||
|
||||
for (uint32_t i = 0; i < resolution; ++i)
|
||||
{
|
||||
const float s = static_cast<float>(static_cast<double>(i) * inv);
|
||||
const double u = u0 + du * static_cast<double>(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<float>(delta_d.x),
|
||||
static_cast<float>(delta_d.y),
|
||||
static_cast<float>(delta_d.z));
|
||||
vert.normal = glm::vec3(static_cast<float>(unit_dir.x),
|
||||
static_cast<float>(unit_dir.y),
|
||||
static_cast<float>(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<double>(n.x),
|
||||
static_cast<double>(n.y),
|
||||
static_cast<double>(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<float>(delta_d.x),
|
||||
static_cast<float>(delta_d.y),
|
||||
static_cast<float>(delta_d.z));
|
||||
vert.normal = glm::vec3(static_cast<float>(unit_dir.x),
|
||||
static_cast<float>(unit_dir.y),
|
||||
static_cast<float>(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<size_t>(resolution - 1u) * static_cast<size_t>(resolution - 1u) * 6u;
|
||||
const size_t skirt_index_count = static_cast<size_t>(4u) * static_cast<size_t>(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
|
||||
69
src/scene/planet/cubesphere.h
Normal file
69
src/scene/planet/cubesphere.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <core/types.h>
|
||||
#include <core/world.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/vec4.hpp>
|
||||
|
||||
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<Vertex> vertices;
|
||||
std::vector<uint32_t> 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
|
||||
242
src/scene/planet/planet_quadtree.cpp
Normal file
242
src/scene/planet/planet_quadtree.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
#include "planet_quadtree.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
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<double>());
|
||||
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<glm::vec3, 8> 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<float>(logical_extent.height) * 0.5f);
|
||||
if (!(proj_scale > 0.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
thread_local std::vector<Node> stack;
|
||||
stack.clear();
|
||||
stack.reserve(256);
|
||||
|
||||
const size_t max_visible_leaves =
|
||||
(_settings.max_patches_visible > 0u)
|
||||
? static_cast<size_t>(std::max(_settings.max_patches_visible, 6u))
|
||||
: std::numeric_limits<size_t>::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<float>(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<float>((error_m / dist_m) * static_cast<double>(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<uint32_t>(_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
|
||||
82
src/scene/planet/planet_quadtree.h
Normal file
82
src/scene/planet/planet_quadtree.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "cubesphere.h"
|
||||
|
||||
#include <core/types.h>
|
||||
#include <core/world.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
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<uint64_t>(k.face) & 0xFFull;
|
||||
const uint64_t l = static_cast<uint64_t>(k.level) & 0x3Full;
|
||||
const uint64_t x = static_cast<uint64_t>(k.x) & 0x1FFFFFull;
|
||||
const uint64_t y = static_cast<uint64_t>(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<uint64_t>{}(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<PatchKey> &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<PatchKey> _visible_leaves;
|
||||
};
|
||||
} // namespace planet
|
||||
@@ -1,15 +1,21 @@
|
||||
#include "planet_system.h"
|
||||
|
||||
#include <core/context.h>
|
||||
#include <core/frame/resources.h>
|
||||
#include <core/types.h>
|
||||
#include <core/assets/manager.h>
|
||||
#include <render/materials.h>
|
||||
#include <render/primitives.h>
|
||||
#include <scene/planet/cubesphere.h>
|
||||
#include <scene/tangent_space.h>
|
||||
#include <scene/vk_scene.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double kEarthRadiusM = 6378137.0; // WGS84 equatorial radius
|
||||
@@ -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<float>(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<MeshAsset> 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<uint32_t>(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<MeshAsset> 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<size_t>(_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<size_t>(_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<MeshAsset> 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<double>(_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<double, std::milli>(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<MeshAsset> &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<uint32_t>(_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<uint32_t>(_earth_patch_cache.size());
|
||||
_earth_debug_stats.estimated_triangles = estimated_tris;
|
||||
_earth_debug_stats.ms_quadtree = static_cast<float>(std::chrono::duration<double, std::milli>(t_q1 - t_q0).count());
|
||||
_earth_debug_stats.ms_patch_create = static_cast<float>(ms_patch_create);
|
||||
const double ms_emit_total = std::chrono::duration<double, std::milli>(t_emit1 - t_emit0).count();
|
||||
_earth_debug_stats.ms_emit = static_cast<float>(std::max(0.0, ms_emit_total - ms_patch_create));
|
||||
_earth_debug_stats.ms_total = static_cast<float>(std::chrono::duration<double, std::milli>(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<size_t>(BodyID::Earth))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!b.visible || !b.mesh || b.mesh->surfaces.empty())
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <core/world.h>
|
||||
#include <scene/planet/planet_quadtree.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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<PlanetBody> &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<MeshAsset> mesh;
|
||||
WorldVec3 patch_center_dir{0.0, 0.0, 1.0};
|
||||
uint32_t last_used_frame = 0;
|
||||
std::list<planet::PatchKey>::iterator lru_it;
|
||||
};
|
||||
|
||||
void ensure_bodies_created();
|
||||
std::shared_ptr<MeshAsset> 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<PlanetBody> _bodies;
|
||||
};
|
||||
|
||||
// Earth cube-sphere quadtree (Milestone B4).
|
||||
planet::PlanetQuadtree _earth_quadtree{};
|
||||
planet::PlanetQuadtree::Settings _earth_quadtree_settings{};
|
||||
EarthDebugStats _earth_debug_stats{};
|
||||
std::unordered_map<planet::PatchKey, EarthPatchCacheEntry, planet::PatchKeyHash> _earth_patch_cache;
|
||||
std::list<planet::PatchKey> _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;
|
||||
};
|
||||
|
||||
@@ -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<uint32_t>(std::min(pointLights.size(), static_cast<size_t>(kMaxPunctualLights)));
|
||||
for (uint32_t i = 0; i < lightCount; ++i)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user