解释浮点精度与原点重定位

解读

面试官抛出该问题,通常出现在“大型场景、开放世界、数字孪生、LBS-AR”这类对空间连续性数值稳定性要求极高的项目背景中。
他真正想确认的是:

  1. 你是否理解IEEE-754单精度浮点在Unity里只有~7位有效十进制数字的硬限制;
  2. 当角色远离世界原点(0,0,0)时,顶点、物理、动画、粒子、NavMesh、Timeline会依次出现哪些可见异常;
  3. 你有没有在生产环境落地过原点偏移(Floating Origin)分块坐标系(Chunk-based)方案,能把CPU、GPU、第三方插件、热更新包体全部拉齐,而不是只改Transform.position。

一句话:不仅知道“为什么抖”,还要知道“怎么挪、挪多少、谁来挪、挪完怎么同步”。

知识点

  1. 浮点精度模型

    • float32:1 符号位 + 8 指数位 + 23 尾数位 ⇒ 有效精度2^-23≈1.19×10^-7
    • 量级每翻2^10≈1024倍,绝对误差翻倍;在Unity单位=1米的设定下,距离原点8192 m时,最小可表示步长已达1 mm65536 m时,步长约8 mm,相机慢移即“抖动”。
  2. 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,会二次精度丢失
  3. 原点重定位(Floating Origin)核心算法

    • 触发阈值:通常取Camera.farClipPlane * 25000 m,保证视锥体内部坐标始终处于高精度区间;
    • 偏移向量:以主摄像机玩家角色为锚点,将originOffset = playerPosition
    • 一次性位移
      – 所有根级GameObject的Transform.position -= originOffset;
      – 物理世界需Physics.SyncTransforms()后,再PhysicsScene.ShiftOrigin(Unity 2022.1+正式API);
      – 已烘焙的NavMeshNavMeshBuilder.UpdateNavMeshData重烘焙或使用局部NavMeshSurface
      – GPU端若使用世界空间着色器,需把_WorldSpaceCameraPos_WorldSpaceLightPos0等常量减掉originOffset,或干脆切到摄像机相对着色(Camera-relative Rendering)
    • 数据层同步
      – 服务端坐标仍以绝对int64或double存储,客户端收到后本地减去当前originOffset再喂给引擎;
      – 热更新层把originOffset暴露为只读全局变量,保证逻辑层所有Vector3运算以float精度、相对坐标进行;
    • 多人联机:每个玩家客户端维护自己的originOffset,RPC只传绝对double,到本地再转相对float,避免不同client精度不一致导致碰撞不同步
  4. 分块坐标系(Chunk-based)

    • 把世界切成1024 m8192 m边长的格子,每个格子用int chunkX,chunkZ + float localX,localZ表示位置;
    • 跨块移动时只切换chunkID,local坐标始终靠近0
    • 需要自定义ChunkSpaceConverter,把chunk+local转回世界double,再转回float渲染;
    • 适用于数字孪生城市、星链级太空场景,能把64位整数网格32位渲染无缝桥接。
  5. 性能与包体注意事项

    • 原点重定位触发时,一次MoveGameObject会产生O(N)的TransformDirty,需分帧分批处理或使用Unity.Entities.WorldEntityManager.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

  1. 以主摄像机为中心,每超过5 km触发一次原点重定位;
  2. 所有根节点、物理场景、GPU常量、NavMesh、VFX整体减去偏移向量,保证渲染与逻辑坐标始终落在高精度区间;
  3. 网络层仍用int64绝对坐标通信,客户端本地转相对float,防止不同客户端精度不一致
  4. 数字孪生超大世界,可进一步采用Chunk-based分块坐标,用chunkID+localOffset双字段表示位置,彻底摆脱float长度限制
    整个过程必须分帧分批、同步Physics、刷新NavMesh、更新Shader常量,并把originOffset暴露给热更新层,确保逻辑、渲染、网络、热更四端一致,才能让用户在1000 km×1000 km的地图上依旧平滑漫游。”

拓展思考

  1. 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端始终相对零点附近运算

  2. 大型地形高度图
    TerrainData.heightmapResolution最大4097,单格浮点高度0~1,实际世界高度乘以terrainSize.y。当terrainSize.y=8000 m时,高度精度只剩8000/65535≈0.12 m,楼梯状断层明显。需要把高度拆成两层float16高位+float16低位,或运行时把高度图切为相对局部零点的float32贴图,再合并到材质里。

  3. 物理连续性与快照回放
    原点重定位后,PhysX内部缓存的contact point仍为旧坐标,若此时保存物理快照(Physics.Simulate + Physics.DumpScene),下次加载再ShiftOrigin会导致contact求解异常。正确顺序是先DumpScene,再ShiftOrigin,最后ReloadScene;或者在Dump前把contact点手动减originOffset,保证回放 determinism

  4. WebGL与移动端适配
    WebGL 2.0不支持64位整数uniform,chunkID需拆成两个float32传参;
    低端安卓GPU(Mali-G52)对camera-relative矩阵fp16快速路径,但要求矩阵元素绝对值<65504,否则回退fp32性能下降15%,需要把originOffset对齐到256 m的整数倍,减少矩阵元素数值。