Original Post — Direct link

[edit] Devin was kind enough to take notice of my plea. Hopefully I'll get an answer soon !

[edit#2] Jared delivered ! with his help, the problem is now solved and rotations are parsed and rendered correctly !

Hi !

I'm desperate: it's been 2 months already and for the life of me, I can't figure out the new rotation encoding in the replay files since version 1.45.

I'd really appreciate any pointers, if anyone from Psyonix could chime in please: /u/psyonix_corey /u/Psyonix_Devin or /u/dirkened

In 1.43, you changed the precision of position vectors to mitigate the compression caused loss of precision between theserver/client.

In v1.45, you added 2 new bit (as in an eighth of a byte) to the RigidBodyState (RBS for short) object: 1 just before and one just after the rotation vector.

And now, in my replay player, rotations are all messed up.

Here's how it used to work before:

before 1.45

And here's how it works now with 1.45+ replay files:

after 1.45

Here's how I'm doing it:

A RBS is encoded as follows:

sleeping = read_1_bit()

// decode position
read_position()

// decode rotation
if version >= 1.45:
    read_1_bit() // <-- This the first new bit

decode_rotation()

if version >= 1.45:
    read_1_bit() // <-- This is the second new bit

decode_rotation is:

rotation.X = decode_fixed_float()
rotation.Y = decode_fixed_float()
rotation.Z = decode_fixed_float()

decode_fixed_float works is:

num_bits = 16
if version >= 1.45:
    num_bits = 18

delta = read_unsigned_int_fixed_bits(num_bits)

// do some math:
bias = 2 ^ (num_bits - 1)

result =  ( delta - bias ) / (bias - 1)

I can't figure out what these 2 bits represent.

I think maybe the rotation vector now only encodes in half the space (0 to π instead of 0 to 2π), and if one of those bits is set the rotation needs to be negated, but I didn't get anywhere with that.

Please please please, pretty please ?

https://i.redd.it/jw1lnulmtbd11.gif

I'd really like to open up https://ballchasing.com to the general public (after 3 or 4 months of closed beta), which I talked about in here: https://www.reddit.com/r/RocketLeague/comments/8g1esv/the_3d_replay_viewer_tool_is_still_alive_now_with/

and this is the last blocker.

External link →
over 6 years ago - /u/Psyonix_Cone - Direct link

We now send the rotation as a quaternion using 18 bits per component instead of 16. Implementation learned from https://gafferongames.com/post/snapshot_compression/ "Optimizing Orientation" Here's snippets of the code for deserializing the quaternion:

struct FQuat
{
    // redacted lots of FQuat stuff...

    struct Component
    {
        enum Type
        {
            X,
            Y,
            Z,
            W,
            Num
        };
    };

    struct FNetCompressedData
    {
        // The 3 smallest components converted to compressed INT
        DWORD A, B, C;

        // Which component of the quaternion is largest
        DWORD LargestComponent;
    };
}

void FQuat::SerializeCompressed(FArchive& Ar, BYTE Bits) // we use 18 Bits
{
    FNetCompressedData Data;
    Ar.SerializeInt(Data.LargestComponent, FQuat::Component::Num); // 2 bits

    const INT MaxValue = (1 << Bits) - 1;
    Ar.SerializeInt(Data.A, MaxValue + 1);
    Ar.SerializeInt(Data.B, MaxValue + 1);
    Ar.SerializeInt(Data.C, MaxValue + 1);

    *this = FromNetCompressedData(Data, Bits);
}

FQuat FQuat::FromNetCompressedData(const FNetCompressedData& Data, BYTE Bits)
{
    return QuatNetCompression::FromNetCompressedData(Data, Bits);
}

namespace QuatNetCompression
{
    const FLOAT MAX_QUAT_VALUE = 0.7071067811865475244f; // 1/sqrt(2)
    const FLOAT INV_MAX_QUAT_VALUE = 1.0f / MAX_QUAT_VALUE;

    FLOAT UncompressComponent(BYTE Bits, DWORD iValue)
    {
        const INT MaxValue = (1 << Bits) - 1;
        const FLOAT PositiveRangedValue = iValue / (FLOAT)MaxValue;
        const FLOAT RangedValue = (PositiveRangedValue - 0.50f) * 2.0f;
        const FLOAT Value = RangedValue * MAX_QUAT_VALUE;
        return Value;
    }

    void UncompressComponents(const FQuat::FNetCompressedData& Data, BYTE Bits, FLOAT& A, FLOAT& B, FLOAT& C, FLOAT& Missing)
    {
        A = UncompressComponent(Bits, Data.A);
        B = UncompressComponent(Bits, Data.B);
        C = UncompressComponent(Bits, Data.C);
        Missing = appSqrt(1.0f - (A*A) - (B*B) - (C*C));
    }

    FQuat FromNetCompressedData(const FQuat::FNetCompressedData& Data, BYTE Bits)
    {
        FQuat Q;

        switch (Data.LargestComponent)
        {
        case FQuat::Component::X:
            UncompressComponents(Data, Bits, Q.Y, Q.Z, Q.W, Q.X);
            break;
        case FQuat::Component::Y:
            UncompressComponents(Data, Bits, Q.X, Q.Z, Q.W, Q.Y);
            break;
        case FQuat::Component::Z:
            UncompressComponents(Data, Bits, Q.X, Q.Y, Q.W, Q.Z);
            break;
        case FQuat::Component::W:
            UncompressComponents(Data, Bits, Q.X, Q.Y, Q.Z, Q.W);
            break;
        }

        Q.Normalize();

        return Q;
    }
}