242 lines
8.5 KiB
C#
242 lines
8.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
|
|
namespace SRMultiplayer
|
|
{
|
|
/// <summary>
|
|
/// Functions to Compress Quaternions and Floats
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Uncompressed Quaternion = 32 * 4 = 128 bits => send 16 bytes
|
|
///
|
|
/// <para>
|
|
/// Quaternion is always normalized so we drop largest value and re-calculate it.
|
|
/// We can encode which one is the largest using 2 bits
|
|
/// <code>
|
|
/// x^2 + y^2 + z^2 + w^2 = 1
|
|
/// </code>
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// 2nd largest value has max size of 1/sqrt(2)
|
|
/// We can encode the smallest three components in [-1/sqrt(2),+1/sqrt(2)] instead of [-1,+1]
|
|
/// <code>
|
|
/// c^2 + c^2 + 0 + 0 = 1
|
|
/// </code>
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Sign of largest value doesn't matter
|
|
/// <code>
|
|
/// Q * vec3 == (-Q) * vec3
|
|
/// </code>
|
|
/// </para>
|
|
///
|
|
/// <list type="bullet">
|
|
/// <listheader><description>
|
|
/// RotationPrecision <br/>
|
|
/// <code>
|
|
/// 2/sqrt(2) / (2^bitCount - 1)
|
|
/// </code>
|
|
/// </description></listheader>
|
|
///
|
|
/// <item><description>
|
|
/// rotation precision +-0.00138 in range [-1,+1]
|
|
/// <code>
|
|
/// 10 bits per value
|
|
/// 2 + 10 * 3 = 32 bits => send 4 bytes
|
|
/// </code>
|
|
/// </description></item>
|
|
/// </list>
|
|
///
|
|
/// <para>
|
|
/// Links for more info:
|
|
/// <br/><see href="https://youtu.be/Z9X4lysFr64">GDC Talk</see>
|
|
/// <br/><see href="https://gafferongames.com/post/snapshot_compression/">Post on Snapshot Compression</see>
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class Compression
|
|
{
|
|
const float QuaternionMinValue = -1f / 1.414214f; // 1/ sqrt(2)
|
|
const float QuaternionMaxValue = 1f / 1.414214f;
|
|
|
|
const int QuaternionBitLength = 10;
|
|
// same as Mathf.Pow(2, targetBitLength) - 1
|
|
const uint QuaternionUintRange = (1 << QuaternionBitLength) - 1;
|
|
|
|
/// <summary>
|
|
/// Used to Compress Quaternion into 4 bytes
|
|
/// </summary>
|
|
public static uint CompressQuaternion(Quaternion value)
|
|
{
|
|
// make sure value is normalized (don't trust user given value, and math here assumes normalized)
|
|
value = value.normalized;
|
|
|
|
int largestIndex = FindLargestIndex(value);
|
|
Vector3 small = GetSmallerDimensions(largestIndex, value);
|
|
// largest needs to be positive to be calculated by reader
|
|
// if largest is negative flip sign of others because Q = -Q
|
|
if (value[largestIndex] < 0)
|
|
{
|
|
small *= -1;
|
|
}
|
|
|
|
uint a = ScaleToUInt(small.x, QuaternionMinValue, QuaternionMaxValue, 0, QuaternionUintRange);
|
|
uint b = ScaleToUInt(small.y, QuaternionMinValue, QuaternionMaxValue, 0, QuaternionUintRange);
|
|
uint c = ScaleToUInt(small.z, QuaternionMinValue, QuaternionMaxValue, 0, QuaternionUintRange);
|
|
|
|
// pack each 10 bits and extra 2 bits into uint32
|
|
uint packed = a | b << 10 | c << 20 | (uint)largestIndex << 30;
|
|
|
|
return packed;
|
|
}
|
|
|
|
internal static int FindLargestIndex(Quaternion q)
|
|
{
|
|
int index = 0;
|
|
float current = 0;
|
|
|
|
// check each value to see which one is largest (ignoring +-)
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
float next = Mathf.Abs(q[i]);
|
|
if (next > current)
|
|
{
|
|
index = i;
|
|
current = next;
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
static Vector3 GetSmallerDimensions(int largestIndex, Quaternion value)
|
|
{
|
|
float x = value.x;
|
|
float y = value.y;
|
|
float z = value.z;
|
|
float w = value.w;
|
|
|
|
switch (largestIndex)
|
|
{
|
|
case 0:
|
|
return new Vector3(y, z, w);
|
|
case 1:
|
|
return new Vector3(x, z, w);
|
|
case 2:
|
|
return new Vector3(x, y, w);
|
|
case 3:
|
|
return new Vector3(x, y, z);
|
|
default:
|
|
throw new IndexOutOfRangeException("Invalid Quaternion index!");
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Used to read a Compressed Quaternion from 4 bytes
|
|
/// <para>Quaternion is normalized</para>
|
|
/// </summary>
|
|
public static Quaternion DecompressQuaternion(uint packed)
|
|
{
|
|
// 10 bits
|
|
const uint mask = 0b11_1111_1111;
|
|
Quaternion result;
|
|
|
|
|
|
uint a = packed & mask;
|
|
uint b = (packed >> 10) & mask;
|
|
uint c = (packed >> 20) & mask;
|
|
uint largestIndex = (packed >> 30) & mask;
|
|
|
|
float x = ScaleFromUInt(a, QuaternionMinValue, QuaternionMaxValue, 0, QuaternionUintRange);
|
|
float y = ScaleFromUInt(b, QuaternionMinValue, QuaternionMaxValue, 0, QuaternionUintRange);
|
|
float z = ScaleFromUInt(c, QuaternionMinValue, QuaternionMaxValue, 0, QuaternionUintRange);
|
|
|
|
Vector3 small = new Vector3(x, y, z);
|
|
result = FromSmallerDimensions(largestIndex, small);
|
|
return result;
|
|
}
|
|
|
|
static Quaternion FromSmallerDimensions(uint largestIndex, Vector3 smallest)
|
|
{
|
|
float a = smallest.x;
|
|
float b = smallest.y;
|
|
float c = smallest.z;
|
|
|
|
float largest = Mathf.Sqrt(1 - a * a - b * b - c * c);
|
|
switch (largestIndex)
|
|
{
|
|
case 0:
|
|
return new Quaternion(largest, a, b, c).normalized;
|
|
case 1:
|
|
return new Quaternion(a, largest, b, c).normalized;
|
|
case 2:
|
|
return new Quaternion(a, b, largest, c).normalized;
|
|
case 3:
|
|
return new Quaternion(a, b, c, largest).normalized;
|
|
default:
|
|
throw new IndexOutOfRangeException("Invalid Quaternion index!");
|
|
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Scales float from minFloat->maxFloat to minUint->maxUint
|
|
/// <para>values out side of minFloat/maxFloat will return either 0 or maxUint</para>
|
|
/// </summary>
|
|
/// <param name="value"></param>
|
|
/// <param name="minFloat"></param>
|
|
/// <param name="maxFloat"></param>
|
|
/// <param name="minUint">should be a power of 2, can be 0</param>
|
|
/// <param name="maxUint">should be a power of 2, for example 1 << 8 for value to take up 8 bytes</param>
|
|
/// <returns></returns>
|
|
public static uint ScaleToUInt(float value, float minFloat, float maxFloat, uint minUint, uint maxUint)
|
|
{
|
|
// if out of range return min/max
|
|
if (value > maxFloat) { return maxUint; }
|
|
if (value < minFloat) { return minUint; }
|
|
|
|
float rangeFloat = maxFloat - minFloat;
|
|
uint rangeUint = maxUint - minUint;
|
|
|
|
// scale value to 0->1 (as float)
|
|
float valueRelative = (value - minFloat) / rangeFloat;
|
|
// scale value to uMin->uMax
|
|
float outValue = valueRelative * rangeUint + minUint;
|
|
|
|
return (uint)outValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scales uint from minUint->maxUint to minFloat->maxFloat
|
|
/// </summary>
|
|
/// <param name="value"></param>
|
|
/// <param name="minFloat"></param>
|
|
/// <param name="maxFloat"></param>
|
|
/// <param name="minUint">should be a power of 2, can be 0</param>
|
|
/// <param name="maxUint">should be a power of 2, for example 1 << 8 for value to take up 8 bytes</param>
|
|
/// <returns></returns>
|
|
public static float ScaleFromUInt(uint value, float minFloat, float maxFloat, uint minUint, uint maxUint)
|
|
{
|
|
// if out of range return min/max
|
|
if (value > maxUint) { return maxFloat; }
|
|
if (value < minUint) { return minFloat; }
|
|
|
|
float rangeFloat = maxFloat - minFloat;
|
|
uint rangeUint = maxUint - minUint;
|
|
|
|
// scale value to 0->1 (as float)
|
|
// make sure divide is float
|
|
float valueRelative = (value - minUint) / (float)rangeUint;
|
|
// scale value to fMin->fMax
|
|
float outValue = valueRelative * rangeFloat + minFloat;
|
|
return outValue;
|
|
}
|
|
}
|
|
}
|