解释浮点精度与原点重定位
解读
面试官抛出该问题,通常出现在“大型场景、开放世界、数字孪生、LBS-AR”这类对空间连续性与数值稳定性要求极高的项目背景中。
他真正想确认的是:
- 你是否理解IEEE-754单精度浮点在Unity里只有~7位有效十进制数字的硬限制;
- 当角色远离世界原点(0,0,0)时,顶点、物理、动画、粒子、NavMesh、Timeline会依次出现哪些可见异常;
- 你有没有在生产环境落地过原点偏移(Floating Origin)或分块坐标系(Chunk-based)方案,能把CPU、GPU、第三方插件、热更新包体全部拉齐,而不是只改Transform.position。
一句话:不仅知道“为什么抖”,还要知道“怎么挪、挪多少、谁来挪、挪完怎么同步”。
知识点
-
浮点精度模型
- float32:1 符号位 + 8 指数位 + 23 尾数位 ⇒ 有效精度2^-23≈1.19×10^-7;
- 量级每翻2^10≈1024倍,绝对误差翻倍;在Unity单位=1米的设定下,距离原点8192 m时,最小可表示步长已达1 mm;65536 m时,步长约8 mm,相机慢移即“抖动”。
-
Unity内部依赖float的模块
- Transform、Camera.worldToCameraMatrix、Shader内置
_WorldSpaceCameraPos; - Physics:PhysX 3.4/4.1所有Collision Shape顶点存储为float,>50 km后刚体开始下沉或穿透;
- NavMesh:烘焙顶点同样float,Agent路径偏移;
- Particle System、LineRenderer、VFX Graph的世界空间模拟;
- Timeline/Playable:曲线关键帧使用float存储时间、值;
- 热更新框架(Lua、ILRuntime)若把Vector3压到lua number,再转回float,会二次精度丢失。
- Transform、Camera.worldToCameraMatrix、Shader内置
-
原点重定位(Floating Origin)核心算法
- 触发阈值:通常取Camera.farClipPlane * 2或5000 m,保证视锥体内部坐标始终处于高精度区间;
- 偏移向量:以主摄像机或玩家角色为锚点,将originOffset = playerPosition;
- 一次性位移:
– 所有根级GameObject的Transform.position -= originOffset;
– 物理世界需Physics.SyncTransforms()后,再PhysicsScene.ShiftOrigin(Unity 2022.1+正式API);
– 已烘焙的NavMesh要NavMeshBuilder.UpdateNavMeshData重烘焙或使用局部NavMeshSurface;
– GPU端若使用世界空间着色器,需把_WorldSpaceCameraPos、_WorldSpaceLightPos0等常量减掉originOffset,或干脆切到摄像机相对着色(Camera-relative Rendering); - 数据层同步:
– 服务端坐标仍以绝对int64或double存储,客户端收到后本地减去当前originOffset再喂给引擎;
– 热更新层把originOffset暴露为只读全局变量,保证逻辑层所有Vector3运算以float精度、相对坐标进行; - 多人联机:每个玩家客户端维护自己的originOffset,RPC只传绝对double,到本地再转相对float,避免不同client精度不一致导致碰撞不同步。
-
分块坐标系(Chunk-based)
- 把世界切成1024 m或8192 m边长的格子,每个格子用int chunkX,chunkZ + float localX,localZ表示位置;
- 跨块移动时只切换chunkID,local坐标始终靠近0;
- 需要自定义ChunkSpaceConverter,把chunk+local转回世界double,再转回float渲染;
- 适用于数字孪生城市、星链级太空场景,能把64位整数网格与32位渲染无缝桥接。
-
性能与包体注意事项
- 原点重定位触发时,一次MoveGameObject会产生O(N)的TransformDirty,需分帧分批处理或使用Unity.Entities.World的EntityManager.MoveEntities;
- 热更新框架若缓存了Vector3,需要主动失效缓存,否则逻辑层继续用旧值;
- IL2CPP下,大规模泛型ValueType(List<Vector3>)会在偏移时触发JIT-like trampoline重排,峰值卡顿1~2 ms,需提前warm。
答案
“浮点精度问题根源于IEEE-754单精度只有7位有效数字,当场景尺寸超过8 km量级,最小可表示步长达到毫米级,造成顶点抖动、物理穿透、导航偏移。
Unity内部从Transform到PhysX、NavMesh、粒子、Timeline全链依赖float,无法靠简单改double解决。
生产级方案是Floating Origin:
- 以主摄像机为中心,每超过5 km触发一次原点重定位;
- 把所有根节点、物理场景、GPU常量、NavMesh、VFX整体减去偏移向量,保证渲染与逻辑坐标始终落在高精度区间;
- 网络层仍用int64绝对坐标通信,客户端本地转相对float,防止不同客户端精度不一致;
- 对数字孪生超大世界,可进一步采用Chunk-based分块坐标,用chunkID+localOffset双字段表示位置,彻底摆脱float长度限制。
整个过程必须分帧分批、同步Physics、刷新NavMesh、更新Shader常量,并把originOffset暴露给热更新层,确保逻辑、渲染、网络、热更四端一致,才能让用户在1000 km×1000 km的地图上依旧平滑漫游。”
拓展思考
-
GPU端Camera-Relative Rendering
Unity 2022.2 URP/HDRP已内置camera-relative matrices,但自定义Shader若直接拿unity_ObjectToWorld乘顶点,会把原点偏移又乘回去,导致TAA、SSR、SSAO全部重影。解决方法是在C#层把unity_ObjectToWorld的w分量减去originOffset,或在Shader里用float3 worldPos = mul(unity_ObjectToWorld, float4(localPos,1)).xyz - _OriginOffset;,保证GPU端始终相对零点附近运算。 -
大型地形高度图
TerrainData.heightmapResolution最大4097,单格浮点高度0~1,实际世界高度乘以terrainSize.y。当terrainSize.y=8000 m时,高度精度只剩8000/65535≈0.12 m,楼梯状断层明显。需要把高度拆成两层:float16高位+float16低位,或运行时把高度图切为相对局部零点的float32贴图,再合并到材质里。 -
物理连续性与快照回放
原点重定位后,PhysX内部缓存的contact point仍为旧坐标,若此时保存物理快照(Physics.Simulate + Physics.DumpScene),下次加载再ShiftOrigin会导致contact求解异常。正确顺序是先DumpScene,再ShiftOrigin,最后ReloadScene;或者在Dump前把contact点手动减originOffset,保证回放 determinism。 -
WebGL与移动端适配
WebGL 2.0不支持64位整数uniform,chunkID需拆成两个float32传参;
低端安卓GPU(Mali-G52)对camera-relative矩阵有fp16快速路径,但要求矩阵元素绝对值<65504,否则回退fp32性能下降15%,需要把originOffset对齐到256 m的整数倍,减少矩阵元素数值。