ADD: BVH Selection triangle

This commit is contained in:
2025-11-19 23:33:05 +09:00
parent ac35de6104
commit 0fe7c0f975
10 changed files with 116 additions and 84 deletions

View File

@@ -8,6 +8,7 @@
#include <render/vk_materials.h>
#include <render/primitives.h>
#include <scene/tangent_space.h>
#include <scene/mesh_bvh.h>
#include <stb_image.h>
#include "asset_locator.h"
#include <core/texture_cache.h>
@@ -531,6 +532,10 @@ std::shared_ptr<MeshAsset> 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;
}

View File

@@ -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<const uint32_t *>(_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<const uint32_t *>(_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,

View File

@@ -127,6 +127,7 @@ public:
uint32_t surfaceIndex = 0;
bool valid = false;
} _lastPick;
uint32_t _lastPickObjectID = 0;
struct PickRequest
{

View File

@@ -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,

View File

@@ -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();

View File

@@ -58,10 +58,14 @@ std::unique_ptr<MeshBVH> 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<float>(p0.x, p0.y, p0.z));
prim.bounds.expand(Vec3<float>(p1.x, p1.y, p1.z));
prim.bounds.expand(Vec3<float>(p2.x, p2.y, p2.z));
prim.v0 = Vec3<float>(p0.x, p0.y, p0.z);
prim.v1 = Vec3<float>(p1.x, p1.y, p1.z);
prim.v2 = Vec3<float>(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;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}