跳到主要内容

GPU拾取与ID缓冲

GPU 没有 isPointInPath。两条生产路径:CPU 空间索引(先粗后精)和 ID 缓冲(把对象 id 画进 FBO,读 1 像素)。场景图仍在 CPU。拾取只返回 id,不要从颜色反推业务对象。

1. 默认走 CPU

实例少于几千、几何是点/圆/AABB:CPU 网格或按深度倒序扫一遍通常更快,也没有 readPixels 同步。矩阵与 指针到 NDC 同一套:点变到世界,再测局部 AABB / 距离。

适用 CPU:散点、标记、2.5D 卡片、UI 热点。
适用 GPU ID:三角网格互交、半透明排序后「点到的是屏幕上那片」、CPU 测不准的复杂 mesh。

不要一上来就给每个产品开 ID pass。读回是架构决策,见 读回与同步陷阱

2. ID 只读 1px

把 id 编码进 RGBA8(24 bit 够 1600 万对象;0 保留背景):

export function packId(id: number): [number, number, number, number] {
const n = id + 1;
return [n & 255, (n >> 8) & 255, (n >> 16) & 255, 255];
}

export function unpackId(p: Uint8Array): number | undefined {
const n = p[0]! | (p[1]! << 8) | (p[2]! << 16);
return n === 0 ? undefined : n - 1;
}

export function pickAtCss(
gl: WebGL2RenderingContext,
fbo: WebGLFramebuffer,
cssX: number,
cssY: number,
cssW: number,
cssH: number,
): number | undefined {
const x = Math.floor((cssX / cssW) * gl.drawingBufferWidth);
const y = Math.floor((1 - cssY / cssH) * gl.drawingBufferHeight);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.scissor(x, y, 1, 1);
gl.enable(gl.SCISSOR_TEST);
const px = new Uint8Array(4);
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px);
gl.disable(gl.SCISSOR_TEST);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return unpackId(px);
}

要点:

  • 原点左下,Y 必须翻。
  • scissor 成 1×1 再画 ID pass,或单独 1×1 FBO 只画候选。不要读整张图。
  • ID pass 关掉 MSAA / 混合,否则边缘 dither 会解出邻居 id。
  • 与颜色 pass 用同一 viewProj,否则点到的是另一台相机。

WebGPU:对 copyTextureToBuffer 只拷 1×1,再 mapAsync。不要为了「简单」readPixels 式地 stall queue。

3. 同一帧、同一 FBO

默认 framebuffer 在 preserveDrawingBuffer: false 时合成后可空。ID 必须画在 自己的 FBO,读完再解绑。不要画到屏幕再读屏幕。

4. 失败形态

症状原因
永远点到背景没翻 Y,读了左上当左下
边缘点到旁边物体ID pass 开了 MSAA 或 alpha 混合
点一下卡 30ms读了整张 ID 纹理
拖动物体 id 乱跳颜色 pass 和 ID pass 矩阵不一致

权威资料

核对日期:2026-08-26