mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
feat: Quaternion and float Compression (#2368)
Adding compression methods for Quaternion and floats. These methods can be used to decrease size of Quaternions before sending the value over the network. ScaleToUInt method can be used to compress float from 32 bits to the range given to the method. This can be used to compress Vector3 if the bounds of the world are known and fixed before runtime.
This commit is contained in:
parent
9182b32946
commit
bbb61848be
225
Assets/Mirror/Runtime/Compression.cs
Normal file
225
Assets/Mirror/Runtime/Compression.cs
Normal file
@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <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 doesnt 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)
|
||||
{
|
||||
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 = default;
|
||||
float current = default;
|
||||
|
||||
// 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>
|
||||
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>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Runtime/Compression.cs.meta
Normal file
11
Assets/Mirror/Runtime/Compression.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c28963f9c4b97e418252a55500fb91e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
141
Assets/Mirror/Tests/Editor/CompressionFloatTest.cs
Normal file
141
Assets/Mirror/Tests/Editor/CompressionFloatTest.cs
Normal file
@ -0,0 +1,141 @@
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Tests
|
||||
{
|
||||
public class CompressionFloatTest
|
||||
{
|
||||
[Test]
|
||||
[TestCaseSource(nameof(FloatTestCases))]
|
||||
public void CanScaleToUintAndBack(float value, float minFloat, float maxFloat, uint minUint, uint maxUint, float allowedPrecision)
|
||||
{
|
||||
uint packed = Compression.ScaleToUInt(value, minFloat, maxFloat, minUint, maxUint);
|
||||
float unpacked = Compression.ScaleFromUInt(packed, minFloat, maxFloat, minUint, maxUint);
|
||||
|
||||
Assert.That(unpacked, Is.Not.NaN);
|
||||
Assert.That(unpacked, Is.EqualTo(value).Within(allowedPrecision), $"Value not in Allowed Precision\n value : {value}\n unpacked: {unpacked}");
|
||||
}
|
||||
|
||||
static IEnumerable FloatTestCases
|
||||
{
|
||||
get
|
||||
{
|
||||
// test values in various ranges with different min/max
|
||||
foreach (object value in range(-2.2f, 2.5f, 0.1f, 0, byte.MaxValue))
|
||||
{
|
||||
yield return value;
|
||||
}
|
||||
foreach (object value in range(-2.2f, 2.5f, 0.1f, byte.MaxValue, ushort.MaxValue))
|
||||
{
|
||||
yield return value;
|
||||
}
|
||||
foreach (object value in range(-200f, 200f, Mathf.PI, 0, (1 << 10) - 1))
|
||||
{
|
||||
yield return value;
|
||||
}
|
||||
foreach (object value in range(-0.7f, 0.7f, 0.03f, 0, (1 << 19) - 1))
|
||||
{
|
||||
yield return value;
|
||||
}
|
||||
foreach (object value in range(-0.7f, 0.7f, 0.03f, (1 << 10) - 1, (1 << 20) - 1))
|
||||
{
|
||||
yield return value;
|
||||
}
|
||||
foreach (object value in range(10f, 200f, 2f, 0, (1 << 19) - 1))
|
||||
{
|
||||
yield return value;
|
||||
}
|
||||
|
||||
IEnumerable range(float min, float max, float step, uint uMin, uint uMax)
|
||||
{
|
||||
float precision = (max - min) / (uMax - uMin);
|
||||
for (float f = min; f <= max; f += step)
|
||||
{
|
||||
yield return new TestCaseData(f, min, max, uMin, uMax, precision);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(InvalidRangeTestCases))]
|
||||
public void ShouldNotThrowWhenGivenInvalidValues(float value, float minFloat, float maxFloat, uint minUint, uint maxUint)
|
||||
{
|
||||
uint packed = Compression.ScaleToUInt(value, minFloat, maxFloat, minUint, maxUint);
|
||||
float unpacked = Compression.ScaleFromUInt(packed, minFloat, maxFloat, minUint, maxUint);
|
||||
|
||||
// should not throw
|
||||
}
|
||||
|
||||
static IEnumerable InvalidRangeTestCases
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new TestCaseData(0, 0, 0, 0u, 0u);
|
||||
yield return new TestCaseData(0, 0, 0, 1u, 0u);
|
||||
yield return new TestCaseData(0, 0, 0, 1u, 1u);
|
||||
yield return new TestCaseData(0, 1, -1, 0u, byte.MaxValue);
|
||||
yield return new TestCaseData(0, 1.5f, 1.5f, 0u, byte.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(OutOfRangeFloatTestCases))]
|
||||
public uint ValuesOutOfRangeArePackedInRange(float value, float minFloat, float maxFloat, uint minUint, uint maxUint)
|
||||
{
|
||||
uint packed = Compression.ScaleToUInt(value, minFloat, maxFloat, minUint, maxUint);
|
||||
|
||||
Assert.That(packed, Is.Not.NaN);
|
||||
|
||||
return packed;
|
||||
}
|
||||
static IEnumerable OutOfRangeFloatTestCases
|
||||
{
|
||||
get
|
||||
{
|
||||
float min = -1;
|
||||
float max = 1;
|
||||
uint uMin = 0;
|
||||
uint uMax = 10;
|
||||
for (float f = -2; f <= min; f += 0.1f)
|
||||
{
|
||||
yield return new TestCaseData(f, min, max, uMin, uMax).Returns(0);
|
||||
}
|
||||
for (float f = max; f <= 2; f += 0.1f)
|
||||
{
|
||||
yield return new TestCaseData(f, min, max, uMin, uMax).Returns(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(OutOfRangeUintTestCases))]
|
||||
public float ValuesOutOfRangeAreUnPackedInRange(uint value, float minFloat, float maxFloat, uint minUint, uint maxUint)
|
||||
{
|
||||
float packed = Compression.ScaleFromUInt(value, minFloat, maxFloat, minUint, maxUint);
|
||||
|
||||
Assert.That(packed, Is.Not.NaN);
|
||||
|
||||
return packed;
|
||||
}
|
||||
static IEnumerable OutOfRangeUintTestCases
|
||||
{
|
||||
get
|
||||
{
|
||||
float min = -1;
|
||||
float max = 1;
|
||||
uint uMin = 5;
|
||||
uint uMax = 25;
|
||||
for (uint u = 0; u <= uMin; u++)
|
||||
{
|
||||
yield return new TestCaseData(u, min, max, uMin, uMax).Returns(-1f);
|
||||
}
|
||||
for (uint u = uMax; u <= (uMax + 10u); u++)
|
||||
{
|
||||
yield return new TestCaseData(u, min, max, uMin, uMax).Returns(1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Tests/Editor/CompressionFloatTest.cs.meta
Normal file
11
Assets/Mirror/Tests/Editor/CompressionFloatTest.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af232bfd6f14b824ba8d1782b5ee181a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
145
Assets/Mirror/Tests/Editor/CompressionQuaternionTest.cs
Normal file
145
Assets/Mirror/Tests/Editor/CompressionQuaternionTest.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Tests
|
||||
{
|
||||
public class CompressionQuaternionTest
|
||||
{
|
||||
// worse case where xyzw all equal error in largest is ~1.732 times greater than error in smallest 3
|
||||
// High/Low Precision fails when xyzw all equal,
|
||||
internal const float AllowedPrecision = 0.00138f;
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(QuaternionTestCases))]
|
||||
public void QuaternionCompressIsWithinPrecision(Quaternion inRot)
|
||||
{
|
||||
uint packed = Compression.CompressQuaternion(inRot);
|
||||
|
||||
Quaternion outRot = Compression.DecompressQuaternion(packed);
|
||||
|
||||
Assert.That(outRot.x, Is.Not.NaN, "x was NaN");
|
||||
Assert.That(outRot.y, Is.Not.NaN, "y was NaN");
|
||||
Assert.That(outRot.z, Is.Not.NaN, "z was NaN");
|
||||
Assert.That(outRot.w, Is.Not.NaN, "w was NaN");
|
||||
|
||||
int largest = Compression.FindLargestIndex(inRot);
|
||||
float sign = Mathf.Sign(inRot[largest]);
|
||||
// flip sign of A if largest is is negative
|
||||
// Q == (-Q)
|
||||
|
||||
Assert.AreEqual(sign * inRot.x, outRot.x, AllowedPrecision, $"x off by {Mathf.Abs(sign * inRot.x - outRot.x)}");
|
||||
Assert.AreEqual(sign * inRot.y, outRot.y, AllowedPrecision, $"y off by {Mathf.Abs(sign * inRot.y - outRot.y)}");
|
||||
Assert.AreEqual(sign * inRot.z, outRot.z, AllowedPrecision, $"z off by {Mathf.Abs(sign * inRot.z - outRot.z)}");
|
||||
Assert.AreEqual(sign * inRot.w, outRot.w, AllowedPrecision, $"w off by {Mathf.Abs(sign * inRot.w - outRot.w)}");
|
||||
}
|
||||
|
||||
static IEnumerable QuaternionTestCases
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new TestCaseData(Quaternion.identity);
|
||||
yield return new TestCaseData(new Quaternion(1, 0, 0, 0));
|
||||
yield return new TestCaseData(new Quaternion(0, 1, 0, 0));
|
||||
yield return new TestCaseData(new Quaternion(0, 0, 1, 0));
|
||||
|
||||
yield return new TestCaseData(new Quaternion(1, 1, 0, 0).normalized);
|
||||
yield return new TestCaseData(new Quaternion(0, 1, 1, 0).normalized);
|
||||
yield return new TestCaseData(new Quaternion(0, 1, 1, 0).normalized);
|
||||
yield return new TestCaseData(new Quaternion(0, 0, 1, 1).normalized);
|
||||
|
||||
yield return new TestCaseData(new Quaternion(1, 1, 1, 0).normalized);
|
||||
yield return new TestCaseData(new Quaternion(1, 1, 0, 1).normalized);
|
||||
yield return new TestCaseData(new Quaternion(1, 0, 1, 1).normalized);
|
||||
yield return new TestCaseData(new Quaternion(0, 1, 1, 1).normalized);
|
||||
|
||||
yield return new TestCaseData(new Quaternion(1, 1, 1, 1).normalized);
|
||||
|
||||
yield return new TestCaseData(new Quaternion(-1, 0, 0, 0));
|
||||
yield return new TestCaseData(new Quaternion(0, -1, 0, 0));
|
||||
yield return new TestCaseData(new Quaternion(0, 0, -1, 0));
|
||||
yield return new TestCaseData(new Quaternion(0, 0, 0, -1));
|
||||
|
||||
yield return new TestCaseData(new Quaternion(-1, -1, 0, 0).normalized);
|
||||
yield return new TestCaseData(new Quaternion(0, -1, -1, 0).normalized);
|
||||
yield return new TestCaseData(new Quaternion(0, -1, -1, 0).normalized);
|
||||
yield return new TestCaseData(new Quaternion(0, 0, -1, -1).normalized);
|
||||
|
||||
yield return new TestCaseData(new Quaternion(-1, -1, -1, 0).normalized);
|
||||
yield return new TestCaseData(new Quaternion(-1, -1, 0, -1).normalized);
|
||||
yield return new TestCaseData(new Quaternion(-1, 0, -1, -1).normalized);
|
||||
yield return new TestCaseData(new Quaternion(0, -1, -1, -1).normalized);
|
||||
|
||||
yield return new TestCaseData(new Quaternion(-1, -1, -1, -1).normalized);
|
||||
|
||||
yield return new TestCaseData(Quaternion.Euler(200, 100, 10));
|
||||
yield return new TestCaseData(Quaternion.LookRotation(new Vector3(0.3f, 0.4f, 0.5f)));
|
||||
yield return new TestCaseData(Quaternion.Euler(45f, 56f, Mathf.PI));
|
||||
yield return new TestCaseData(Quaternion.AngleAxis(30, new Vector3(1, 2, 5)));
|
||||
yield return new TestCaseData(Quaternion.AngleAxis(5, new Vector3(-1, .01f, 0.44f)));
|
||||
yield return new TestCaseData(Quaternion.AngleAxis(358, new Vector3(0.5f, 2, 5)));
|
||||
yield return new TestCaseData(Quaternion.AngleAxis(-54, new Vector3(1, 2, 5)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(QuaternionTestCases))]
|
||||
public void RotationIsWithinPrecision(Quaternion rotationIn)
|
||||
{
|
||||
uint packed = Compression.CompressQuaternion(rotationIn);
|
||||
|
||||
Quaternion rotationOut = Compression.DecompressQuaternion(packed);
|
||||
|
||||
Assert.That(rotationOut.x, Is.Not.NaN, "x was NaN");
|
||||
Assert.That(rotationOut.y, Is.Not.NaN, "y was NaN");
|
||||
Assert.That(rotationOut.z, Is.Not.NaN, "z was NaN");
|
||||
Assert.That(rotationOut.w, Is.Not.NaN, "w was NaN");
|
||||
|
||||
Vector3 inVec = rotationIn * Vector3.forward;
|
||||
Vector3 outVec = rotationOut * Vector3.forward;
|
||||
// allow for extra precision when rotating vector
|
||||
float precision = AllowedPrecision * 2;
|
||||
|
||||
Assert.AreEqual(inVec.x, outVec.x, precision, $"x off by {Mathf.Abs(inVec.x - outVec.x)}");
|
||||
Assert.AreEqual(inVec.y, outVec.y, precision, $"y off by {Mathf.Abs(inVec.y - outVec.y)}");
|
||||
Assert.AreEqual(inVec.z, outVec.z, precision, $"z off by {Mathf.Abs(inVec.z - outVec.z)}");
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(LargestIndexTestCases))]
|
||||
public void FindLargestIndexWork(Quaternion quaternion, int expected)
|
||||
{
|
||||
int largest = Compression.FindLargestIndex(quaternion);
|
||||
|
||||
Assert.That(largest, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
static IEnumerable LargestIndexTestCases
|
||||
{
|
||||
get
|
||||
{
|
||||
// args = Quaternion quaternion, int expected
|
||||
yield return new TestCaseData(new Quaternion(1, 0, 0, 0), 0);
|
||||
yield return new TestCaseData(new Quaternion(0, 1, 0, 0), 1);
|
||||
yield return new TestCaseData(new Quaternion(0, 0, 1, 0), 2);
|
||||
yield return new TestCaseData(new Quaternion(0, 0, 0, 1), 3);
|
||||
|
||||
yield return new TestCaseData(new Quaternion(-1, 0, 0, 0), 0);
|
||||
yield return new TestCaseData(new Quaternion(0, -1, 0, 0), 1);
|
||||
yield return new TestCaseData(new Quaternion(0, 0, -1, 0), 2);
|
||||
yield return new TestCaseData(new Quaternion(0, 0, 0, -1), 3);
|
||||
|
||||
yield return new TestCaseData(new Quaternion(1, 0, 0.5f, 0).normalized, 0);
|
||||
yield return new TestCaseData(new Quaternion(0, 1, 0.5f, 0).normalized, 1);
|
||||
yield return new TestCaseData(new Quaternion(0, 0.5f, 1, 0).normalized, 2);
|
||||
yield return new TestCaseData(new Quaternion(0, 0.5f, 0, 1).normalized, 3);
|
||||
|
||||
yield return new TestCaseData(new Quaternion(-1, 0.9f, 0.5f, 0).normalized, 0);
|
||||
yield return new TestCaseData(new Quaternion(0.9f, -1, 0.5f, 0).normalized, 1);
|
||||
yield return new TestCaseData(new Quaternion(0, 0.5f, -1, 0.9f).normalized, 2);
|
||||
yield return new TestCaseData(new Quaternion(0, 0.5f, 0.9f, -1).normalized, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Tests/Editor/CompressionQuaternionTest.cs.meta
Normal file
11
Assets/Mirror/Tests/Editor/CompressionQuaternionTest.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9df82db27e550f49888f01b737e9cc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Reference in New Issue
Block a user