feat(Compression): Vector4Long for Quaternion Delta compression (V1) (#3907)

Co-authored-by: mischa <info@noobtuts.com>
This commit is contained in:
mischa 2024-10-07 11:31:43 +02:00 committed by GitHub
parent b0e60a8e3d
commit 2277d66cea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 449 additions and 2 deletions

View File

@ -71,6 +71,24 @@ public static bool ScaleToLong(Vector3 value, float precision, out long x, out l
return result;
}
// returns
// 'true' if scaling was possible within 'long' bounds.
// 'false' if clamping was necessary.
// never throws. checking result is optional.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ScaleToLong(Quaternion value, float precision, out long x, out long y, out long z, out long w)
{
// attempt to convert every component.
// do not return early if one conversion returned 'false'.
// the return value is optional. always attempt to convert all.
bool result = true;
result &= ScaleToLong(value.x, precision, out x);
result &= ScaleToLong(value.y, precision, out y);
result &= ScaleToLong(value.z, precision, out z);
result &= ScaleToLong(value.w, precision, out w);
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ScaleToLong(Vector3 value, float precision, out Vector3Long quantized)
{
@ -78,6 +96,13 @@ public static bool ScaleToLong(Vector3 value, float precision, out Vector3Long q
return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ScaleToLong(Quaternion value, float precision, out Vector4Long quantized)
{
quantized = Vector4Long.zero;
return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z, out quantized.w);
}
// multiple by precision.
// for example, 0.1 cm precision converts '50' long to '5.0f' float.
public static float ScaleToFloat(long value, float precision)
@ -103,10 +128,25 @@ public static Vector3 ScaleToFloat(long x, long y, long z, float precision)
return v;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Quaternion ScaleToFloat(long x, long y, long z, long w, float precision)
{
Quaternion v;
v.x = ScaleToFloat(x, precision);
v.y = ScaleToFloat(y, precision);
v.z = ScaleToFloat(z, precision);
v.w = ScaleToFloat(w, precision);
return v;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 ScaleToFloat(Vector3Long value, float precision) =>
ScaleToFloat(value.x, value.y, value.z, precision);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Quaternion ScaleToFloat(Vector4Long value, float precision) =>
ScaleToFloat(value.x, value.y, value.z, value.w, precision);
// scale a float within min/max range to an ushort between min/max range
// note: can also use this for byte range from byte.MinValue to byte.MaxValue
public static ushort ScaleFloatToUShort(float value, float minValue, float maxValue, ushort minTarget, ushort maxTarget)

View File

@ -26,6 +26,16 @@ public static void Compress(NetworkWriter writer, Vector3Long last, Vector3Long
Compress(writer, last.z, current.z);
}
// delta (usually small), then zigzag varint to support +- changes
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Compress(NetworkWriter writer, Vector4Long last, Vector4Long current)
{
Compress(writer, last.x, current.x);
Compress(writer, last.y, current.y);
Compress(writer, last.z, current.z);
Compress(writer, last.w, current.w);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3Long Decompress(NetworkReader reader, Vector3Long last)
{
@ -34,5 +44,15 @@ public static Vector3Long Decompress(NetworkReader reader, Vector3Long last)
long z = Decompress(reader, last.z);
return new Vector3Long(x, y, z);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4Long Decompress(NetworkReader reader, Vector4Long last)
{
long x = Decompress(reader, last.x);
long y = Decompress(reader, last.y);
long z = Decompress(reader, last.z);
long w = Decompress(reader, last.w);
return new Vector4Long(x, y, z, w);
}
}
}

View File

@ -0,0 +1,126 @@
#pragma warning disable CS0659 // 'Vector4Long' overrides Object.Equals(object o) but does not override Object.GetHashCode()
#pragma warning disable CS0661 // 'Vector4Long' defines operator == or operator != but does not override Object.GetHashCode()
// Vector4Long by mischa (based on game engine project)
using System;
using System.Runtime.CompilerServices;
namespace Mirror
{
public struct Vector4Long
{
public long x;
public long y;
public long z;
public long w;
public static readonly Vector4Long zero = new Vector4Long(0, 0, 0, 0);
public static readonly Vector4Long one = new Vector4Long(1, 1, 1, 1);
// constructor /////////////////////////////////////////////////////////
public Vector4Long(long x, long y, long z, long w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
// operators ///////////////////////////////////////////////////////////
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4Long operator +(Vector4Long a, Vector4Long b) =>
new Vector4Long(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4Long operator -(Vector4Long a, Vector4Long b) =>
new Vector4Long(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4Long operator -(Vector4Long v) =>
new Vector4Long(-v.x, -v.y, -v.z, -v.w);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4Long operator *(Vector4Long a, long n) =>
new Vector4Long(a.x * n, a.y * n, a.z * n, a.w * n);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4Long operator *(long n, Vector4Long a) =>
new Vector4Long(a.x * n, a.y * n, a.z * n, a.w * n);
// == returns true if approximately equal (with epsilon).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Vector4Long a, Vector4Long b) =>
a.x == b.x &&
a.y == b.y &&
a.z == b.z &&
a.w == b.w;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Vector4Long a, Vector4Long b) => !(a == b);
// NO IMPLICIT System.Numerics.Vector4Long conversion because double<->float
// would silently lose precision in large worlds.
// [i] component index. useful for iterating all components etc.
public long this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
switch (index)
{
case 0: return x;
case 1: return y;
case 2: return z;
case 3: return w;
default: throw new IndexOutOfRangeException($"Vector4Long[{index}] out of range.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
case 2:
z = value;
break;
case 3:
w = value;
break;
default: throw new IndexOutOfRangeException($"Vector4Long[{index}] out of range.");
}
}
}
// instance functions //////////////////////////////////////////////////
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"({x} {y} {z} {w})";
// equality ////////////////////////////////////////////////////////////
// implement Equals & HashCode explicitly for performance.
// calling .Equals (instead of "==") checks for exact equality.
// (API compatibility)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Vector4Long other) =>
x == other.x && y == other.y && z == other.z && w == other.w;
// Equals(object) can reuse Equals(Vector4)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool Equals(object other) =>
other is Vector4Long vector4 && Equals(vector4);
#if UNITY_2021_3_OR_NEWER
// Unity 2019/2020 don't have HashCode.Combine yet.
// this is only to avoid reflection. without defining, it works too.
// default generated by rider
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => HashCode.Combine(x, y, z, w);
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9f69bf6b6d73476ab3f31dd635bb6497
timeCreated: 1726777293

View File

@ -86,6 +86,32 @@ public void ScaleToLong_Vector3_OutOfRange()
Assert.That(z, Is.EqualTo(long.MinValue));
}
[Test]
public void ScaleToLong_Quaternion()
{
// 0, positive, negative
Assert.True(Compression.ScaleToLong(new Quaternion(0, 10.5f, -100.5f, -10.5f), 0.1f, out long x, out long y, out long z, out long w));
Assert.That(x, Is.EqualTo(0));
Assert.That(y, Is.EqualTo(105));
Assert.That(z, Is.EqualTo(-1005));
Assert.That(w, Is.EqualTo(-105));
}
[Test]
public void ScaleToLong_Quaternion_OutOfRange()
{
float precision = 0.1f;
float largest = long.MaxValue / 0.1f;
float smallest = long.MinValue / 0.1f;
// 0, largest, smallest
Assert.False(Compression.ScaleToLong(new Quaternion(0, largest, smallest, 0), precision, out long x, out long y, out long z, out long w));
Assert.That(x, Is.EqualTo(0));
Assert.That(y, Is.EqualTo(long.MaxValue));
Assert.That(z, Is.EqualTo(long.MinValue));
Assert.That(w, Is.EqualTo(0));
}
[Test]
public void ScaleToFloat()
{
@ -130,6 +156,17 @@ public void ScaleToFloat_Vector3()
Assert.That(v.z, Is.EqualTo(-100.5f));
}
[Test]
public void ScaleToFloat_Quaternion()
{
// 0, positive, negative
Quaternion q = Compression.ScaleToFloat(0, 105, -1005, -105, 0.1f);
Assert.That(q.x, Is.EqualTo(0));
Assert.That(q.y, Is.EqualTo(10.5f));
Assert.That(q.z, Is.EqualTo(-100.5f));
Assert.That(q.w, Is.EqualTo(-10.5f));
}
[Test]
public void LargestAbsoluteComponentIndex()
{

View File

@ -59,7 +59,7 @@ public void Compress_Long_Changed_Large()
// two components = 8 bytes changed.
// delta should compress it down a lot.
// 7000 delta should use a few more bytse.
// 7000 delta should use a few more bytes.
Assert.That(writer.Position, Is.EqualTo(3));
// decompress should get original result
@ -123,7 +123,7 @@ public void Compress_Vector3Long_YZChanged_Large()
// two components = 8 bytes changed.
// delta should compress it down a lot.
// 7000 delta should use a few more bytse.
// 7000 delta should use a few more bytes.
Assert.That(writer.Position, Is.EqualTo(5));
// decompress should get original result
@ -131,5 +131,69 @@ public void Compress_Vector3Long_YZChanged_Large()
Vector3Long decompressed = DeltaCompression.Decompress(reader, last);
Assert.That(decompressed, Is.EqualTo(current));
}
[Test]
public void Compress_Vector4Long_Unchanged()
{
NetworkWriter writer = new NetworkWriter();
Vector4Long last = new Vector4Long(1, 2, 3, 4);
Vector4Long current = new Vector4Long(1, 2, 3, 4); // unchanged
// delta compress current against last
DeltaCompression.Compress(writer, last, current);
// nothing changed.
// delta should compress it down a lot.
Assert.That(writer.Position, Is.EqualTo(4));
// decompress should get original result
NetworkReader reader = new NetworkReader(writer);
Vector4Long decompressed = DeltaCompression.Decompress(reader, last);
Assert.That(decompressed, Is.EqualTo(current));
}
[Test]
public void Compress_Vector4Long_ZWChanged()
{
NetworkWriter writer = new NetworkWriter();
Vector4Long last = new Vector4Long(1, 2, 3, 4);
Vector4Long current = new Vector4Long(1, 2, 7, 8); // only 2 components change
// delta compress current against last
DeltaCompression.Compress(writer, last, current);
// two components = 8 bytes changed.
// delta should compress it down a lot.
Assert.That(writer.Position, Is.EqualTo(4));
// decompress should get original result
NetworkReader reader = new NetworkReader(writer);
Vector4Long decompressed = DeltaCompression.Decompress(reader, last);
Assert.That(decompressed, Is.EqualTo(current));
}
[Test]
public void Compress_Vector4Long_ZWChanged_Large()
{
NetworkWriter writer = new NetworkWriter();
Vector4Long last = new Vector4Long(1, 2, 3, 4);
Vector4Long current = new Vector4Long(1, 2, 5, 7000); // only 2 components change
// delta compress current against last
DeltaCompression.Compress(writer, last, current);
// two components = 8 bytes changed.
// delta should compress it down a lot.
// 7000 delta should use a few more bytes.
Assert.That(writer.Position, Is.EqualTo(6));
// decompress should get original result
NetworkReader reader = new NetworkReader(writer);
Vector4Long decompressed = DeltaCompression.Decompress(reader, last);
Assert.That(decompressed, Is.EqualTo(current));
}
}
}

View File

@ -0,0 +1,154 @@
using System;
using NUnit.Framework;
namespace Mirror.Tests.Tools
{
public class Vector4LongTests
{
[Test]
public void Constructor()
{
Vector4Long v = new Vector4Long();
Assert.That(v.x, Is.EqualTo(0));
Assert.That(v.y, Is.EqualTo(0));
Assert.That(v.z, Is.EqualTo(0));
Assert.That(v.w, Is.EqualTo(0));
}
[Test]
public void ConstructorXYZ()
{
Vector4Long v = new Vector4Long(1, 2, 3, 4);
Assert.That(v.x, Is.EqualTo(1));
Assert.That(v.y, Is.EqualTo(2));
Assert.That(v.z, Is.EqualTo(3));
Assert.That(v.w, Is.EqualTo(4));
}
[Test]
public void OperatorAdd()
{
Vector4Long a = new Vector4Long(1, 2, 3, 4);
Vector4Long b = new Vector4Long(2, 3, 4, 5);
Assert.That(a + b, Is.EqualTo(new Vector4Long(3, 5, 7, 9)));
}
[Test]
public void OperatorSubtract()
{
Vector4Long a = new Vector4Long(1, 2, 3, 4);
Vector4Long b = new Vector4Long(-2, -3, -4, -5);
Assert.That(a - b, Is.EqualTo(new Vector4Long(3, 5, 7, 9)));
}
[Test]
public void OperatorInverse()
{
Vector4Long v = new Vector4Long(1, 2, 3, 4);
Assert.That(-v, Is.EqualTo(new Vector4Long(-1, -2, -3, -4)));
}
[Test]
public void OperatorMultiply()
{
Vector4Long a = new Vector4Long(1, 2, 3, 4);
// a * n, n * a are two different operators. test both.
Assert.That(a * 2, Is.EqualTo(new Vector4Long(2, 4, 6, 8)));
Assert.That(2 * a, Is.EqualTo(new Vector4Long(2, 4, 6, 8)));
}
#if UNITY_2021_3_OR_NEWER
[Test]
public void OperatorEquals()
{
// two vectors which are approximately the same
Vector4Long a = new Vector4Long(1, 2, 3, 4);
Vector4Long b = new Vector4Long(1, 2, 3, 4);
Assert.That(a == b, Is.True);
// two vectors which are definitely not the same
Assert.That(a == Vector4Long.one, Is.False);
}
[Test]
public void OperatorNotEquals()
{
// two vectors which are approximately the same
Vector4Long a = new Vector4Long(1, 2, 3, 4);
Vector4Long b = new Vector4Long(1, 2, 3, 4);
Assert.That(a != b, Is.False);
// two vectors which are definitely not the same
Assert.That(a != Vector4Long.one, Is.True);
}
#endif
[Test]
public void OperatorIndexer()
{
Vector4Long a = new Vector4Long(1, 2, 3, 4);
// get
Assert.That(a[0], Is.EqualTo(1));
Assert.That(a[1], Is.EqualTo(2));
Assert.That(a[2], Is.EqualTo(3));
Assert.That(a[3], Is.EqualTo(4));
Assert.Throws<IndexOutOfRangeException>(() =>
{
double _ = a[-1];
});
Assert.Throws<IndexOutOfRangeException>(() =>
{
double _ = a[4];
});
// set
a[0] = -1;
a[1] = -2;
a[2] = -3;
a[3] = -4;
Assert.Throws<IndexOutOfRangeException>(() => { a[-1] = 0; });
Assert.Throws<IndexOutOfRangeException>(() => { a[4] = 0; });
Assert.That(a, Is.EqualTo(new Vector4Long(-1, -2, -3, -4)));
}
[Test]
public void ToStringTest()
{
// should be rounded to :F2
Vector4Long v = new Vector4Long(-10, 0, 42, 3);
Assert.That(v.ToString(), Is.EqualTo("(-10 0 42 3)"));
}
#if UNITY_2021_3_OR_NEWER
[Test]
public void EqualsVector4Long()
{
Assert.That(Vector4Long.one.Equals(Vector4Long.one), Is.True);
Assert.That(Vector4Long.one.Equals(Vector4Long.zero), Is.False);
}
[Test]
public void EqualsObject()
{
Assert.That(Vector4Long.one.Equals((object)42), Is.False);
Assert.That(Vector4Long.one.Equals((object)Vector4Long.one), Is.True);
Assert.That(Vector4Long.one.Equals((object)Vector4Long.zero), Is.False);
}
[Test]
public void GetHashCodeTest()
{
// shouldn't be 0
Assert.That(Vector4Long.zero.GetHashCode(), !Is.EqualTo(0));
Assert.That(Vector4Long.one.GetHashCode(), !Is.EqualTo(0));
// should be same for same vector
Assert.That(Vector4Long.zero.GetHashCode(), Is.EqualTo(Vector4Long.zero.GetHashCode()));
// should be different for different vectors
Assert.That(Vector4Long.zero.GetHashCode(), !Is.EqualTo(Vector4Long.one.GetHashCode()));
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a966f5fd1b865460abc471e46bcee889
timeCreated: 1666897728