使用Curve Compression减少浮点精度

解读

国内Unity项目普遍面临包体与内存双压:动画曲线、Timeline、粒子系统、UI Tween 动辄上千条浮点曲线,原始float(4 Byte)× 采样密度 × 轨道数,在移动端极易把AnimClip内存撑爆。Curve Compression 不是简单“降精度”,而是用有损量化+二次拟合+端点保留的策略,把曲线拆成“可接受误差”内的线段或Hermite,再用均匀/非均匀量化把float压到uint8/uint16,甚至bit-pack,最终让内存↓50%–80%,采样时通过固定点查表+线性插值还原,GPU 端也能直接解压缩。面试想听到的是:你如何权衡误差、如何选算法、如何落地到Unity管线、如何与美术QA对齐

知识点

  1. 曲线存储格式:Unity原生使用dense float[],采样率30/60 Hz;压缩后改为curve segment header + quantized key + tangent
  2. 误差度量:国内大厂统一用max absolute error ≤ 0.01°/0.01units,或0.3% of range;面试必须说出误差预算如何与美术签字。
  3. 主流算法
    a. Uniform Quantization:range map到[0,255],GPU用1 cycle MAD还原;
    b. Optimal Segment Fit:Douglas-Peucker 迭代,把曲线拆成最少线段,端点保留全精度;
    c. Hermite/Spline Fit:用cubic Hermite 拟合,存储2个uint8 tangent + 2个uint16 key,误差由二次误差函数反向求解;
    d. Bit-pack:把scale曲线[-2,2] map到10bit,4条曲线打包到40bit,内存对齐5Byte。
  4. Unity落地:
    • 导入管线:AssetPostprocessor 在OnPostprocessAnimation 回调里替换AnimationCurve为自定义CompressedCurve;
    • 运行时:Playable API 自定义CompressedAnimationPlayable,在ProcessFrame里用Burst+SIMD解包;
    • 热更兼容:把压缩数据放Addressable的BlobAsset,不破坏原AnimClip,老客户端回退到原始曲线。
  5. 性能指标:iPhone 8 上1000条压缩曲线同时采样,主线程耗时<0.1 ms,相比原始floatcache-miss↓35%

答案

“我在上一个项目中把角色、相机、UI 三大动画系统全部做了Curve Compression。
第一步,与美术对齐误差预算:旋转≤0.01°,位移≤0.5cm,缩放≤0.2%,误差超过即回退。
第二步,离线工具链:写了一个AnimationClipPostprocessor,在导入时把每段曲线做Douglas-Peucker+Uniform Quantization,key 数量降到原来的18%,再把range线性映射到uint8;如果某段误差超标,自动拆成两段并保留端点float精度。
第三步,运行时自定义CompressedCurvePlayable,用Burst编译的SIMD kernel 把uint8 解包成float,耗时只有0.08 ms/1000曲线,内存从5.2MB降到1.1MB,低端机发热降低1.3℃。
第四步,为了兼容热更,压缩数据存到Addressable的BlobAsset,老版本客户端无感知回退,零线上事故。上线后DAU 800万,崩溃率无上升,美术验收一次性通过。”

拓展思考

  1. 如果项目用到Timeline + Control Track,如何把压缩曲线与PlayableAsset 的ExposedReference 结合,保证在Timeline 1.7+版本里不触发重新序列化
  2. WebGL 平台,JavaScript 端无法使用SIMD,如何设计two-pass 解压:第一趟用uint8 线性表在Worker 里解压,第二趟主线程只做lerp,避免主线程卡顿
  3. 面对IK 动画,压缩后可能出现末端效应器超差,如何引入end-effector constraint,在压缩阶段用Jacobian 迭代反向调整关键帧,使得误差累积<0.5cm
  4. 如果未来项目要上GPU Driven Animation,如何把压缩曲线直接上传到ComputeBuffer,用DXBC/Metal 的bitfield extract 指令在GPU 端解包,节省CPU 回读?