
鼠标拾取是引擎场景编辑器的一个非常基础的功能,通过点击屏幕像素选择场景中的物体,能够使用户更加方便地选择物体、编辑场景。利用空闲时间我将IlumEngine场景编辑器的鼠标拾取功能做了一个优化,从原来的基于Ray Casting方法到现在所使用的G-Buffer回读的方法,两种方法各有优劣,下面详细介绍这两种鼠标拾取的方法。
1. 基于Ray Casting的鼠标拾取方法
基于Ray Casting的鼠标拾取是一种几何方法,其基本原理如下:
- 由鼠标点击的屏幕像素坐标,生成一条从摄像机发射的射线
- 对场景作求交计算(与Ray Tracing中的相交检测相同)
- 寻找与光线相交的最近包围盒,其对应的物体即为鼠标将选中的物体
已知我们已从窗口/UI系统中得到鼠标点击的像素坐标click_pos
,首先将其转化为屏幕空间坐标:
1 2
| float x = (click_pos.x / scene_view_size.x) * 2.f - 1.f; float y = -((click_pos.y / scene_view_size.y) * 2.f - 1.f);
|
我们希望利用拾取点的屏幕空间坐标从相机发射一条射线,一种思路是计算拾取点的远近平面投影坐标,然后将它们连起来即可:
1 2 3 4 5 6 7 8 9 10
| glm::mat4 inv = glm::inverse(main_camera.view_projection);
glm::vec4 near_point = inv * glm::vec4(x, y, 0.f, 1.f); near_point /= near_point.w; glm::vec4 far_point = inv * glm::vec4(x, y, 1.f, 1.f); far_point /= far_point.w;
geometry::Ray ray; ray.origin = main_camera.position; ray.direction = glm::normalize(glm::vec3(far_point - near_point));
|
最后,用射线做与包围盒的求交,求交的计算也可用BVH、KD-Tree等加速结构进行加速,这里为了快速实现只是简单遍历并对每个包围盒进行求交:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| float distance = std::numeric_limits<float>::infinity(); const auto group = Scene::instance()->getRegistry().group<>(entt::get<cmpt::MeshRenderer, cmpt::Transform>); group.each([&](const entt::entity &entity, const cmpt::MeshRenderer &mesh_renderer, const cmpt::Transform &transform) { if (!Renderer::instance()->getResourceCache().hasModel(mesh_renderer.model)) { return; } auto &model = Renderer::instance()-getResourceCache().loadModel(mesh_renderer.model); float hit_distance = ray.hit(model.get().bounding_box.transform(transform.world_transform)); if (distance > hit_distance) { distance = hit_distance; Editor::instance()->select(Entity(entity)); } }); }
|
具体求交的计算后续光线追踪模块的开发将会提到,这里不作具体阐述。
2. 基于G-Buffer回读的鼠标拾取方法
基于G-Buffer的鼠标拾取是一种图像方法,在几何阶段生成G-Buffers时我们顺带生成一张带有场景物体实体ID的G-Buffer,格式为VK_FORMAT_R32_UINT
。在得到鼠标响应时,将该G-Buffer的数据回读到CPU中,利用像素坐标查找相应的实体ID,得到拾取到的对象。完整过程的源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ImageReference entity_id_buffer = Renderer::instance()->getRenderGraph()->getAttachment("debug - entity");
CommandBuffer cmd_buffer; cmd_buffer.begin(); Buffer staging_buffer(static_cast<VkDeviceSize>(entity_id_buffer.get().getWidth() * entity_id_buffer.get().getHeight()) * sizeof(uint32_t), VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_TO_CPU); cmd_buffer.transferLayout(entity_id_buffer, VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_USAGE_TRANSFER_SRC_BIT); cmd_buffer.copyImageToBuffer(ImageInfo{entity_id_buffer, VK_IMAGE_USAGE_TRANSFER_SRC_BIT, 0, 0}, BufferInfo{staging_buffer, 0}); cmd_buffer.transferLayout(entity_id_buffer, VK_IMAGE_USAGE_TRANSFER_SRC_BIT, VK_IMAGE_USAGE_SAMPLED_BIT); cmd_buffer.end(); cmd_buffer.submitIdle(); std::vector<uint32_t> image_data(entity_id_buffer.get().getWidth() * entity_id_buffer.get().getHeight()); std::memcpy(image_data.data(), staging_buffer.map(), image_data.size() * sizeof(uint32_t));
click_pos.x = glm::clamp(click_pos.x, 0.f, static_cast<float>(entity_id_buffer.get().getWidth())); click_pos.y = glm::clamp(click_pos.y, 0.f, static_cast<float>(entity_id_buffer.get().getHeight()));
auto entity = Entity(static_cast<entt::entity>(image_data[static_cast<uint32_t>(click_pos.y) * entity_id_buffer.get().getWidth() + static_cast<uint32_t>(click_pos.x)])); Editor::instance()->select(entity);
staging_buffer.unmap();
|
由于G-Buffer的内存访问方式均为GPU_only
的,我们需要使用一块GPU_to_CPU
的Buffer进行暂存,最后Map到CPU内存中。
3. 比较与选择
- Ray Casting方法
- 优点
- 缺点
- 不够精确,由于是射线与包围盒求交,拾取的实际上是物体对应的包围盒而不是物体本身,有时候会带来误差,在场景复杂时效果不好
- 性能受场景规模影响较大,而使用加速结构进行求交加速实际上也增加了集成复杂度(需要引擎具有光追或物理模块支持)
- G-Buffer方法
- 优点
- 精准,由于是直接把实体ID贴到纹理上,因此能够做到像素级的拾取
- 缺点
- 需要一张G-Buffer,增加了带宽开销
- 需要回读GPU数据,不过只有在鼠标点击时才会触发,影响并不大
- 需要渲染管线支持,需要配合整个渲染系统进行设计
在开发前期,Render Graph还不够完善,渲染管线扩展能力一般,为了简便,我先直接用Ray Casting的方法给IlumEngine加上一个基本能用的拾取方法,后来为了拾取精度的需要,将拾取算法改为了基于G-Buffer方法。
最终实现效果如下: