渲染 · 性能
做单件物体的 3DGS 和做园区级的 3DGS,是两种难度。 这篇记录我们在 1193 万高斯点场景上做的事: 分块流式加载、帧时间该看哪个数、以及一次"单帧 483 毫秒卡顿"是怎么定位并消掉的。
你在网上看到的"某某场景跑到 60fps",如果不带下面这四项, 基本没有参考价值:
业内生产系统通常用 LOD 预算把每帧压在 200 万到 400 万点, 而我们当时这份资产是单层 LOD,每帧接近全量 —— 约 1200 万点。
也就是说,同样的硬件下,按业内预算配置会快得多。 所以下面所有数字都请连带条件一起看。
1193 万高斯点的原始模型体积很大,不可能让用户等整个文件下载完。 做法是把它烘焙成八叉树分块:
lod-meta.json 记录整棵空间树我们这份资产的规模:78 个分块、167MB。 因为按需加载,点开页面几秒内就能看到内容,而不是等 167MB 下完。
这一点比上面那条重要得多,而且很多人搞错了。
不要只看平均帧率。用户感觉到的"卡",来自偶尔出现的慢帧, 而平均帧率会把它们平均掉。
举个真实的例子:平均 30fps 听起来还行,但如果这个平均值是由 "大部分 20ms 的帧 + 偶尔一个 400ms 的帧"组成的,用户就会明显感觉到顿挫。 反过来,如果帧时间稳定在 33ms 左右,虽然帧率不高,观感却是顺的。
所以我们以 p95 / p99 帧时间作为验收标准,而不是平均 fps。
统计帧时间时,如果用"最近 60 帧"这样的帧数窗口, 在低帧率下这个窗口会横跨几十秒 —— 你看到的数字其实是很久以前的, 完全没法用来判断当前状态。
正确的做法是按时间窗口统计(比如"最近 1 秒内的所有帧")。
这是我们在这份资产上遇到的最有价值的一个问题。
场景加载过程中,会间歇性出现单帧长达 483 毫秒的停顿 —— 相当于画面卡住半秒。p99 帧时间被这个彻底拉坏。
我们怀疑是解码的问题,而不是渲染 —— 因为纯渲染的话,帧时间应该是平稳的,不会出现孤立的尖峰。
查下来确认了:加载阶段的分块请求并发数达到了 128。 128 个 WebP 分块同时解码,在浏览器主线程上造成了一次"解码风暴", 直接把单帧顶到了 483 毫秒。
不是简单地"把并发调小",而是分两段处理:
效果:尖峰从 3 次降到 0 次,最大帧时间 ≤ 75 毫秒。
这个结论是实测出来的,而且它指向一个更普遍的道理: 大场景的性能问题,往往不在渲染,而在数据加载和解码。 先量一量,别急着优化渲染器。
当帧时间超标时,主动降低渲染分辨率来保住流畅度;有余量了再升回来。
这里有个不踩一次就不知道的坑:改完像素比之后,必须调用一次
syncCanvasSize(),否则设置不会立即生效 ——
我们因为这个白查了一段时间。
另外,我们的实测结论是:分辨率比"点数预算"更有效。 在当时的工况下,降低点数预算对帧时间影响有限,而降分辨率效果立竿见影。 所以策略定为:分辨率为主,点数预算保底。
同一台机器(Apple M1 Pro,16 核 GPU)、同一场景、同一相机路径, 在浏览器中以 WebGPU 后端渲染:
| 指标 | 自适应关 | 自适应开 | 变化 |
|---|---|---|---|
| 帧时间 avg | 25.0 ms | 16.7 ms | −33% |
| p95 | 31.4 ms | 17.9 ms | −43% |
| 帧率 | 39.9 | 60.0 | +50% |
| 渲染像素比 | 1.00 | 0.34 | — |
场景 11,933,631 高斯点(单层 LOD,78 分块)· 每帧约 1200 万点 · 分辨率 1400×900 · Chrome / WebGPU · Apple M1 Pro(16 核 GPU)
注意最后一行:自适应是用渲染分辨率换来的帧率。 这是有代价的取舍,不是免费的午餐 —— 所以它应该是"保底手段",而不是默认状态。
诚实地说几条我们还在做的: