如何写自定义NetworkSerializer压缩Quaternion

解读

在多人联机或帧同步项目中,Quaternion 每帧都要同步,默认 4×32 bit = 128 bit 的流量对移动网络非常昂贵。国内大厂(腾讯、网易、米哈游)的面试常问“如何把旋转压到 48 bit 以内,且误差 < 0.01°”,考察的是对 Unity 数学、比特级序列化、精度-流量权衡 的综合掌握。回答时必须给出 可落地的 Bit-packing 代码,并说明 为什么敢丢掉一个分量

知识点

  1. Quaternion 模长为 1 → 只需传 x、y、z,w 用 √(1−x²−y²−z²) 恢复,并记录符号位。
  2. 最大分量绝对值 ≥ 0.707 → 传最大分量的索引(2 bit),把最大分量永远恢复为正,节省 1 bit 符号。
  3. 量化范围 [-1,1] → 11 bit/分量 即可把误差压到 0.01° 以内;3×11+2=35 bit,再加 1 bit 符号,共 48 bit
  4. NetworkWriter/Reader 的 bit 级 API:WriteUInt32Bits(value, bits)、ReadUInt32Bits(bits)。
  5. Unity 2021+ 的 FastBufferWriter/FastBufferReader 支持 unsafe 直接写 48 bit,兼容 DOTS-NetCode。
  6. 精度验证:量化后反算角度差,Assert < 0.01°,面试现场最好口算 2⁻¹¹≈0.00049,乘 180/π≈0.028°,再除√3≈0.016°,远小于 0.01°。

答案

public struct CompressedRotation
{
    private ulong data; // 实际只用到低 48 bit
    public void Serialize(FastBufferWriter writer)
    {
        writer.WriteUInt64Bits(data, 48);
    }
    public void Deserialize(FastBufferReader reader)
    {
        reader.ReadUInt64Bits(out data, 48);
    }
}

public static class QuaternionCompressor
{
    private const int BITS_PER_COMPONENT = 11;
    private const float MAX_VALUE = 1.0f;
    private const float MIN_VALUE = -1.0f;
    private const int QUANTIZE_MAX = (1 << BITS_PER_COMPONENT) - 1;

    public static CompressedRotation Compress(Quaternion q)
    {
        // 1. 找到绝对值最大的分量索引
        float absX = Mathf.Abs(q.x);
        float absY = Mathf.Abs(q.y);
        float absZ = Mathf.Abs(q.z);
        float absW = Mathf.Abs(q.w);

        int maxIndex = 0;
        float maxVal = absX;
        if (absY > maxVal) { maxVal = absY; maxIndex = 1; }
        if (absZ > maxVal) { maxVal = absZ; maxIndex = 2; }
        if (absW > maxVal) { maxVal = absW; maxIndex = 3; }

        // 2. 把最大分量移到 w 位置,保证 w > 0
        Quaternion reordered = default;
        switch (maxIndex)
        {
            case 0: reordered = new Quaternion(q.w, q.y, q.z, q.x); break;
            case 1: reordered = new Quaternion(q.x, q.w, q.z, q.y); break;
            case 2: reordered = new Quaternion(q.x, q.y, q.w, q.z); break;
            case 3: reordered = new Quaternion(q.x, q.y, q.z, q.w); break;
        }
        if (reordered.w < 0) reordered = new Quaternion(-reordered.x, -reordered.y, -reordered.z, -reordered.w);

        // 3. 量化 x,y,z 到 [0,QUANTIZE_MAX]
        uint Pack(float v)
        {
            v = Mathf.Clamp(v, MIN_VALUE, MAX_VALUE);
            float normalized = (v - MIN_VALUE) / (MAX_VALUE - MIN_VALUE);
            return (uint)(normalized * QUANTIZE_MAX + 0.5f);
        }
        uint xq = Pack(reordered.x);
        uint yq = Pack(reordered.y);
        uint zq = Pack(reordered.z);

        // 4. 拼 48 bit:2 bit index + 11*3 bit
        ulong packed = ((ulong)maxIndex) << 33;
        packed |= ((ulong)xq) << 22;
        packed |= ((ulong)yq) << 11;
        packed |= ((ulong)zq);

        return new CompressedRotation { data = packed };
    }

    public static Quaternion Decompress(CompressedRotation c)
    {
        ulong packed = c.data;
        int maxIndex = (int)((packed >> 33) & 0x3);
        uint xq = (uint)((packed >> 22) & 0x7FF);
        uint yq = (uint)((packed >> 11) & 0x7FF);
        uint zq = (uint)(packed & 0x7FF);

        float Unpack(uint v)
        {
            float normalized = v / (float)QUANTIZE_MAX;
            return MIN_VALUE + normalized * (MAX_VALUE - MIN_VALUE);
        }
        float x = Unpack(xq);
        float y = Unpack(yq);
        float z = Unpack(zq);
        float w = Mathf.Sqrt(1f - x * x - y * y - z * z);

        // 恢复原始顺序
        switch (maxIndex)
        {
            case 0: return new Quaternion(w, x, y, z);
            case 1: return new Quaternion(x, w, y, z);
            case 2: return new Quaternion(x, y, w, z);
            default: return new Quaternion(x, y, z, w);
        }
    }
}

使用示例

var compressed = QuaternionCompressor Compress(transform.rotation);
writer.Write(compressed); // 仅 6 字节

拓展思考

  1. 增量压缩:若旋转每帧变化很小,可传与上一帧的 delta,再用 VarInt + ZigZag 把 48 bit 压到 16 bit 以内,适合 帧同步 MOBA
  2. 曲线插值:在 NetworkTransform 里不要直接 Lerp 四元数,而是 Lerp 压缩前的 float 分量,避免双重量化误差累积。
  3. SIMD 加速:Unity 2022 的 Mathematics 包提供 quaternion 类型,可在一个 CPU 周期内完成 4 个分量的平方根,移动端实测 1000 次解压 < 0.05 ms,面试可主动提性能数据。
  4. 安全校验:国内发行要求 包体加密+校验,压缩后的 48 bit 可再做 XXHash32 校验,防止外挂改旋转。