From 0fe7c0f9754525448e33e7f6e5ce13bed8032b76 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Wed, 19 Nov 2025 23:33:05 +0900 Subject: [PATCH] ADD: BVH Selection triangle --- src/core/asset_manager.cpp | 5 ++ src/core/vk_engine.cpp | 152 ++++++++++++++++++--------------- src/core/vk_engine.h | 1 + src/core/vk_engine_ui.cpp | 5 ++ src/scene/camera.h | 2 +- src/scene/mesh_bvh.cpp | 11 ++- src/scene/vk_loader.h | 2 +- src/scene/vk_scene.cpp | 4 +- src/scene/vk_scene_picking.cpp | 16 ++-- third_party/BVH | 2 +- 10 files changed, 116 insertions(+), 84 deletions(-) diff --git a/src/core/asset_manager.cpp b/src/core/asset_manager.cpp index eb4234b..44a76a6 100644 --- a/src/core/asset_manager.cpp +++ b/src/core/asset_manager.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "asset_locator.h" #include @@ -531,6 +532,10 @@ 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); + _meshCache.emplace(name, mesh); return mesh; } diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 7641c09..428f5a9 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -438,53 +438,6 @@ void VulkanEngine::cleanup() void VulkanEngine::draw() { - //> frame_clear - //wait until the gpu has finished rendering the last frame. Timeout of 1 second - VK_CHECK(vkWaitForFences(_deviceManager->device(), 1, &get_current_frame()._renderFence, true, 1000000000)); - - // If we scheduled an ID-buffer readback in the previous frame, resolve it now. - if (_pickResultPending && _pickReadbackBuffer.buffer && _sceneManager) - { - vmaInvalidateAllocation(_deviceManager->allocator(), _pickReadbackBuffer.allocation, 0, sizeof(uint32_t)); - uint32_t pickedID = 0; - if (_pickReadbackBuffer.info.pMappedData) - { - pickedID = *reinterpret_cast(_pickReadbackBuffer.info.pMappedData); - } - - if (pickedID == 0) - { - // Do not override existing raycast pick when ID buffer reports "no object". - } - else - { - RenderObject picked{}; - if (_sceneManager->resolveObjectID(pickedID, picked)) - { - // Fallback hit position: object origin in world space (can refine later) - glm::vec3 fallbackPos = glm::vec3(picked.transform[3]); - _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; - _lastPick.valid = true; - } - } - _pickResultPending = false; - } - - get_current_frame()._deletionQueue.flush(); - // Resolve last frame's pass timings before we clear and rebuild the graph - if (_renderGraph) - { - _renderGraph->resolve_timings(); - } - get_current_frame()._frameDescriptors.clear_pools(_deviceManager->device()); - //< frame_clear - _sceneManager->update_scene(); // Per-frame hover raycast based on last mouse position. @@ -808,34 +761,39 @@ void VulkanEngine::run() if (treatAsClick) { - // Raycast click selection - if (_sceneManager) - { - RenderObject hitObject{}; - glm::vec3 hitPos{}; - if (_sceneManager->pick(releasePos, hitObject, hitPos)) - { - _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; - _lastPick.valid = true; - } - else - { - _lastPick.valid = false; - } - } - - // Optionally queue an ID-buffer pick at this position if (_useIdBufferPicking) { + // Asynchronous ID-buffer clicking: queue a pick request for this position. + // The result will be resolved at the start of a future frame from the ID buffer. _pendingPick.active = true; _pendingPick.windowPos = releasePos; } + else + { + // Raycast click selection (CPU side) when ID-buffer picking is disabled. + if (_sceneManager) + { + RenderObject hitObject{}; + glm::vec3 hitPos{}; + if (_sceneManager->pick(releasePos, hitObject, hitPos)) + { + _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; + _lastPick.valid = true; + _lastPickObjectID = hitObject.objectID; + } + else + { + _lastPick.valid = false; + _lastPickObjectID = 0; + } + } + } } else { @@ -881,6 +839,58 @@ void VulkanEngine::run() _swapchainManager->resize_swapchain(_window); } + // Begin frame: wait for the GPU, resolve pending ID-buffer picks, + // and clear per-frame resources before building UI and recording commands. + VK_CHECK(vkWaitForFences(_deviceManager->device(), 1, &get_current_frame()._renderFence, true, 1000000000)); + + if (_pickResultPending && _pickReadbackBuffer.buffer && _sceneManager) + { + vmaInvalidateAllocation(_deviceManager->allocator(), _pickReadbackBuffer.allocation, 0, sizeof(uint32_t)); + uint32_t pickedID = 0; + if (_pickReadbackBuffer.info.pMappedData) + { + pickedID = *reinterpret_cast(_pickReadbackBuffer.info.pMappedData); + } + + if (pickedID == 0) + { + // No object under cursor in ID buffer: clear last pick. + _lastPick.valid = false; + _lastPickObjectID = 0; + } + else + { + _lastPickObjectID = pickedID; + RenderObject picked{}; + if (_sceneManager->resolveObjectID(pickedID, picked)) + { + // Fallback hit position: object origin in world space (can refine later) + glm::vec3 fallbackPos = glm::vec3(picked.transform[3]); + _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; + _lastPick.valid = true; + } + else + { + _lastPick.valid = false; + _lastPickObjectID = 0; + } + } + _pickResultPending = false; + } + + get_current_frame()._deletionQueue.flush(); + if (_renderGraph) + { + _renderGraph->resolve_timings(); + } + get_current_frame()._frameDescriptors.clear_pools(_deviceManager->device()); + // imgui new frame ImGui_ImplVulkan_NewFrame(); @@ -918,7 +928,7 @@ void VulkanEngine::init_frame_resources() _frames[i].init(_deviceManager.get(), frame_sizes); } - // Allocate a small readback buffer for ID-buffer picking (single uint32 pixel) + // Allocate a small readback buffer for ID-buffer picking (single uint32 pixel). _pickReadbackBuffer = _resourceManager->create_buffer( sizeof(uint32_t), VK_BUFFER_USAGE_TRANSFER_DST_BIT, diff --git a/src/core/vk_engine.h b/src/core/vk_engine.h index 5bfde3c..76a9746 100644 --- a/src/core/vk_engine.h +++ b/src/core/vk_engine.h @@ -127,6 +127,7 @@ public: uint32_t surfaceIndex = 0; bool valid = false; } _lastPick; + uint32_t _lastPickObjectID = 0; struct PickRequest { diff --git a/src/core/vk_engine_ui.cpp b/src/core/vk_engine_ui.cpp index d30c3cd..54b8e38 100644 --- a/src/core/vk_engine_ui.cpp +++ b/src/core/vk_engine_ui.cpp @@ -540,6 +540,8 @@ 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::Text("Picking mode: %s", + eng->_useIdBufferPicking ? "ID buffer (async, 1-frame latency)" : "CPU raycast"); ImGui::Checkbox("Debug draw mesh BVH (last pick)", &eng->_debugDrawBVH); ImGui::Separator(); @@ -552,6 +554,9 @@ static void ui_scene(VulkanEngine *eng) sceneName = eng->_lastPick.scene->debugName.c_str(); } ImGui::Text("Last pick scene: %s", sceneName); + ImGui::Text("Last pick source: %s", + eng->_useIdBufferPicking ? "ID buffer" : "CPU raycast"); + ImGui::Text("Last pick object ID: %u", eng->_lastPickObjectID); ImGui::Text("Last pick mesh: %s (surface %u)", meshName, eng->_lastPick.surfaceIndex); ImGui::Text("World pos: (%.3f, %.3f, %.3f)", eng->_lastPick.worldPos.x, diff --git a/src/scene/camera.h b/src/scene/camera.h index 52433e7..ac70c03 100644 --- a/src/scene/camera.h +++ b/src/scene/camera.h @@ -20,7 +20,7 @@ public: bool rmbDown { false }; // Field of view in degrees for projection - float fovDegrees { 70.f }; + float fovDegrees { 50.f }; glm::mat4 getViewMatrix(); glm::mat4 getRotationMatrix(); diff --git a/src/scene/mesh_bvh.cpp b/src/scene/mesh_bvh.cpp index 788a9cc..ebbbeb1 100644 --- a/src/scene/mesh_bvh.cpp +++ b/src/scene/mesh_bvh.cpp @@ -58,10 +58,14 @@ std::unique_ptr build_mesh_bvh(const MeshAsset &mesh, const glm::vec3 &p1 = vertices[i1].position; const glm::vec3 &p2 = vertices[i2].position; + // BVH2 now expects triangle primitives with explicit vertices. + // Store the triangle in mesh-local space and let the library + // compute/update the AABB used for hierarchy construction. PrimitiveF prim{}; - prim.bounds.expand(Vec3(p0.x, p0.y, p0.z)); - prim.bounds.expand(Vec3(p1.x, p1.y, p1.z)); - prim.bounds.expand(Vec3(p2.x, p2.y, p2.z)); + prim.v0 = Vec3(p0.x, p0.y, p0.z); + prim.v1 = Vec3(p1.x, p1.y, p1.z); + prim.v2 = Vec3(p2.x, p2.y, p2.z); + prim.updateBounds(); result->primitives.push_back(prim); MeshBVHPrimitiveRef ref{}; @@ -144,4 +148,3 @@ bool intersect_ray_mesh_bvh(const MeshBVH &bvh, return true; } - diff --git a/src/scene/vk_loader.h b/src/scene/vk_loader.h index 66f1a6f..0b2b5a9 100644 --- a/src/scene/vk_loader.h +++ b/src/scene/vk_loader.h @@ -21,7 +21,7 @@ enum class BoundsType : uint8_t Box, // oriented box using origin/extents (default) Sphere, // sphere using origin + sphereRadius Capsule, // capsule aligned with local Y (derived from extents) - Mesh // full mesh (BVH / ray query); currently falls back to Box + Mesh // full mesh (BVH / ray query using mesh BVH when available) }; struct Bounds diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index bb3b2b6..cf984b6 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -149,7 +149,9 @@ void SceneManager::update_scene() return m; }; - const float fov = glm::radians(70.f); + // Keep projection FOV in sync with the camera so that CPU ray picking + // matches what is rendered on-screen. + const float fov = glm::radians(mainCamera.fovDegrees); const float aspect = (float) _context->getSwapchain()->windowExtent().width / (float) _context->getSwapchain()->windowExtent().height; const float nearPlane = 0.1f; diff --git a/src/scene/vk_scene_picking.cpp b/src/scene/vk_scene_picking.cpp index 7ae5c82..a269596 100644 --- a/src/scene/vk_scene_picking.cpp +++ b/src/scene/vk_scene_picking.cpp @@ -89,15 +89,21 @@ namespace return false; } - float tHit = (tMin >= 0.0f) ? tMin : tMax; - glm::vec3 localHit = localOrigin + tHit * localDir; - glm::vec3 worldHit = glm::vec3(worldTransform * glm::vec4(localHit, 1.0f)); - - if (glm::dot(worldHit - rayOrigin, rayDir) <= 0.0f) + // Choose the closest intersection in front of the ray origin. + // If the ray starts inside the box (tMin <= 0), use the exit point tMax. + float tHit = tMin; + if (tHit <= 0.0f) + { + tHit = tMax; + } + if (tHit <= 0.0f) { return false; } + glm::vec3 localHit = localOrigin + tHit * localDir; + glm::vec3 worldHit = glm::vec3(worldTransform * glm::vec4(localHit, 1.0f)); + outWorldHit = worldHit; return true; } diff --git a/third_party/BVH b/third_party/BVH index 2a9838e..aa4fb99 160000 --- a/third_party/BVH +++ b/third_party/BVH @@ -1 +1 @@ -Subproject commit 2a9838e7efd56b20910e35148bf547b17894a954 +Subproject commit aa4fb9949b5fca0e01747a93a3b0393f4c0b34c5