Pick
GPU Pick
为场景中的每个对象分配一个唯一的颜色,将所有对象渲染到一个FBO中,拾取时基于鼠标指针在屏幕上的位置索引FBO中的颜色值。如果该颜色与某个对象匹配,则该对象被选中。以下是绘制到SelectionBuffer和从SelectionBuffer读取的详细步骤:
Drawing to Selection Buffer
首先需要一个单的pass将物体分配的颜色写入单独的FBO,这里需要处理几件事:
| 步骤 |
操作 |
详细说明 |
| 1 |
创建帧缓冲区 |
创建一个与渲染屏幕分辨率相同的帧缓冲区 |
| 2 |
分配唯一索引 |
为每个对象分配一个唯一的索引值(通常是从 1 开始的整数) |
| 3 |
索引转颜色 |
将索引值转换为 32 位颜色值 (r, g, b, a) |
| 4 |
渲染 |
用索引转换得到的颜色将对象绘制到帧缓冲区 |
Reading from Selection Buffer
| 步骤 |
操作 |
详细说明 |
| 1 |
获取鼠标位置 |
从屏幕获取鼠标坐标 (x, y) |
| 2 |
读取像素 |
使用 glReadPixels() 读取鼠标位置的颜色值 |
| 3 |
颜色转索引 |
将帧缓冲区中的颜色值转换回对象索引值 |
| 4 |
命中检测 |
如果索引值非零,表示选中了对象 |
| 5 |
未命中检测 |
如果索引值为零,表示未选中任何对象 |
IndexToColor & ColorToIndex
1 2 3 4 5 6 7 8
| int index = ...; int r = index & 0xFF; int g = index >> 8 & 0xFF; int b = index >> 16 & 0xFF;
Color color = [r/255, g/255, b/255, 1];
|
1 2 3
| Color color = ...; int index = color[0] + (color[1] << 8) + (color[1] << 16);
|
glReadPixels
在opengl中读取FBO纹理附件像素值通过glReadPixels函数:
1 2 3 4 5 6 7
| void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid * data);
|
其中xy表示像素位置, 即屏幕空间鼠标指针的位置,width和height表示要读取的像素区域大小
pipeline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void render(objects, camera) { gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); objects.forEach((obj, index) => { const color = SelectionBuffer.encodeIndex(index + 1); obj.renderWithColor(color, camera); }); gl.bindFramebuffer(gl.FRAMEBUFFER, 0); } read(x, y) { const webglY = this.height - y; gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); const pixels = new Uint8Array(4); gl.readPixels(x, webglY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); gl.bindFramebuffer(gl.FRAMEBUFFER, null); return SelectionBuffer.decodeColor(pixels); }
|
CPU Pick
Construct ViewRay
射线由起点和方向向量定义。对于透视相机,起点通常是相机位置,方向是从相机所在位置指向屏幕点击位置的向量。首先需要将屏幕空间坐标转换为世界空间中的坐标,然后减去相机位置得到方向向量。
坐标转换流程
屏幕坐标(通常原点在左上角) → 归一化设备坐标(NDC,范围[-1, 1]) → 视图空间 → 世界空间
screenSpace->NDC
假设鼠标点击坐标为 (mouseX, mouseY),屏幕宽高为 viewportWidth、viewportHeight。注意Y轴方向:大多数窗口系统原点在左上角,Y向下为正,而NDC中Y向上为正,所以需要翻转Y。
1 2 3 4 5 6 7 8
|
float ndcX = (2.0f * mouseX) / viewportWidth - 1.0f; float ndcY = 1.0f - (2.0f * mouseY) / viewportHeight; float ndcZ = -1.0f; float ndcW = 1.0f;
Vector4 ndcPos(ndcX, ndcY, ndcZ, ndcW);
|
NDC->worldSpace
将viewProjectionMatrix的逆矩阵乘以ndcPos,得到鼠标指针在近裁剪平面上世界空间的位置:
1 2 3
| glm::mat4 invViewProj = glm::inverse(projectionMatrix * viewMatrix); glm::vec4 rayWorld = invViewProj * ndcPos; rayWorld /= rayWorld.w;
|
Construct ViewRay
1 2
| glm::vec3 rayOrigin = cameraPosition; glm::vec3 rayDir = glm::normalize(rayWorld.xyz - rayOrigin.xyz);
|