Skip to content

Instantly share code, notes, and snippets.

@StagPoint
Last active April 29, 2024 12:40
Show Gist options
  • Star 43 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save StagPoint/bb7edf61c2e97ce54e3e4561627f6582 to your computer and use it in GitHub Desktop.
Save StagPoint/bb7edf61c2e97ce54e3e4561627f6582 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
}
}
@StagPoint
Copy link
Author

Since I no longer use Unity, and all internal version updates have been largely forgotten, there are likely never going to be any additional updates.

If anyone else has an updated version posted somewhere that they'd like to let us know about, I'd be happy to redirect people to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment