Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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