跳到主要内容

大量标记与散点

万级点的瓶颈是 draw call,不是 VS 里多几个 MAD。场景图(id、世界坐标、业务字段)留在 CPU;GPU 只吃一条四边形几何 + 一条 instance buffer。不要每点 drawArrays,也不要从颜色反推「这是哪只股票」。

1. 场景怎么切

CPU 文档:id / worldXY / 颜色 / 选中态
↓ 视口裁剪、像素去重
instance buffer(投影)
↓ drawArraysInstanced 一次
drawing buffer

相机是 世界单位 + viewProj。点的真值在世界;CSS 盒只决定可见范围。轴/网格另走低频 pass 或 DOM,不要和点抢同一条 instance。

export type ScatterPoint = {
id: number;
x: number;
y: number;
r: number;
g: number;
b: number;
};

export type Camera2D = {
x: number;
y: number;
scale: number;
};

export function packVisible(
camera: Camera2D,
cssW: number,
cssH: number,
points: readonly ScatterPoint[],
): { packed: Float32Array; ids: number[] } {
const maxX = camera.x + cssW * camera.scale;
const maxY = camera.y + cssH * camera.scale;
const inv = 1 / camera.scale;
const seen = new Set<number>();
const tmp: number[] = [];
const ids: number[] = [];
for (const p of points) {
if (p.x < camera.x || p.x > maxX || p.y < camera.y || p.y > maxY) continue;
const key = (Math.round((p.x - camera.x) * inv) << 16) ^ Math.round((p.y - camera.y) * inv);
if (seen.has(key)) continue;
seen.add(key);
tmp.push(p.x, p.y, p.r, p.g, p.b);
ids.push(p.id);
}
return { packed: new Float32Array(tmp), ids };
}

ids[i] 与 instance 第 i 行对齐,拾取只查这份可见投影。像素去重是屏幕密度,不是统计 bin;热力图要另做 grid count。

适用:监控散点、行情点、地图 POI,量级从几千到几十万。
不适用:每点不同 mesh / 不同 program(那是多条合批,见 实例化);< 2k 且只要轴+tooltip,先看 Canvas 散点

2. 哪层必须 CPU,哪层进 GPU

归属
点列表、id、业务字段、选中集CPU 文档
视口裁剪、像素 LOD、空间索引CPU,相机变了才重打
四边形几何、instance buffer、viewProjGPU 投影
tooltip / 图例DOM overlay,见 Overlay

相机动、点不动:只改 uniform。点增删或裁剪结果变了:bufferSubData 脏区,不要每帧 bufferData 十万行。合批键必须同一 shader + 同一图集,见 合批

拾取默认 CPU:可见 ids + 网格,比距离。点是圆/AABB 时不要开 ID pass。三角标牌互交、半透明叠完才知道点到谁,再走 GPU 拾取 且只读 1px。热路径禁止全屏 readPixels

GPU-driven 剔除 + drawIndirect 是 WebGPU 的活:先 requestAdapter(),失败就留在 WebGL2 实例化。不要为散点默认切 WebGPU。

3. 失败形态

症状原因
一万点 10fps每点一次 drawArrays
hover 错点用 CSS 像素去比世界坐标,或裁剪后 ids 没对齐
缩轴后点变成椭圆半径写进世界单位,没按 view 尺度
拖一个点全体跳world 当 uniform,不是 instance 行
一拾取就卡读了整张 ID 图

权威资料

核对日期:2026-08-26