跳到主要内容

视口裁剪瓦片与空间索引

对象过千以后,性能不在 fillRect,而在 每帧对不可见物体做矩阵和命中。先裁视口,再考虑瓦片缓存,最后才上四叉树。

CameraviewToWorld相机

1. 视口世界 AABB

export interface Aabb {
minX: number;
minY: number;
maxX: number;
maxY: number;
}

export function aabbHits(a: Aabb, b: Aabb): boolean {
return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY;
}
export function viewWorldAabb(
camera: Camera,
cssWidth: number,
cssHeight: number,
padCss = 0,
): Aabb {
const a = viewToWorld(camera, -padCss, -padCss);
const b = viewToWorld(camera, cssWidth + padCss, cssHeight + padCss);
return {
minX: Math.min(a.x, b.x),
minY: Math.min(a.y, b.y),
maxX: Math.max(a.x, b.x),
maxY: Math.max(a.y, b.y),
};
}

export function isVisible(bounds: Aabb, viewport: Aabb): boolean {
return aabbHits(bounds, viewport);
}

半透明阴影要把 bounds 外扩 blur。旋转用 OBB 的 AABB。宁可多画一个,不要裁掉投影。

线性扫描 2k 个 AABB 在 JS 里通常 <1ms。先测再上树。

2. 均匀网格(默认索引)

export interface SpatialItem {
id: string;
bounds: Aabb;
}

export function gridInsert(
cell: number,
items: readonly SpatialItem[],
): Map<string, string[]> {
const buckets = new Map<string, string[]>();
for (const item of items) {
const x0 = Math.floor(item.bounds.minX / cell);
const y0 = Math.floor(item.bounds.minY / cell);
const x1 = Math.floor(item.bounds.maxX / cell);
const y1 = Math.floor(item.bounds.maxY / cell);
for (let x = x0; x <= x1; x += 1) {
for (let y = y0; y <= y1; y += 1) {
const key = `${x}:${y}`;
const list = buckets.get(key);
if (list) list.push(item.id);
else buckets.set(key, [item.id]);
}
}
}
return buckets;
}

export function gridQuery(
buckets: Map<string, string[]>,
cell: number,
region: Aabb,
): Set<string> {
const ids = new Set<string>();
const x0 = Math.floor(region.minX / cell);
const y0 = Math.floor(region.minY / cell);
const x1 = Math.floor(region.maxX / cell);
const y1 = Math.floor(region.maxY / cell);
for (let x = x0; x <= x1; x += 1) {
for (let y = y0; y <= y1; y += 1) {
for (const id of buckets.get(`${x}:${y}`) ?? []) ids.add(id);
}
}
return ids;
}

cell 取场景中位物体尺寸的 2–4 倍。物体极大(整张底图)会进很多格:这种东西单独当「层」,不进索引。

四叉树只在 查询远多于重建(静态地图)时有意义。编辑器每帧都有节点移动,重建网格往往更便宜。

3. 瓦片缓存

地图、PDF、大图:世界切成 tileSize(如 512 世界单位)的块,每块一张 ImageBitmap

export interface TileKey {
z: number;
x: number;
y: number;
}

export function tilesForViewport(
viewport: Aabb,
tileSize: number,
): TileKey[] {
const x0 = Math.floor(viewport.minX / tileSize);
const y0 = Math.floor(viewport.minY / tileSize);
const x1 = Math.floor(viewport.maxX / tileSize);
const y1 = Math.floor(viewport.maxY / tileSize);
const keys: TileKey[] = [];
for (let x = x0; x <= x1; x += 1) {
for (let y = y0; y <= y1; y += 1) {
keys.push({ z: 0, x, y });
}
}
return keys;
}

绘制:可见瓦片 drawImage,未就绪画占位。解码在 Worker + createImageBitmap。LRU 按 key 淘汰,bitmap.close()

缩放层级 z:和地图一样,不要用一张 32k 图硬缩小。Canvas 对超大 drawImage 源仍要上传,移动端会 OOM。

矢量层(标注)不要烘进瓦片,除非冻结。否则每改一个框要废一块缓存。

4. 失败形态

症状原因
平移边缘物体闪没pad 不够,阴影/线宽被裁
索引越用越慢动态物体每帧全量重建却用深四叉树
内存只涨瓦片 bitmap 没 close
缩放到 8x 糊只有 z0 瓦片
编辑一个标注全图重解码矢量烘进了栅格瓦片

权威资料

核对日期:2026-08-26