超大点云处理策略(实战版):空间分片 + 分片内 4D 融合 + Worker 预取缓存
目标:在浏览器里稳定加载超大点云,支持时间轴切帧(4D),同时把网络、内存、GPU 压力控制在可用范围。
你们项目采用的策略可以概括为三件事:
- 空间维度:按区域把点云切成多个分片(shard)
- 时间维度:每个分片文件内包含该区域所有帧(融合 4D 点云),每个点带
frameIndex - 加载策略:只加载当前分片;其他分片在 Worker 里预加载/缓存
1. 空间分片(Sharding):rangeJSON 是“分片索引 + 全局配置”
当点云非常大时,后端会把场景按空间区域切成多个分片:
0.pcd:区域 0 的融合点云(包含所有帧的点)1.pcd:区域 1 的融合点云2.pcd:区域 2 的融合点云- …
这些分片的定义信息由 range.json 给出,也就是你提到的 rangeJSON。
1.1 rangeJSON 的典型结构
rangeJSON 一般同时包含两类字段:
A) 分片条目(数字 key:0/1/2/...)
每个分片至少包含一个 range:
rangeJSON = {
0: { range: [x1, y1, x2, y2], ... }, // 区域 0
1: { range: [x3, y3, x4, y4], ... }, // 区域 1
2: { range: [x5, y5, x6, y6], ... }, // 区域 2
// ...
frames: 50,
pcds_ds_mode: "split"
}
其中 range: [x_min, y_min, x_max, y_max] 表示该分片覆盖的空间范围(你们当前是 2D 范围)。
B) 全局配置(非数字 key)
例如:
frames: 总帧数pcds_ds_mode: 数据集模式(比如 split)- 其他版本/阈值配置(可选)
2. 按需加载分片:只请求当前 shard 的融合点云
2.1 主流程(你们现在的真实逻辑)
- 打开任务
- 前端确定当前分片
infoShardingId = 0 - 只请求当前分片点云:
GET oss.com/0.pcd
- Worker 后台预缓存其他分片(比如
1.pcd,2.pcd...) - 用户切换区域时:
- 优先从 Worker 缓存拿
- 不命中再发网络请求
3. 4D 点云:分片内融合 + 点级 frameIndex
你们方案的关键不是“每帧一个文件”,而是:
每个分片文件本身就是该区域的 4D 融合点云(包含所有帧)
时间信息在点上,而不是在文件上。
每个点都有 frameIndex 属性,切帧只是改显示范围:
filterPcd(frameStart, frameEnd) {
for (每个点) {
visible = (frameIndex 在范围内) ? 1 : 0;
}
}
所以在网络面板里:
- 切帧不会产生新请求
- 只有切区域(换 shard)才可能产生新请求
4. 为什么你只看到 0.pcd?这并不必然等于“只有一个分片”
你提出的判断逻辑非常接近事实,但这里有两个可能性,需要区分:
情况 A:确实只有一个分片
rangeJSON 里只有数字 key 0,没有 1/2/...:
{
0: { range: [...] },
frames: 50,
pcds_ds_mode: "split"
}
这种情况下:
- 切区域也不存在
- 永远只会请求
0.pcd
✅ 你看到的现象完全吻合。
情况 B:有多个分片,但策略导致你“暂时看不到”其他请求
即便 rangeJSON 里有 1/2/...,你也可能只看到 0.pcd,原因包括:
- 你从未触发切区域
前端只加载当前 shard,不切换就不会发其他请求。 - 预加载命中缓存
如果 Worker 预加载成功,后续切区域时可能是从内存取,不会走网络(或者请求被 Service Worker/HTTP cache 命中,显示不明显)。 - 预加载逻辑没开启/被节流/失败
例如只在空闲时预取,或因网络策略/并发限制没拉到。
所以:仅凭 Network 面板“只看到 0.pcd”是强信号,但建议结合 rangeJSON 的 shard 列表做最终判断。
5. rangeJSON 如何定义与解析(前端推荐写法)
5.1 获取 range.json
你们是从 OSS 拉:
const rangeUrl = baseURL + 'range.json';
const rangeJSON = await fetch(rangeUrl).then(r => r.json());
5.2 解析 shard 列表(严谨版)
关键是:只取数字 key:
const shardIds = Object.keys(rangeJSON)
.map(k => Number(k))
.filter(n => Number.isInteger(n))
.sort((a, b) => a - b);
判断:
shardIds = [0]:单分片shardIds = [0,1,2]:多分片
5.3 根据 range 找当前 shard(典型)
如果你们是按“相机中心/鼠标点/车辆位置”等定位到 (x,y),就能选 shard:
function pickShardIdByXY(x, y, rangeJSON) {
for (const id of shardIds) {
const [xmin, ymin, xmax, ymax] = rangeJSON[id].range;
if (x >= xmin && x <= xmax && y >= ymin && y <= ymax) return id;
}
return 0; // fallback
}
6. 方案总结
你们的超大点云处理方案核心是:
- ✅ 空间分片:把点云按区域切成多个
.pcd文件(0.pcd、1.pcd、2.pcd…) - ✅ 按需加载:只加载当前区域分片,避免一次性加载全量
- ✅ Worker 缓存/预取:后台预加载其他分片,提高切区域体验
- ✅ 4D 融合点云:每个分片内包含所有帧点云,点级
frameIndex标识时间 - ✅ 切帧不请求:通过
frameIndex + visible控制显示范围 - ✅ 切区域才请求:加载新分片文件时才会出现新的
.pcd请求
因此你看到“只有一次 0.pcd 请求但能切帧”,完全符合 分片内 4D 融合的设计。
一定是先读取
range.json,再决定拉取哪个(或哪些)点云文件。
不读range.json,前端根本不知道该拉0.pcd还是1.pcd / 2.pcd。
一、为什么 range.json 必须先读?
从前端架构角度看,range.json 本质上是:
点云数据集的“索引文件 / 元数据入口”
它解决了 4 个前端在“点云拉取前”必须知道的问题:
1️⃣ 这是不是分片数据集?
pcds_ds_mode: "split"
split→ 需要按 shard 拉取- 非 split(或不存在)→ 单一点云文件
2️⃣ 有多少个分片?
0: {...}
1: {...}
2: {...}
不读 range.json,前端根本不知道:
- 有没有
1.pcd 2.pcd是否存在- 甚至是否只有
0.pcd
3️⃣ 每个分片对应什么空间范围?
0: { range: [x1, y1, x2, y2] }
这一步决定了:
- 当前视角 / 当前点位 属于哪个 shard
- 第一次该拉哪个
.pcd
4️⃣ 这是几帧点云?
frames: 50
这直接影响:
- 时间轴长度
- frameIndex 的合法范围
- 是否启用切帧逻辑
二、正确的加载时序(你们现在这套方案)
从架构角度,完整正确顺序是这样的:
1. 打开任务
2. 请求 range.json ← 必须
3. 解析 rangeJSON
- 判断是否 split
- 构建 shard 列表
- 拿到 frames 数
4. 确定当前 shardId
5. 请求 shard 对应的 .pcd(比如 0.pcd)
6. 构建点云 geometry
7. 切帧 / 渲染 / 交互
👉 第 5 步永远不可能在第 2 步之前发生。
三、为什么你只看到 0.pcd,但一定已经读过 range.json?
注意一个容易忽略的点:
你“看到”的是 Network 面板里显眼的大请求,不等于没有其他请求。
常见情况:
-
range.json:- 很小(几 KB)
- 可能被浏览器缓存
- 很容易被你忽略
-
0.pcd:- 体积大
- 下载时间长
- 非常显眼
但逻辑上:
- 没有
range.json - 前端不可能“凭空知道”去请求
0.pcd
四、如果不读 range.json,会发生什么?(反证)
假设前端不先读 range.json:
❌ 情况 1:盲拉 0.pcd
-
如果数据集其实是多分片:
- 用户当前区域在 shard 2
- 你却拉了 shard 0
-
空间完全不匹配
❌ 情况 2:拉全部 .pcd
- 网络、内存直接爆炸
- 完全违背“超大点云”的设计初衷
❌ 情况 3:靠后端接口直接返回 pcd 地址
那这个接口本质上就等价于 range.json,只是换了名字而已。
五、工程化总结(你这套方案的“必然因果链”)
从因果关系而不是实现细节看:
range.json 存在
↓
前端必须先读取它
↓
才能知道:
- 是否分片
- 有多少 shard
- 当前 shard 是哪个
- frames 数是多少
↓
才能决定:
- 拉不拉点云
- 拉哪个 .pcd
- 是否预加载其他 shard
range.json 是点云数据集的 index.html,
.pcd只是静态资源。
没有 index,你连该点哪个文件都不知道。
好,下面我给你一张**「博客可直接用」的精简流程图**,只保留关键决策点和因果关系,非常适合放在技术博客里(读者一眼就懂,不会被细节淹没)。
超大点云前端加载流程图
(空间分片 + 4D 融合点云)
┌──────────────┐
│ 用户打开任务 │
└──────┬───────┘
│
▼
┌────────────────────┐
│ 请求 range.json │
│(点云索引 / 元数据)│
└──────┬─────────────┘
│
▼
┌────────────────────┐
│ 解析 rangeJSON │
│ - 是否分片 split │
│ - shard 列表 │
│ - frames 数 │
└──────┬─────────────┘
│
▼
┌────────────────────────────┐
│ 判断当前空间位置 → shardId │
└──────┬─────────────────────┘
│
▼
┌────────────────────┐
│ 请求 shard.pcd │
│(如 0.pcd) │
└──────┬─────────────┘
│
▼
┌────────────────────┐
│ 构建 4D 点云 │
│(点级 frameIndex) │
└──────┬─────────────┘
│
▼
┌──────────────────────────────────┐
│ 进入交互阶段 │
│ - 切帧:改可见性(不请求) │
│ - 切区域:加载新 shard.pcd │
└──────────────────────────────────┘
图中每一步在“工程上”代表什么?
1️⃣ range.json
-
整个点云系统的入口
-
决定:
- 有没有分片
- 有多少分片
- 每个分片的空间范围
- 总帧数
2️⃣ shardId
-
前端根据:
- 当前视角 / 中心点 / 业务位置
-
判断该加载哪个区域的点云
3️⃣ shard.pcd
-
每个
.pcd是:- 该区域所有帧的融合点云
-
所以:
- 初次加载只请求一次
- 切帧不会产生新请求
4️⃣ 交互阶段(最容易被误解的地方)
- 切帧 ≠ 拉新点云
- 切区域 ≠ 切帧
- 只有切区域,才可能出现新的
.pcd请求
前端并不是“一帧一帧”加载点云,而是先读取
range.json确定空间分片,再按需加载对应区域的融合点云文件。
时间维度被编码在点级frameIndex中,因此切帧只是改变点的可见性,不会触发新的网络请求。
超大点云前端加载时序图(Sharding + 4D 点云)
一、整体时序(主干流程)
┌────────┐
│ 用户打开 │
│ 点云任务 │
└────┬───┘
│
▼
┌────────────────────┐
│ 请求 range.json │ ← 数据集入口 / 索引
│ (OSS / CDN) │
└────┬───────────────┘
│
▼
┌────────────────────┐
│ 解析 rangeJSON │
│ - pcds_ds_mode │
│ - shard 列表 │
│ - frames 数 │
│ - 每个 shard range │
└────┬───────────────┘
│
▼
┌────────────────────────────┐
│ 判断是否 split 分片模式 │
└────┬───────────────┬──────┘
│否 │是
▼ ▼
┌──────────────┐ ┌────────────────────┐
│ 拉取单一 pcd │ │ 计算当前 shardId │
│ (0.pcd) │ │ (根据视角/位置) │
└────┬─────────┘ └────┬───────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ 解析点云数据 │ │ 请求 shard.pcd │
│ 构建 Geometry │ │ (如 0.pcd) │
└────┬───────────────┘ └────┬───────────────┘
│ │
▼ ▼
┌────────────────────────────────────┐
│ 构建 4D 点云(含 frameIndex) │
│ 初始 frame 过滤 / 渲染 │
└────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────┐
│ 进入交互阶段 │
│ - 切帧(不请求) │
│ - 切区域(可能请求新 shard) │
└────────────────────────────────────┘
二、切帧 vs 切区域(这是最容易被误解的点)
1️⃣ 切帧时序(不会有网络请求)
┌──────────┐
│ 用户拖动 │
│ 时间轴 │
└────┬─────┘
│
▼
┌────────────────────────┐
│ 更新 frameStart/End │
│ (state / uniform) │
└────┬───────────────────┘
│
▼
┌────────────────────────┐
│ 点级 frameIndex 过滤 │
│ - visible / shader │
└────┬───────────────────┘
│
▼
┌──────────┐
│ 重新渲染 │
└──────────┘
关键点:
- ❌ 不请求 pcd
- ❌ 不 rebuild geometry
- ✅ 只是改变“哪些点可见”
2️⃣ 切区域时序(唯一会产生新 pcd 请求的场景)
┌──────────┐
│ 用户移动 │
│ 到新区域 │
└────┬─────┘
│
▼
┌────────────────────────────┐
│ 根据 rangeJSON 判断 shard │
│ oldShard → newShard │
└────┬───────────────┬──────┘
│相同 │不同
▼ ▼
┌──────────────┐ ┌────────────────────┐
│ 什么都不做 │ │ 查询 Worker 缓存 │
└──────────────┘ └────┬───────────────┘
│
┌──────────┴──────────┐
│ 命中 │ 未命中
▼ ▼
┌──────────────┐ ┌────────────────┐
│ 直接复用数据 │ │ 请求 new.pcd │
└────┬─────────┘ └────┬───────────┘
│ │
▼ ▼
┌────────────────────────────────────┐
│ 构建新 Geometry / 替换场景节点 │
│ 释放旧 shard(可延迟) │
└────────────────────────────────────┘
三、range.json 在时序中的“真实地位”
从这张时序图你可以清楚看到:
range.json
↓
决定 shard 数量
↓
决定第一次拉哪个 pcd
↓
决定切区域是否要请求新 pcd
↓
决定 frames / 时间轴
👉 它不是“可选配置”,而是整个点云系统的入口文件。
四、你现在这个现象在时序图中的位置
「我只在浏览器看到 0.pcd 的请求」
在这张图里意味着:
-
要么:
- rangeJSON 只有 shard
0
- rangeJSON 只有 shard
-
要么:
- 当前视角始终在 shard
0 - 其他 shard 要么没加载、要么命中 Worker 缓存
- 当前视角始终在 shard
但无论如何:
range.json一定已经被读取过- 否则系统根本不知道
0.pcd的存在
range.json 决定“点云世界的地图”,
.pcd 只是地图上的某一块地形数据。