Best way to serialize/unserialize a Quaternion

Started by
11 comments, last by Obs-D 9 months, 3 weeks ago

Hello there,

I'm making this question from an a code agnostic perspective but you may assume C# (not unity, pure c#) if so you choose.

What would be the best way to serialize/unserialize a Quaternion?

Asking for saving purposes but it will also be cool if you can tell me a good way to transmit that data, if that change the answer somehow, i ask this because i'm not certain what parts of the quaternium are relevant when the time comes to transmit them to the server.

Thank you and have a nice day

None

Advertisement

I convert to half precision and send that with no further fuss.

To get the best possible size, I guess you would want to transmit a quaternion in an axis+angle representation. Normally a quaternion is like this: [ cos(theta/2), sin(theta/2)*axis.x, sin(theta/2)*axis.y, sin(theta/2)*axis.z ]. You can easily recover the axis and angle using acos(). If you can afford some loss of precision, then I would serialize theta and axis as signed 8-bit integers (a total of 32 bits). This gives you almost 1 degree precision. If you need better precision then maybe 16-bit integers would be good enough. You could maybe cut it down to 3 8-bit values if you use Euler angles instead, then turn those into a quaternion on the receiving end.

It depends on how much precision you want.

In general, you can reconstruct the fourth component of a quaternion from three of its components and knowing which one is lost.

This means you can quantize the quaternion axes to 10 bits each, and use the final 2 bits of a 32-bit value to store which axies was thrown away.

You throw away the biggest magnitude axis, which means that the maximum magnitude stored in the remaining values is sqrt(0.5) so your quantization range is -sqrt(0.5) .. sqrt(0.5)

10 bits is okay; you'll get within 1% of a steradian if I remember correctly. If you need better than that, keep piling on the bits – still, two bits for “what did I throw away,” and the remaining bits for each of the components. 18 bits each for the components and 2 bits for selector means 56 bits total, or 7 bytes, for example.

enum Bool { True, False, FileNotFound };

I thought about this in the past. Start with a + bi + cj + dk. The quaternions we are interested in have length 1, and it doesn't matter if you flip their sign. So we can encode in 2 bits which of the coordinates is the largest in absolute value (say it's b) and divide by it. Now we have something like a/b + i + (c/b)j + (d/b)k. It's enough to encode the numbers a/b, c/b and d/b, and they are all between -1 and 1.

So you could use 32 bits to encode a quaternion, using code like this:

using System;

class QuaternionEncoder
{
    public static uint Encode3(float x, float y, float z)
    {
        uint ux = (uint)Math.Round((x + 1.0f) * 511.0f);
        uint uy = (uint)Math.Round((y + 1.0f) * 511.0f);
        uint uz = (uint)Math.Round((z + 1.0f) * 511.0f);
        return (ux << 20) | (uy << 10) | uz;
    }

    public static uint EncodeQuaternion(float a, float b, float c, float d)
    {
        float max = Math.Max(Math.Max(Math.Max(Math.Abs(a), Math.Abs(b)), Math.Abs(c)), Math.Abs(d));
        if (Math.Abs(a) == max)
            return (0u << 30) | Encode3(b / a, c / a, d / a);
        if (Math.Abs(b) == max)
            return (1u << 30) | Encode3(a / b, c / b, d / b);
        if (Math.Abs(c) == max)
            return (2u << 30) | Encode3(a / c, b / c, d / c);
        return (3u << 30) | Encode3(a / d, b / d, c / d);
    }

    public static void NormalizeQuaternion(ref float a, ref float b, ref float c, ref float d)
    {
        float invLength = 1.0f / (float)Math.Sqrt(a * a + b * b + c * c + d * d);
        a *= invLength;
        b *= invLength;
        c *= invLength;
        d *= invLength;
    }

    public static void Decode3(uint u, out float x, out float y, out float z)
    {
        x = ((u >> 20) & 0x3ffu) / 511.0f - 1.0f;
        y = ((u >> 10) & 0x3ffu) / 511.0f - 1.0f;
        z = (u & 0x3ffu) / 511.0f - 1.0f;
    }

    public static void DecodeQuaternion(uint u, out float a, out float b, out float c, out float d)
    {
        a = b = c = d = 0;
        switch (u >> 30)
        {
            case 0: a = 1.0f; Decode3(u, out b, out c, out d); break;
            case 1: b = 1.0f; Decode3(u, out a, out c, out d); break;
            case 2: c = 1.0f; Decode3(u, out a, out b, out d); break;
            case 3: d = 1.0f; Decode3(u, out a, out b, out c); break;
        }
        NormalizeQuaternion(ref a, ref b, ref c, ref d);
    }
}

Aressera said:
To get the best possible size, I guess you would want to transmit a quaternion in an axis+angle representation.

That's still 4 values, but you could use a ‘rotation vector’ instead. There seems no general name for this, so that's just how i call it.
I mean simply axis scaled by angle, so 3 values, as used to represent angular velocity in physics, for example.

@alvaro Shouldn't Decode3() actually set a = b = c = d = 1 for the decode path?

I think the code posted won't work without that!

enum Bool { True, False, FileNotFound };

hplus0603 said:

@alvaro Shouldn't Decode3() actually set a = b = c = d = 1 for the decode path?

I think the code posted won't work without that!

I don't understand your objection. Decode3 doesn't know about a, b, c or d.

alvaro said:
Decode3 doesn't know about a, b, c or d.

Sorry, I meant DecodeQuaternion before it calls Decode3

As it is, the a = b = c = d = 0 line will make sure that at least one value is always 0, which clearly isn't correct. The eliminated element should be the one that's divided out by itself to 1.0, so the non-decoded element should be 1 before re-normalization.

enum Bool { True, False, FileNotFound };

Actually, the line a=b=c=d=0 isn't needed at all. I originally wrote the code in C++ and used GPT-4 to translate it to C#, and I think it added that for no reason. My bad for not checking more carefully.

All variables are assigned values in every case.

This topic is closed to new replies.

Advertisement