diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d689b6d..a23dfec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -78,6 +78,8 @@ add_executable (vulkan_engine scene/vk_scene.h scene/vk_scene.cpp scene/vk_scene_picking.cpp + scene/mesh_bvh.h + scene/mesh_bvh.cpp scene/vk_loader.h scene/vk_loader.cpp scene/tangent_space.h diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index ed576f2..7641c09 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -466,6 +466,7 @@ void VulkanEngine::draw() _lastPick.mesh = picked.sourceMesh; _lastPick.scene = picked.sourceScene; _lastPick.worldPos = fallbackPos; + _lastPick.worldTransform = picked.transform; _lastPick.firstIndex = picked.firstIndex; _lastPick.indexCount = picked.indexCount; _lastPick.surfaceIndex = picked.surfaceIndex; @@ -496,6 +497,7 @@ void VulkanEngine::draw() _hoverPick.mesh = hoverObj.sourceMesh; _hoverPick.scene = hoverObj.sourceScene; _hoverPick.worldPos = hoverPos; + _hoverPick.worldTransform = hoverObj.transform; _hoverPick.firstIndex = hoverObj.firstIndex; _hoverPick.indexCount = hoverObj.indexCount; _hoverPick.surfaceIndex = hoverObj.surfaceIndex; @@ -816,6 +818,7 @@ void VulkanEngine::run() _lastPick.mesh = hitObject.sourceMesh; _lastPick.scene = hitObject.sourceScene; _lastPick.worldPos = hitPos; + _lastPick.worldTransform = hitObject.transform; _lastPick.firstIndex = hitObject.firstIndex; _lastPick.indexCount = hitObject.indexCount; _lastPick.surfaceIndex = hitObject.surfaceIndex; @@ -851,6 +854,7 @@ void VulkanEngine::run() // Use bounds origin transformed to world as a representative point. glm::vec3 centerWorld = glm::vec3(obj.transform * glm::vec4(obj.bounds.origin, 1.0f)); info.worldPos = centerWorld; + info.worldTransform = obj.transform; info.firstIndex = obj.firstIndex; info.indexCount = obj.indexCount; info.surfaceIndex = obj.surfaceIndex; diff --git a/src/core/vk_engine.h b/src/core/vk_engine.h index 27b6fd9..5bfde3c 100644 --- a/src/core/vk_engine.h +++ b/src/core/vk_engine.h @@ -121,6 +121,7 @@ public: MeshAsset *mesh = nullptr; LoadedGLTF *scene = nullptr; glm::vec3 worldPos{0.0f}; + glm::mat4 worldTransform{1.0f}; uint32_t indexCount = 0; uint32_t firstIndex = 0; uint32_t surfaceIndex = 0; @@ -151,6 +152,8 @@ public: // Toggle to enable/disable ID-buffer picking in addition to raycast bool _useIdBufferPicking = false; + // Debug: draw mesh BVH boxes for last pick + bool _debugDrawBVH = false; // Debug: persistent pass enable overrides (by pass name) std::unordered_map _rgPassToggles; diff --git a/src/core/vk_engine_ui.cpp b/src/core/vk_engine_ui.cpp index 288ea87..d30c3cd 100644 --- a/src/core/vk_engine_ui.cpp +++ b/src/core/vk_engine_ui.cpp @@ -19,6 +19,8 @@ #include "engine_context.h" #include +#include "mesh_bvh.h" + namespace { // Background / compute playground @@ -538,7 +540,9 @@ static void ui_scene(VulkanEngine *eng) ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size()); ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size()); ImGui::Checkbox("Use ID-buffer picking", &eng->_useIdBufferPicking); + ImGui::Checkbox("Debug draw mesh BVH (last pick)", &eng->_debugDrawBVH); ImGui::Separator(); + if (eng->_lastPick.valid) { const char *meshName = eng->_lastPick.mesh ? eng->_lastPick.mesh->name.c_str() : ""; @@ -556,6 +560,21 @@ static void ui_scene(VulkanEngine *eng) ImGui::Text("Indices: first=%u count=%u", eng->_lastPick.firstIndex, eng->_lastPick.indexCount); + + if (eng->_sceneManager) + { + const SceneManager::PickingDebug &dbg = eng->_sceneManager->getPickingDebug(); + ImGui::Text("Mesh BVH used: %s, hit: %s, fallback box: %s", + dbg.usedMeshBVH ? "yes" : "no", + dbg.meshBVHHit ? "yes" : "no", + dbg.meshBVHFallbackBox ? "yes" : "no"); + if (dbg.meshBVHPrimCount > 0) + { + ImGui::Text("Mesh BVH stats: prims=%u, nodes=%u", + dbg.meshBVHPrimCount, + dbg.meshBVHNodeCount); + } + } } else { diff --git a/src/scene/vk_loader.cpp b/src/scene/vk_loader.cpp index 8c45481..78e49db 100644 --- a/src/scene/vk_loader.cpp +++ b/src/scene/vk_loader.cpp @@ -18,6 +18,7 @@ #include #include #include "tangent_space.h" +#include "mesh_bvh.h" //> loadimg std::optional load_image(VulkanEngine *engine, fastgltf::Asset &asset, fastgltf::Image &image, bool srgb) { @@ -592,18 +593,25 @@ std::optional > loadGltf(VulkanEngine *engine, std:: newSurface.bounds.origin = (maxpos + minpos) / 2.f; newSurface.bounds.extents = (maxpos - minpos) / 2.f; newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents); - newSurface.bounds.type = BoundsType::Box; + newSurface.bounds.type = BoundsType::Mesh; } else { newSurface.bounds.origin = glm::vec3(0.0f); newSurface.bounds.extents = glm::vec3(0.5f); newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents); - newSurface.bounds.type = BoundsType::Box; + newSurface.bounds.type = BoundsType::Mesh; } newmesh->surfaces.push_back(newSurface); } + // Build CPU BVH for precise picking over this mesh (triangle-level). + { + std::span vSpan(vertices.data(), vertices.size()); + std::span iSpan(indices.data(), indices.size()); + newmesh->bvh = build_mesh_bvh(*newmesh, vSpan, iSpan); + } + newmesh->meshBuffers = engine->_resourceManager->uploadMesh(indices, vertices); // If CPU vectors ballooned for this mesh, release capacity back to the OS auto shrink_if_huge = [](auto &vec, size_t elemSizeBytes) { diff --git a/src/scene/vk_loader.h b/src/scene/vk_loader.h index d11970f..66f1a6f 100644 --- a/src/scene/vk_loader.h +++ b/src/scene/vk_loader.h @@ -45,6 +45,8 @@ struct GeoSurface std::shared_ptr material; }; +struct MeshBVH; + struct MeshAsset { std::string name; @@ -52,6 +54,9 @@ struct MeshAsset std::vector surfaces; GPUMeshBuffers meshBuffers; + + // Optional CPU BVH for precise picking / queries. + std::shared_ptr bvh; }; struct LoadedGLTF : public IRenderable diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index 581731f..2b99e8a 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -115,6 +115,17 @@ public: float scene_update_time = 0.f; } stats; + struct PickingDebug + { + bool usedMeshBVH = false; + bool meshBVHHit = false; + bool meshBVHFallbackBox = false; + uint32_t meshBVHPrimCount = 0; + uint32_t meshBVHNodeCount = 0; + }; + + const PickingDebug &getPickingDebug() const { return pickingDebug; } + private: EngineContext *_context = nullptr; @@ -126,4 +137,6 @@ private: std::unordered_map > loadedNodes; std::unordered_map dynamicMeshInstances; std::unordered_map dynamicGLTFInstances; + + PickingDebug pickingDebug{}; }; diff --git a/src/scene/vk_scene_picking.cpp b/src/scene/vk_scene_picking.cpp index fff977b..7ae5c82 100644 --- a/src/scene/vk_scene_picking.cpp +++ b/src/scene/vk_scene_picking.cpp @@ -2,6 +2,7 @@ #include "vk_swapchain.h" #include "core/engine_context.h" +#include "mesh_bvh.h" #include "glm/gtx/transform.hpp" #include @@ -14,6 +15,13 @@ namespace { + struct BoundsHitDebug + { + bool usedBVH = false; + bool bvhHit = false; + bool fallbackBox = false; + }; + // Ray / oriented-box intersection in world space using object-local AABB. // Returns true when hit; outWorldHit is the closest hit point in world space. bool intersect_ray_box(const glm::vec3 &rayOrigin, @@ -262,14 +270,18 @@ namespace } // Ray / oriented-bounds intersection in world space using object-local shape. - // Uses a quick sphere test first; on success refines based on BoundsType. + // For non-mesh shapes we use a quick world-space bounding-sphere pretest; + // for mesh bounds we go directly to the mesh BVH (which already has a root AABB). // Returns true when hit; outWorldHit is the closest hit point in world space. bool intersect_ray_bounds(const glm::vec3 &rayOrigin, const glm::vec3 &rayDir, - const Bounds &bounds, - const glm::mat4 &worldTransform, - glm::vec3 &outWorldHit) + const RenderObject &obj, + glm::vec3 &outWorldHit, + BoundsHitDebug *debug) { + const Bounds &bounds = obj.bounds; + const glm::mat4 &worldTransform = obj.transform; + // Non-pickable object. if (bounds.type == BoundsType::None) { @@ -281,32 +293,63 @@ namespace return false; } - // Early reject using bounding sphere in world space. - float sphereT = 0.0f; - if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT)) - { - return false; - } - - // Shape-specific refinement after the conservative sphere test. switch (bounds.type) { case BoundsType::Sphere: { + // Early reject using bounding sphere in world space. + float sphereT = 0.0f; + if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT)) + { + return false; + } // We already have the hit distance along the ray from the sphere test. outWorldHit = rayOrigin + rayDir * sphereT; return true; } case BoundsType::Capsule: { + float sphereT = 0.0f; + if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT)) + { + return false; + } return intersect_ray_capsule(rayOrigin, rayDir, bounds, worldTransform, outWorldHit); } + case BoundsType::Mesh: + { + // Try high-precision mesh BVH first when available, then fall back to box. + if (obj.sourceMesh && obj.sourceMesh->bvh) + { + if (debug) + { + debug->usedBVH = true; + } + MeshBVHPickHit meshHit{}; + if (intersect_ray_mesh_bvh(*obj.sourceMesh->bvh, worldTransform, rayOrigin, rayDir, meshHit)) + { + if (debug) + { + debug->bvhHit = true; + } + outWorldHit = meshHit.worldPos; + return true; + } + if (debug) + { + debug->fallbackBox = true; + } + } + // return intersect_ray_box(rayOrigin, rayDir, bounds, worldTransform, outWorldHit); + } case BoundsType::Box: - case BoundsType::Mesh: // TODO: replace with BVH/mesh query; box is a safe fallback. default: { - // For Capsule and Mesh we currently fall back to the oriented box; - // this still benefits from tighter AABBs if you author them. + float sphereT = 0.0f; + if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT)) + { + return false; + } return intersect_ray_box(rayOrigin, rayDir, bounds, worldTransform, outWorldHit); } } @@ -397,12 +440,16 @@ bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject float bestDist2 = std::numeric_limits::max(); glm::vec3 bestHitPos{}; + // Reset debug info for this pick. + pickingDebug = {}; + auto testList = [&](const std::vector &list) { for (const RenderObject &obj: list) { glm::vec3 hitPos{}; - if (!intersect_ray_bounds(rayOrigin, rayDir, obj.bounds, obj.transform, hitPos)) + BoundsHitDebug localDebug{}; + if (!intersect_ray_bounds(rayOrigin, rayDir, obj, hitPos, &localDebug)) { continue; } @@ -414,6 +461,23 @@ bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject bestHitPos = hitPos; outObject = obj; anyHit = true; + + // Capture debug info for the best hit so far. + pickingDebug.usedMeshBVH = localDebug.usedBVH; + pickingDebug.meshBVHHit = localDebug.bvhHit; + pickingDebug.meshBVHFallbackBox = localDebug.fallbackBox; + if (obj.sourceMesh && obj.sourceMesh->bvh) + { + pickingDebug.meshBVHPrimCount = + static_cast(obj.sourceMesh->bvh->primitives.size()); + pickingDebug.meshBVHNodeCount = + static_cast(obj.sourceMesh->bvh->nodes.size()); + } + else + { + pickingDebug.meshBVHPrimCount = 0; + pickingDebug.meshBVHNodeCount = 0; + } } } };