Skip to content

Instantly share code, notes, and snippets.

@ismail0234
Forked from StagPoint/QuaternionCompression.cs
Created January 9, 2023 10:13
Show Gist options
  • Save ismail0234/1a3622661be6e4019531cbf496cb41e5 to your computer and use it in GitHub Desktop.
Save ismail0234/1a3622661be6e4019531cbf496cb41e5 to your computer and use it in GitHub Desktop.
C# - Use "smallest three" compression for transmitting Quaternion rotations in Unity's UNET networking, from 16 bytes to 7 bytes.
// Copyright (c) 2016 StagPoint Software
namespace StagPoint.Networking
{
using System;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// Provides some commonly-used functions for transferring compressed data over the network using
/// Unity's UNET networking library.
/// </summary>
public static class NetworkingExtensions
{
#region Constants and static variables
/// <summary>
/// Used when compressing float values, where the decimal portion of the floating point value
/// is multiplied by this number prior to storing the result in an Int16. Doing this allows
/// us to retain five decimal places, which for many purposes is more than adequate.
/// </summary>
private const float FLOAT_PRECISION_MULT = 10000f;
#endregion
#region NetworkReader and NetworkWriter extension methods
/// <summary>
/// Writes a compressed Quaternion value to the network stream. This function uses the "smallest three"
/// method, which is well summarized here: http://gafferongames.com/networked-physics/snapshot-compression/
/// </summary>
/// <param name="writer">The stream to write the compressed rotation to.</param>
/// <param name="rotation">The rotation value to be written to the stream.</param>
public static void WriteCompressedRotation( this NetworkWriter writer, Quaternion rotation )
{
var maxIndex = (byte)0;
var maxValue = float.MinValue;
var sign = 1f;
// Determine the index of the largest (absolute value) element in the Quaternion.
// We will transmit only the three smallest elements, and reconstruct the largest
// element during decoding.
for( int i = 0; i < 4; i++ )
{
var element = rotation[ i ];
var abs = Mathf.Abs( rotation[ i ] );
if( abs > maxValue )
{
// We don't need to explicitly transmit the sign bit of the omitted element because you
// can make the omitted element always positive by negating the entire quaternion if
// the omitted element is negative (in quaternion space (x,y,z,w) and (-x,-y,-z,-w)
// represent the same rotation.), but we need to keep track of the sign for use below.
sign = ( element < 0 ) ? -1 : 1;
// Keep track of the index of the largest element
maxIndex = (byte)i;
maxValue = abs;
}
}
// If the maximum value is approximately 1f (such as Quaternion.identity [0,0,0,1]), then we can
// reduce storage even further due to the fact that all other fields must be 0f by definition, so
// we only need to send the index of the largest field.
if( Mathf.Approximately( maxValue, 1f ) )
{
// Again, don't need to transmit the sign since in quaternion space (x,y,z,w) and (-x,-y,-z,-w)
// represent the same rotation. We only need to send the index of the single element whose value
// is 1f in order to recreate an equivalent rotation on the receiver.
writer.Write( maxIndex + 4 );
return;
}
var a = (short)0;
var b = (short)0;
var c = (short)0;
// We multiply the value of each element by QUAT_PRECISION_MULT before converting to 16-bit integer
// in order to maintain precision. This is necessary since by definition each of the three smallest
// elements are less than 1.0, and the conversion to 16-bit integer would otherwise truncate everything
// to the right of the decimal place. This allows us to keep five decimal places.
if( maxIndex == 0 )
{
a = (short)( rotation.y * sign * FLOAT_PRECISION_MULT );
b = (short)( rotation.z * sign * FLOAT_PRECISION_MULT );
c = (short)( rotation.w * sign * FLOAT_PRECISION_MULT );
}
else if( maxIndex == 1 )
{
a = (short)( rotation.x * sign * FLOAT_PRECISION_MULT );
b = (short)( rotation.z * sign * FLOAT_PRECISION_MULT );
c = (short)( rotation.w * sign * FLOAT_PRECISION_MULT );
}
else if( maxIndex == 2 )
{
a = (short)( rotation.x * sign * FLOAT_PRECISION_MULT );
b = (short)( rotation.y * sign * FLOAT_PRECISION_MULT );
c = (short)( rotation.w * sign * FLOAT_PRECISION_MULT );
}
else
{
a = (short)( rotation.x * sign * FLOAT_PRECISION_MULT );
b = (short)( rotation.y * sign * FLOAT_PRECISION_MULT );
c = (short)( rotation.z * sign * FLOAT_PRECISION_MULT );
}
writer.Write( maxIndex );
writer.Write( a );
writer.Write( b );
writer.Write( c );
}
/// <summary>
/// Reads a compressed rotation value from the network stream. This value must have been previously written
/// with WriteCompressedRotation() in order to be properly decompressed.
/// </summary>
/// <param name="reader">The network stream to read the compressed rotation value from.</param>
/// <returns>Returns the uncompressed rotation value as a Quaternion.</returns>
public static Quaternion ReadCompressedRotation( this NetworkReader reader )
{
// Read the index of the omitted field from the stream.
var maxIndex = reader.ReadByte();
// Values between 4 and 7 indicate that only the index of the single field whose value is 1f was
// sent, and (maxIndex - 4) is the correct index for that field.
if( maxIndex >= 4 && maxIndex <= 7 )
{
var x = ( maxIndex == 4 ) ? 1f : 0f;
var y = ( maxIndex == 5 ) ? 1f : 0f;
var z = ( maxIndex == 6 ) ? 1f : 0f;
var w = ( maxIndex == 7 ) ? 1f : 0f;
return new Quaternion( x, y, z, w );
}
// Read the other three fields and derive the value of the omitted field
var a = (float)reader.ReadInt16() / FLOAT_PRECISION_MULT;
var b = (float)reader.ReadInt16() / FLOAT_PRECISION_MULT;
var c = (float)reader.ReadInt16() / FLOAT_PRECISION_MULT;
var d = Mathf.Sqrt( 1f - ( a * a + b * b + c * c ) );
if( maxIndex == 0 )
return new Quaternion( d, a, b, c );
else if( maxIndex == 1 )
return new Quaternion( a, d, b, c );
else if( maxIndex == 2 )
return new Quaternion( a, b, d, c );
return new Quaternion( a, b, c, d );
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment