ADD: BVH Selection triangle
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -127,6 +127,7 @@ public:
|
||||
uint32_t surfaceIndex = 0;
|
||||
bool valid = false;
|
||||
} _lastPick;
|
||||
uint32_t _lastPickObjectID = 0;
|
||||
|
||||
struct PickRequest
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
2
third_party/BVH
vendored
2
third_party/BVH
vendored
Submodule third_party/BVH updated: 2a9838e7ef...aa4fb9949b
Reference in New Issue
Block a user