8.8 KiB
Picking System: Object Selection and Interaction
Unified picking system that handles single-click selection, hover detection, and drag-box multi-select. Supports both CPU ray-casting (via BVH) and GPU ID-buffer picking.
Components
-
PickingSystem(src/core/picking/picking_system.h/.cpp)- Main entry point for all picking operations.
- Processes SDL mouse events (click, drag, release).
- Maintains per-frame hover picks and last click selection.
- Integrates with RenderGraph for async ID-buffer readback.
-
SceneManagerpicking helpers (src/scene/vk_scene_picking.cpp)- CPU ray-casting against
RenderObjectbounds. - BVH-accelerated mesh picking for precise triangle-level hits.
- Rectangle selection in NDC space.
- CPU ray-casting against
PickInfo Structure
Result of any pick operation:
struct PickInfo
{
MeshAsset *mesh = nullptr; // Source mesh asset
LoadedGLTF *scene = nullptr; // Source glTF scene
Node *node = nullptr; // glTF node that owns this surface
RenderObject::OwnerType ownerType; // StaticGLTF, GLTFInstance, MeshInstance
std::string ownerName; // Logical name (e.g., "player", "cube01")
WorldVec3 worldPos; // Hit position in world space (double-precision)
glm::mat4 worldTransform; // Object's world transform
uint32_t indexCount; // Index count of picked surface
uint32_t firstIndex; // First index of picked surface
uint32_t surfaceIndex; // Surface index within mesh
bool valid = false; // True if pick hit something
};
Picking Modes
1. CPU Ray Picking (Default)
Uses camera ray against object bounds with BVH acceleration:
- Sphere bounds: Quick bounding sphere test.
- Box bounds: Ray-box intersection in local space.
- Capsule bounds: Combined cylinder + sphere caps intersection.
- Mesh bounds: Full BVH traversal for triangle-level precision.
Advantages:
- Immediate results (same frame).
- No GPU readback latency.
- Precise triangle-level hits with mesh BVH.
2. ID-Buffer Picking
Renders object IDs to a GPU buffer and reads back single pixel:
- Each
RenderObjecthas a uniqueobjectIDassigned during draw. - Geometry pass writes IDs to an R32_UINT attachment.
PickReadbackrender pass copies single pixel to CPU-readable buffer.- Result resolved on next frame after GPU fence wait.
Advantages:
- Pixel-perfect accuracy (no false positives from bounds).
- Handles complex overlapping geometry.
- Works with any object shape.
Enable via:
picking_system.set_use_id_buffer_picking(true);
PickingSystem API
Initialization:
void init(EngineContext *context);
void cleanup();
Frame Lifecycle:
void begin_frame(); // Resolve pending async picks after fence wait
void process_event(const SDL_Event &event, bool ui_want_capture_mouse);
void update_hover(); // Update hover pick each frame
Results Access:
const PickInfo& last_pick() const; // Last click selection
const PickInfo& hover_pick() const; // Current hover (under cursor)
const std::vector<PickInfo>& drag_selection() const; // Multi-select results
uint32_t last_pick_object_id() const; // Raw object ID of last pick
Configuration:
bool use_id_buffer_picking() const;
void set_use_id_buffer_picking(bool enabled);
bool debug_draw_bvh() const;
void set_debug_draw_bvh(bool enabled);
Utilities:
// Clear picks for removed objects (call when deleting instances)
void clear_owner_picks(RenderObject::OwnerType owner_type, const std::string &owner_name);
// Mutable access for engine integration
PickInfo* mutable_last_pick();
PickInfo* mutable_hover_pick();
RenderGraph Integration:
// Called during RenderGraph build when ID-buffer picking is enabled
void register_id_buffer_readback(RenderGraph &graph,
RGImageHandle id_buffer,
VkExtent2D draw_extent,
VkExtent2D swapchain_extent);
SceneManager Picking API
Lower-level picking functions for custom use:
// Single-object ray pick
bool pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, WorldVec3 &outWorldPos);
// Resolve ID from ID-buffer back to RenderObject
bool resolveObjectID(uint32_t id, RenderObject &outObject) const;
// Rectangle selection (drag-box)
void selectRect(const glm::vec2 &p0, const glm::vec2 &p1,
std::vector<RenderObject> &outObjects) const;
Usage Examples
Basic Click Selection:
void Game::handle_selection(PickingSystem& picking)
{
const PickingSystem::PickInfo& pick = picking.last_pick();
if (pick.valid)
{
fmt::println("Selected: {} at ({}, {}, {})",
pick.ownerName,
pick.worldPos.x, pick.worldPos.y, pick.worldPos.z);
// Handle different owner types
switch (pick.ownerType)
{
case RenderObject::OwnerType::GLTFInstance:
select_actor(pick.ownerName);
break;
case RenderObject::OwnerType::MeshInstance:
select_prop(pick.ownerName);
break;
default:
break;
}
}
}
Hover Tooltips:
void Game::update_ui(PickingSystem& picking)
{
const PickingSystem::PickInfo& hover = picking.hover_pick();
if (hover.valid)
{
show_tooltip(hover.ownerName);
}
}
Multi-Select with Drag Box:
void Game::process_drag_selection(PickingSystem& picking)
{
const auto& selection = picking.drag_selection();
for (const PickingSystem::PickInfo& pick : selection)
{
if (pick.valid)
{
add_to_selection(pick.ownerName);
}
}
}
Custom Ray Pick:
void Game::custom_pick(SceneManager& scene, const glm::vec2& screen_pos)
{
RenderObject hit_object{};
WorldVec3 hit_pos{};
if (scene.pick(screen_pos, hit_object, hit_pos))
{
// hit_object contains the picked render object
// hit_pos is the precise world-space hit position
spawn_effect_at(hit_pos);
}
}
Bounds Types
Objects can use different bounds types for picking:
enum class BoundsType : uint8_t
{
None = 0, // Not pickable
Box = 1, // AABB (default for most objects)
Sphere = 2, // Bounding sphere
Capsule = 3, // Cylinder + hemisphere caps
Mesh = 4, // Full BVH mesh intersection
};
Set bounds type when adding instances:
scene->addMeshInstance("capsule_enemy", mesh, transform, BoundsType::Capsule);
Mesh BVH Picking
For precise triangle-level picking on complex meshes:
- Mesh assets automatically build a BVH during loading.
BoundsType::Meshtriggers BVH traversal instead of simple bounds test.- Returns exact triangle hit position, not just bounding box intersection.
PickInfo::firstIndexandindexCountare refined to the exact triangle.
Debug BVH visualization:
picking.set_debug_draw_bvh(true); // Shows BVH nodes in debug overlay
Coordinate Space Handling
The picking system handles multiple coordinate transformations:
- Window pixels (SDL event coordinates, top-left origin)
- Swapchain pixels (scaled for HiDPI displays)
- Logical render pixels (internal render resolution)
- NDC (normalized device coordinates, -1 to 1)
- World space (double-precision
WorldVec3)
The window_to_swapchain_pixels() helper handles HiDPI scaling. Letterboxing is accounted for when render resolution differs from swapchain size.
Frame Flow
begin_frame()— Resolve pending async ID-buffer picks from previous frame.process_event()— Handle mouse events (click start/end, motion).update_hover()— CPU ray-cast for current hover under cursor.- RenderGraph build — If ID-buffer picking enabled, register readback pass.
- Next frame — Async pick result becomes available.
Integration with ImGui
The picking system respects ImGui's input capture:
// In event loop
bool ui_capture = imgui_system.want_capture_mouse();
picking_system.process_event(event, ui_capture);
When ui_want_capture_mouse is true:
- Click events are ignored (no picks started).
- Mouse motion still updates cursor position for future picks.
- Hover picking is not affected.
Tips
- Use CPU ray picking (
set_use_id_buffer_picking(false)) for immediate feedback. - Use ID-buffer picking for pixel-perfect selection in dense scenes.
- Call
clear_owner_picks()when removing instances to avoid stale picks. - For mesh BVH to work, ensure the mesh was loaded with BVH generation enabled.
- The drag selection threshold is 3 pixels — smaller motions are treated as clicks.