Skip to content

Commit

Permalink
feat(Compression): Vector4Long for Quaternion Delta compression (V1) (#…
Browse files Browse the repository at this point in the history
…3907)

Co-authored-by: mischa <info@noobtuts.com>
  • Loading branch information
miwarnec and mischa authored Oct 7, 2024
1 parent b0e60a8 commit 2277d66
Show file tree
Hide file tree
Showing 8 changed files with 449 additions and 2 deletions.
40 changes: 40 additions & 0 deletions Assets/Mirror/Core/Tools/Compression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,38 @@ 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)
{
quantized = Vector3Long.zero;
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)
Expand All @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions Assets/Mirror/Core/Tools/DeltaCompression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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);
}
}
}
126 changes: 126 additions & 0 deletions Assets/Mirror/Core/Tools/Vector4Long.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions Assets/Mirror/Core/Tools/Vector4Long.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions Assets/Mirror/Tests/Editor/Tools/CompressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down
68 changes: 66 additions & 2 deletions Assets/Mirror/Tests/Editor/Tools/DeltaCompressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,13 +123,77 @@ 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
NetworkReader reader = new NetworkReader(writer);
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));
}
}
}
Loading

0 comments on commit 2277d66

Please sign in to comment.