三维开发中的浮点精度问题与常见解决方案
前言
做 WebGIS、数字孪生这类使用真实世界坐标的三维应用时,经常会碰到这些问题:
- 模型或顶点轻微抖动,离世界原点越远越明显;
- 相机移动时,模型位置出现跳变;
- 水纹、粒子等依赖世界坐标的效果开始不稳定。
这些现象背后通常都是同一个问题:大坐标下的浮点精度损失。
本文先讲清楚为什么会抖,再介绍 RTC、RTE、Floating Origin、High / Low Encoding 等常见方案,最后结合 AGI、Cesium、UE 和 Three.js 看它们在实际引擎中是怎么用的。

为什么会抖?
问题主要出在 CPU 和 GPU 的浮点精度不同。
像 Cesium、Three.js 这类引擎,世界坐标在 CPU 侧通常可以用双精度保存。JavaScript 的 Number 本身就是 Float64,C++ 中也可以使用 double。经纬度转换成 ECEF 后,坐标虽然达到几百万米,但 Float64 仍然有足够的精度。
进入 GPU 后情况就不同了。WebGL 和常见实时渲染管线中的顶点属性、Uniform、矩阵和 Shader 浮点运算通常以 Float32 为主。CPU 中精确的世界坐标转换成 Float32 后,低位精度就可能丢失。
Float32 是 IEEE 754 标准中的 32 位单精度浮点数,大约只有 7 位十进制有效数字。这里的 7 位不是“小数点后只能保留 7 位”,而是数值越大,相邻两个可表示数之间的间隔越大。
这个间隔可以用 ULP(Unit in the Last Place) 来描述:
| 数值附近 | 相邻 Float32 的典型间隔 |
|---|---|
1 |
≈ 1.19 × 10⁻⁷ |
1,000 |
≈ 0.000061 |
100,000 |
≈ 0.0078125 |
1,000,000 |
≈ 0.0625 |
4,000,000 |
≈ 0.25 |
8,000,000 |
≈ 0.5 |
数字地球的 ECEF 坐标正好处在百万量级。地球赤道半径约为 6,378,137 m,在这个数量级附近,Float32 的位置分辨率已经只有几十厘米。
这意味着厘米级的位置变化可能无法连续表示,而是被量化到同一个 Float32 值,直到变化足够大才突然跳到下一格,画面上就表现为抖动。
更麻烦的是大数相减:
1 | Vertex - Camera |
如果 Vertex 和 Camera 都是百万级坐标,并且先转换成 Float32,低位精度在减法之前就已经丢失了。即使两者实际只相距几十米,后面也无法把这些精度找回来。
所以大世界渲染有一条很实用的原则:
全局坐标可以很大,但尽量不要让 GPU 直接使用巨大的绝对坐标完成需要高精度的计算。
后面的几种方案,基本都围绕这条原则展开。
常见技术
RTC — Relative To Center
RTC 的思路是:
给一组几何选择一个附近的参考中心,顶点只保存相对于这个中心的局部坐标。
例如某个 Tile 的世界坐标为:
1 | P = (4,123,456.123, 3,456,789.456, 3,987,654.789) |
附近选择参考中心:
1 | C = (4,123,000, 3,456,000, 3,987,000) |
顶点实际保存:
$$
P_{local}=P_{world}-C
$$
原本百万级的坐标就变成了几百量级。
CPU 仍然保存高精度世界坐标和参考中心,GPU 顶点缓冲只处理较小的局部坐标。Tile-based 数字地球通常会给每个 Tile 或一组局部几何设置自己的参考中心。
RTE — Relative To Eye
RTE 全称 Relative To Eye,也常叫 Camera-relative Rendering。
渲染时真正需要的是物体相对于相机的位置:
$$
P_{relative}=P_{world}-P_{camera}
$$
例如:
1 | World = 6,378,237.123 |
两个百万级坐标最终变成了不到 100 米的相对位置。
关键在于,大数相减必须在足够高的精度下完成。如果 World 和 Camera 先转换成 Float32,再做减法,低位精度已经丢失。
Floating Origin / Origin Rebasing
Floating Origin 常见于大世界游戏和仿真系统。
当相机或玩家离原点太远时,对场景执行一次 Origin Rebase,让当前活动区域重新回到原点附近,同时用高精度的 World Origin Offset 记录全局偏移。
它和 RTE 的目标类似,但作用层次不同:RTE 通常只影响渲染计算;Floating Origin 会直接改变场景对象所在的局部坐标系。
因此实现时还要同步处理物理、粒子、轨迹、导航等依赖坐标的系统。
High / Low Encoding
如果高精度世界坐标必须进入 Shader,可以把一个 Double 拆成两个 Float32:
$$
P \approx P_{high}+P_{low}
$$
常见做法是:
1 | high = float(value) |
high 保存 Float32 能表示的高位近似,low 保存剩余误差。
位置和相机都可以这样编码,再在 Shader 中利用 high / low 计算相对位置,避免直接拿两个已经损失低位的大 Float32 相减。
CesiumJS 的 EncodedCartesian3 就采用了这类思路。
直接提高计算精度
另一个思路是直接使用 Float64。
CPU 上这很常见,但 GPU FP64 的支持和性能取决于图形 API、Shader 环境和硬件,尤其在 Web 图形环境中,很难把它作为通用、跨平台的大世界解决方案。
因此实际项目中更常见的策略是:
CPU 保持高精度世界坐标,GPU 尽量使用局部、小范围的 Float32 坐标。
工程化实践
这些技术并不互斥。实际引擎会根据几何怎么存、最终怎么画以及场景跨度来组合使用。
AGI:RTC 与 RTE
Deron Ohlarik 在 2008 年的 Precisions, Precisions 中给出了 RTC 和 RTE 的实际用法。
RTC 并不只是把顶点改成局部坐标。渲染时,还会在 CPU 双精度环境下计算参考中心相对于相机的位置,再用于 ModelView 变换,从而避免把大坐标平移直接交给 GPU。
对于赤道面、长距离轨道线这类跨度很大的几何,一个参考中心已经无法覆盖足够小的局部范围,这时则使用 RTE。为了避免每帧在 CPU 重算所有顶点,还可以把高精度坐标拆分后传入 Shader,在 GPU 上完成相对相机计算。
Cesium:把几种方案组合起来
Cesium 基本沿用了这套思路,并根据数据和渲染阶段组合使用。
glTF 扩展 CESIUM_RTC 中,顶点相对于 center 存储,运行时再将 center 转换成 relative-to-eye 的位置。
高精度世界坐标必须进入 GPU 时,则通过 EncodedCartesian3 拆成 High / Low,并在 Shader 中结合相机的 High / Low 计算相对眼睛的位置。
因此 Cesium 并不是在 RTC、RTE、High / Low 之间三选一,而是把它们用在不同阶段:
- RTC:局部化几何数据;
- RTE:得到相对相机的位置;
- High / Low:需要在 GPU 中保留高精度坐标时使用。
UE5:相机相对世界空间
UE5 在大世界渲染中更偏向 Camera-relative 的坐标表达。
Epic 在 Efficient materials for large worlds 中推荐使用 Translated World Space,也就是 Camera-Relative World Space,让材质和渲染计算尽量发生在相机附近,而不是直接使用巨大的绝对世界坐标。
思路和 RTE 一致:全局世界可以很大,但进入 Float32 渲染计算时尽量使用相对于相机的小坐标。
Three.js:没有现成的坐标精度管线
Three.js 本身没有像 Cesium 那样完整的 RTC、RTE、High / Low 坐标精度方案。做数字地球时,通常需要自己处理大坐标精度问题。
比较常见的方式是使用 RTC:顶点坐标相对于某个参考中心存入 BufferGeometry,参考中心放到 mesh.position。在 WebGL 渲染路径中,model-view 矩阵会在 CPU 端以 Float64 完成计算,因此这种方式已经能解决大部分场景下的精度问题。
如果单个瓦片范围很大,或者必须在 Shader 中使用世界坐标,则可以进一步使用相对相机坐标,或者将 Double 拆成 High / Low 两部分传入 GPU。
WebGPU 的情况有所不同。默认情况下,model-view、normal-view 等矩阵运算会在 GPU 上以 Float32 进行,大坐标下可能在矩阵计算阶段就产生精度损失。renderer.highPrecision = true 会将这部分计算移回 CPU,以 64 位精度完成后再上传 GPU。
需要注意的是,highPrecision 解决的是矩阵计算精度,并不会提高顶点缓冲本身的精度。如果 BufferGeometry 中已经直接存入百万级 Float32 世界坐标,开启它也无法消除顶点坐标自身的精度损失。
另外,官方目前注明该模式与 InstancedMesh、SkinnedMesh 不兼容,因为实例变换和蒙皮变换仍需要在 GPU 上完成。
因此在 Three.js 中,大坐标精度仍然需要从顶点坐标的存储方式入手;WebGPU 下的 highPrecision 主要用于解决矩阵运算从 CPU 转移到 GPU 后带来的精度问题。
不只是顶点会抖
解决顶点位置精度,并不意味着整条渲染管线都稳定了。
例如水面 Shader:
1 | float n = noise(worldPosition.xz); |
如果:
1 | worldPosition.x ≈ 6,000,000 |
即使水面几何本身已经稳定,noise 的输入仍然是百万级 Float32,程序化纹理依然可能跳变。
类似的:
1 | vec2 waterUV = worldPosition.xz * scale; |
也可能让水纹 UV 变得不稳定。
粒子、阴影、程序化噪声、体积效果以及其他依赖世界坐标的算法,都可能遇到相同的问题。
所以大世界精度不能只看顶点。什么时候保持 Float64,什么时候转换成局部坐标,相对相机计算在哪一层完成,以及 Shader 中哪些计算应该避免使用绝对世界坐标,都需要统一考虑。
总结
大世界中的浮点精度问题,本质上是:
数值越大,相邻 Float32 可表示数之间的间隔越大,能够分辨的局部变化也就越粗。
解决思路也可以归结成一句话:
CPU 保持高精度的全局坐标,GPU 尽量使用局部、小范围的坐标。
RTC、RTE、Floating Origin 和 High / Low Encoding,只是在不同阶段实现这条原则。
对于数字地球这类天然使用大世界坐标的应用,精度管理更适合作为 Renderer 的底层能力统一设计。要解决的不只是某个模型不抖,而是让整条渲染管线在巨大世界尺度下,仍然保持稳定的局部精度。


