圖形學(xué)中拾取的幾種思路
拾取是圖形交互的核心操作,連接了渲染的畫面和用戶的響應(yīng)。有好幾種不同的拾取方式,每種方式都有其優(yōu)劣勢,所以基本上都需要實現(xiàn)來應(yīng)對不同的需求。簡單來說,拾取物體分為幾種思路:
2D情況下,直接判斷鼠標(biāo)坐標(biāo)是否在某個元素內(nèi)即可,大多數(shù)情況下都可以用矩形和圓來解決。在IMGUI中就是采用這種方式。由于GUI的元素相對規(guī)則,所以這種方法是比較通用的。
3D情況下,有兩種思路:
幾何思路:通過連接攝像機和屏幕坐標(biāo)生成射線,然后與場景中的物體做相交判斷。
渲染思路:對渲染的每個物體給予編號,將編號轉(zhuǎn)換成顏色,然后通過拾取framebuffer的顏色來判斷拾取到了哪個物體。
這里重點討論一下3D的情況,在組件架構(gòu)下,如果要走幾何思路,有兩個問題:
需要給每一個需要檢測的物體配置碰撞體,很多時候這件事情是沒有必要的,特別是編輯器里面,場景對象有些是不需要碰撞器,強行加入反而會很奇怪。
射線檢測精度的問題,本質(zhì)上這件事的核心在于CPU的運算,對于高效的檢測算法可能還要更新搜索結(jié)構(gòu),但即使如此,精度問題依舊存在,特別當(dāng)物體特別靠近攝像機的時候,精度反而更加差。
但如果如果場景中本來就有大量碰撞體,那么用射線檢測的方式就會更加自然。
如果走渲染思路,很明顯的問題就是會增加一個Pass以及GPU和CPU的強行同步,好處當(dāng)然是對原有的場景不做侵入式的影響。在編輯器當(dāng)中很自然就可以拾取物體,不需要做額外的操作。如果考慮優(yōu)化,實際上可以只根據(jù)鼠標(biāo)事件選擇性的增加pass,而沒必要每幀都做額外的渲染。并且可以將渲染放到場景剔除之后進行,而不是重復(fù)性地進行整個管線。
射線檢測
射線檢測一般會基于物理引擎來做:
只要設(shè)置對了碰撞體的位置,物理引擎很容易就可以進行檢測,在這個基礎(chǔ)上可以進一步將鼠標(biāo)操作封裝成事件腳本:
https://oasisengine.cn/0.6/examples#input-pointer
oasisengine.cn/0.6/examples#input-pointer
顏色拾取(Framebuffer Picker)
Framebuffer Picker(這個詞我之前搜索了半天都沒找到太多資料)原理更加簡單,只依賴于渲染API,但是我在實現(xiàn)的時候遇到一系列坑,這也是激發(fā)我寫這篇文章總結(jié)的原因。特別總結(jié)一下。
首先我們需要顏色相互轉(zhuǎn)換的函數(shù):
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就可以表示上萬種物體。
接下來要構(gòu)建一個framebuffer,其實只需要創(chuàng)建一個texture并且綁定到MTLRenderPassDescriptor即可,最后我們需要個函數(shù)來讀取texture當(dāng)中的數(shù)據(jù):
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可以共享內(nèi)存,但MacOS上,GPU和CPU的內(nèi)存是分開的,所以要進行顯式同步:
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];
}
同步操作需要創(chuàng)建一個blitCommandEncoder,但即使是這里還是沒有完成同步,因為此時還只是為命令隊列添加操作,真正的同步要commit之后,但commit之后立刻返回如果此時獲取數(shù)據(jù)可能還是錯誤的,所以要強制CPU和GPU同步:
void MetalRenderer::end() {
? ? [commandBuffer presentDrawable:drawable];
? ? [commandBuffer commit];
? ? [commandBuffer waitUntilCompleted];
}
然后再去調(diào)用readColorFromRenderTarget,才能夠正確獲取顏色并且轉(zhuǎn)換成對應(yīng)ID。
類似的同步操作其實是現(xiàn)代圖形API比較困難的部分,因為這些API把同步的控制權(quán)交給了開發(fā)者,但只是抓幀的話,抓到的也都是一幀結(jié)束的時候,所以渲染結(jié)果看上去都是沒問題的,很難找到這里面的問題。
https://oasisengine.cn/0.6/examples#framebuffer-picker
oasisengine.cn/0.6/examples#framebuffer-picker
總結(jié)
做到這一步基本上打通了引擎Runtime和編輯器開發(fā)的橋梁,通過物體的拾取就可以掛載其他輔助的組件,例如Gizmos,進而編輯場景?;蛘咄ㄟ^腳本來調(diào)用raycast對場景的物體進行射線檢測或者動畫拾取。希望你看了這篇文章之后,不會言拾取,必稱射線檢測,不同的方法有不同的適用范圍,可以按需選擇。