首页 资讯 正文

当前简讯:图形学中拾取的几种思路

2023-06-14 17:51:46   来源:哔哩哔哩    

拾取是图形交互的核心操作,连接了渲染的画面和用户的响应。有好几种不同的拾取方式,每种方式都有其优劣势,所以基本上都需要实现来应对不同的需求。简单来说,拾取物体分为几种思路:

2D情况下,直接判断鼠标坐标是否在某个元素内即可,大多数情况下都可以用矩形和圆来解决。在IMGUI中就是采用这种方式。由于GUI的元素相对规则,所以这种方法是比较通用的。


(资料图片)

3D情况下,有两种思路:

几何思路:通过连接摄像机和屏幕坐标生成射线,然后与场景中的物体做相交判断。

渲染思路:对渲染的每个物体给予编号,将编号转换成颜色,然后通过拾取framebuffer的颜色来判断拾取到了哪个物体。

这里重点讨论一下3D的情况,在组件架构下,如果要走几何思路,有两个问题:

需要给每一个需要检测的物体配置碰撞体,很多时候这件事情是没有必要的,特别是编辑器里面,场景对象有些是不需要碰撞器,强行加入反而会很奇怪。

射线检测精度的问题,本质上这件事的核心在于CPU的运算,对于高效的检测算法可能还要更新搜索结构,但即使如此,精度问题依旧存在,特别当物体特别靠近摄像机的时候,精度反而更加差。

但如果如果场景中本来就有大量碰撞体,那么用射线检测的方式就会更加自然。

如果走渲染思路,很明显的问题就是会增加一个Pass以及GPU和CPU的强行同步,好处当然是对原有的场景不做侵入式的影响。在编辑器当中很自然就可以拾取物体,不需要做额外的操作。如果考虑优化,实际上可以只根据鼠标事件选择性的增加pass,而没必要每帧都做额外的渲染。并且可以将渲染放到场景剔除之后进行,而不是重复性地进行整个管线。

射线检测

射线检测一般会基于物理引擎来做:

只要设置对了碰撞体的位置,物理引擎很容易就可以进行检测,在这个基础上可以进一步将鼠标操作封装成事件脚本:

https://oasisengine.cn/0.6/examples#input-pointer

oasisengine.cn/0.6/examples#input-pointer

颜色拾取(Framebuffer Picker)

Framebuffer Picker(这个词我之前搜索了半天都没找到太多资料)原理更加简单,只依赖于渲染API,但是我在实现的时候遇到一系列坑,这也是激发我写这篇文章总结的原因。特别总结一下。

首先我们需要颜色相互转换的函数:

math::Float3 ColorMaterial::id2Color(uint32_t id) {

if (id >= 0xffffff) {

std::cout<< "Framebuffer Picker encounter primitive's id greater than " + std::to_string(0xffffff) <<std::endl;

return math::Float3(0, 0, 0);

}

return math::Float3((id & 0xff) / 255.0, ((id & 0xff00) >> 8) / 255.0, ((id & 0xff0000) >> 16) / 255.0);

}

uint32_t ColorMaterial::color2Id(const std::array<uint8_t, 4>& color) {

return color[0] | (color[1] << 8) | (color[2] << 16);

}

8-bits刚好可以表示0-255,然后用RGB三个颜色一共3个8-bits就可以表示上万种物体。

接下来要构建一个framebuffer,其实只需要创建一个texture并且绑定到MTLRenderPassDescriptor即可,最后我们需要个函数来读取texture当中的数据:

std::array<uint8_t, 4> ColorRenderPass::readColorFromRenderTarget(Camera* camera) {

const auto& screenPoint = _pickPos;

auto window =  camera->engine()->canvas()->handle();

int clientWidth, clientHeight;

glfwGetWindowSize(window, &clientWidth, &clientHeight);

int canvasWidth, canvasHeight;

glfwGetFramebufferSize(window, &canvasWidth, &canvasHeight);

const auto px = (screenPoint.x / clientWidth) * canvasWidth;

const auto py = (screenPoint.y / clientHeight) * canvasHeight;

const auto viewport = camera->viewport();

const auto viewWidth = (viewport.z - viewport.x) * canvasWidth;

const auto viewHeight = (viewport.w - viewport.y) * canvasHeight;

const auto nx = (px - viewport.x) / viewWidth;

const auto ny = (py - viewport.y) / viewHeight;

auto texture = renderTarget.colorAttachments[0].texture;

const auto left = std::floor(nx * (texture.width - 1));

const auto bottom = std::floor((1 - ny) * (texture.height - 1));

std::array<uint8_t, 4> pixel;

[renderTarget.colorAttachments[0].texture getBytes:pixel.data()

bytesPerRow:sizeof(uint8_t)*4

fromRegion:MTLRegionMake2D(left, canvasHeight - bottom, 1, 1)

mipmapLevel:0];

return pixel;

}

这一块一开始死活弄不对,主要就是缺少了同步操作,首先在MacOS上,MTLTexture的StorageMode要么是Managed要么是Private,在iOS上可以用Shared使得CPU和GPU可以共享内存,但MacOS上,GPU和CPU的内存是分开的,所以要进行显式同步:

void ColorRenderPass::postRender(Camera* camera, const RenderQueue& opaqueQueue,

const RenderQueue& alphaTestQueue, const RenderQueue& transparentQueue) {

auto blit = [camera->engine()->_hardwareRenderer.commandBuffer blitCommandEncoder];

[blit synchronizeResource:renderTarget.colorAttachments[0].texture];

[blit endEncoding];

}

同步操作需要创建一个blitCommandEncoder,但即使是这里还是没有完成同步,因为此时还只是为命令队列添加操作,真正的同步要commit之后,但commit之后立刻返回如果此时获取数据可能还是错误的,所以要强制CPU和GPU同步:

void MetalRenderer::end() {

[commandBuffer presentDrawable:drawable];

[commandBuffer commit];

[commandBuffer waitUntilCompleted];

}

然后再去调用readColorFromRenderTarget,才能够正确获取颜色并且转换成对应ID。

类似的同步操作其实是现代图形API比较困难的部分,因为这些API把同步的控制权交给了开发者,但只是抓帧的话,抓到的也都是一帧结束的时候,所以渲染结果看上去都是没问题的,很难找到这里面的问题。

https://oasisengine.cn/0.6/examples#framebuffer-picker

oasisengine.cn/0.6/examples#framebuffer-picker

总结

做到这一步基本上打通了引擎Runtime和编辑器开发的桥梁,通过物体的拾取就可以挂载其他辅助的组件,例如Gizmos,进而编辑场景。或者通过脚本来调用raycast对场景的物体进行射线检测或者动画拾取。希望你看了这篇文章之后,不会言拾取,必称射线检测,不同的方法有不同的适用范围,可以按需选择。

关键词:

为你推荐