超大点云处理策略(实战版)

1 阅读9分钟

超大点云处理策略(实战版):空间分片 + 分片内 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 主流程(你们现在的真实逻辑)

  1. 打开任务
  2. 前端确定当前分片 infoShardingId = 0
  3. 只请求当前分片点云:
GET oss.com/0.pcd
  1. Worker 后台预缓存其他分片(比如 1.pcd, 2.pcd...
  2. 用户切换区域时:
  • 优先从 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,原因包括:

  1. 你从未触发切区域
    前端只加载当前 shard,不切换就不会发其他请求。
  2. 预加载命中缓存
    如果 Worker 预加载成功,后续切区域时可能是从内存取,不会走网络(或者请求被 Service Worker/HTTP cache 命中,显示不明显)。
  3. 预加载逻辑没开启/被节流/失败
    例如只在空闲时预取,或因网络策略/并发限制没拉到。

所以:仅凭 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
  • 要么:

    • 当前视角始终在 shard 0
    • 其他 shard 要么没加载、要么命中 Worker 缓存

但无论如何:

  • range.json 一定已经被读取过
  • 否则系统根本不知道 0.pcd 的存在

range.json 决定“点云世界的地图”,
.pcd 只是地图上的某一块地形数据。