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; 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ScaleToLong(Vector3 value, float precision, out Vector3Long quantized) 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); 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. // multiple by precision.
// for example, 0.1 cm precision converts '50' long to '5.0f' float. // for example, 0.1 cm precision converts '50' long to '5.0f' float.
public static float ScaleToFloat(long value, float precision) 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; 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 ScaleToFloat(Vector3Long value, float precision) => public static Vector3 ScaleToFloat(Vector3Long value, float precision) =>
ScaleToFloat(value.x, value.y, value.z, 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 // 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 // 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) 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); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3Long Decompress(NetworkReader reader, Vector3Long last) 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); long z = Decompress(reader, last.z);
return new Vector3Long(x, y, 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)); 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] [Test]
public void ScaleToFloat() public void ScaleToFloat()
{ {
@ -130,6 +156,17 @@ public void ScaleToFloat_Vector3()
Assert.That(v.z, Is.EqualTo(-100.5f)); 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] [Test]
public void LargestAbsoluteComponentIndex() public void LargestAbsoluteComponentIndex()
{ {

View File

@ -59,7 +59,7 @@ public void Compress_Long_Changed_Large()
// two components = 8 bytes changed. // two components = 8 bytes changed.
// delta should compress it down a lot. // 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)); Assert.That(writer.Position, Is.EqualTo(3));
// decompress should get original result // decompress should get original result
@ -123,7 +123,7 @@ public void Compress_Vector3Long_YZChanged_Large()
// two components = 8 bytes changed. // two components = 8 bytes changed.
// delta should compress it down a lot. // 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)); Assert.That(writer.Position, Is.EqualTo(5));
// decompress should get original result // decompress should get original result
@ -131,5 +131,69 @@ public void Compress_Vector3Long_YZChanged_Large()
Vector3Long decompressed = DeltaCompression.Decompress(reader, last); Vector3Long decompressed = DeltaCompression.Decompress(reader, last);
Assert.That(decompressed, Is.EqualTo(current)); 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