Mirror Networking
Compression.cs
1// Quaternion compression from DOTSNET
2using System;
3using System.Runtime.CompilerServices;
4using UnityEngine;
5
6namespace Mirror
7{
9 public static class Compression
10 {
11 // quaternion compression //////////////////////////////////////////////
12 // smallest three: https://gafferongames.com/post/snapshot_compression/
13 // compresses 16 bytes quaternion into 4 bytes
14
15 // helper function to find largest absolute element
16 // returns the index of the largest one
17 public static int LargestAbsoluteComponentIndex(Vector4 value, out float largestAbs, out Vector3 withoutLargest)
18 {
19 // convert to abs
20 Vector4 abs = new Vector4(Mathf.Abs(value.x), Mathf.Abs(value.y), Mathf.Abs(value.z), Mathf.Abs(value.w));
21
22 // set largest to first abs (x)
23 largestAbs = abs.x;
24 withoutLargest = new Vector3(value.y, value.z, value.w);
25 int largestIndex = 0;
26
27 // compare to the others, starting at second value
28 // performance for 100k calls
29 // for-loop: 25ms
30 // manual checks: 22ms
31 if (abs.y > largestAbs)
32 {
33 largestIndex = 1;
34 largestAbs = abs.y;
35 withoutLargest = new Vector3(value.x, value.z, value.w);
36 }
37 if (abs.z > largestAbs)
38 {
39 largestIndex = 2;
40 largestAbs = abs.z;
41 withoutLargest = new Vector3(value.x, value.y, value.w);
42 }
43 if (abs.w > largestAbs)
44 {
45 largestIndex = 3;
46 largestAbs = abs.w;
47 withoutLargest = new Vector3(value.x, value.y, value.z);
48 }
49
50 return largestIndex;
51 }
52
53 // scale a float within min/max range to an ushort between min/max range
54 // note: can also use this for byte range from byte.MinValue to byte.MaxValue
55 public static ushort ScaleFloatToUShort(float value, float minValue, float maxValue, ushort minTarget, ushort maxTarget)
56 {
57 // note: C# ushort - ushort => int, hence so many casts
58 // max ushort - min ushort only fits into something bigger
59 int targetRange = maxTarget - minTarget;
60 float valueRange = maxValue - minValue;
61 float valueRelative = value - minValue;
62 return (ushort)(minTarget + (ushort)(valueRelative / valueRange * targetRange));
63 }
64
65 // scale an ushort within min/max range to a float between min/max range
66 // note: can also use this for byte range from byte.MinValue to byte.MaxValue
67 public static float ScaleUShortToFloat(ushort value, ushort minValue, ushort maxValue, float minTarget, float maxTarget)
68 {
69 // note: C# ushort - ushort => int, hence so many casts
70 float targetRange = maxTarget - minTarget;
71 ushort valueRange = (ushort)(maxValue - minValue);
72 ushort valueRelative = (ushort)(value - minValue);
73 return minTarget + (valueRelative / (float)valueRange * targetRange);
74 }
75
76 const float QuaternionMinRange = -0.707107f;
77 const float QuaternionMaxRange = 0.707107f;
78 const ushort TenBitsMax = 0x3FF;
79
80 // helper function to access 'nth' component of quaternion
81 [MethodImpl(MethodImplOptions.AggressiveInlining)]
82 static float QuaternionElement(Quaternion q, int element)
83 {
84 switch (element)
85 {
86 case 0: return q.x;
87 case 1: return q.y;
88 case 2: return q.z;
89 case 3: return q.w;
90 default: return 0;
91 }
92 }
93
94 // note: assumes normalized quaternions
95 public static uint CompressQuaternion(Quaternion q)
96 {
97 // note: assuming normalized quaternions is enough. no need to force
98 // normalize here. we already normalize when decompressing.
99
100 // find the largest component index [0,3] + value
101 int largestIndex = LargestAbsoluteComponentIndex(new Vector4(q.x, q.y, q.z, q.w), out float _, out Vector3 withoutLargest);
102
103 // from here on, we work with the 3 components without largest!
104
105 // "You might think you need to send a sign bit for [largest] in
106 // case it is negative, but you don’t, because you can make
107 // [largest] always positive by negating the entire quaternion if
108 // [largest] is negative. in quaternion space (x,y,z,w) and
109 // (-x,-y,-z,-w) represent the same rotation."
110 if (QuaternionElement(q, largestIndex) < 0)
111 withoutLargest = -withoutLargest;
112
113 // put index & three floats into one integer.
114 // => index is 2 bits (4 values require 2 bits to store them)
115 // => the three floats are between [-0.707107,+0.707107] because:
116 // "If v is the absolute value of the largest quaternion
117 // component, the next largest possible component value occurs
118 // when two components have the same absolute value and the
119 // other two components are zero. The length of that quaternion
120 // (v,v,0,0) is 1, therefore v^2 + v^2 = 1, 2v^2 = 1,
121 // v = 1/sqrt(2). This means you can encode the smallest three
122 // components in [-0.707107,+0.707107] instead of [-1,+1] giving
123 // you more precision with the same number of bits."
124 // => the article recommends storing each float in 9 bits
125 // => our uint has 32 bits, so we might as well store in (32-2)/3=10
126 // 10 bits max value: 1023=0x3FF (use OSX calc to flip 10 bits)
127 ushort aScaled = ScaleFloatToUShort(withoutLargest.x, QuaternionMinRange, QuaternionMaxRange, 0, TenBitsMax);
128 ushort bScaled = ScaleFloatToUShort(withoutLargest.y, QuaternionMinRange, QuaternionMaxRange, 0, TenBitsMax);
129 ushort cScaled = ScaleFloatToUShort(withoutLargest.z, QuaternionMinRange, QuaternionMaxRange, 0, TenBitsMax);
130
131 // now we just need to pack them into one integer
132 // -> index is 2 bit and needs to be shifted to 31..32
133 // -> a is 10 bit and needs to be shifted 20..30
134 // -> b is 10 bit and needs to be shifted 10..20
135 // -> c is 10 bit and needs to be at 0..10
136 return (uint)(largestIndex << 30 | aScaled << 20 | bScaled << 10 | cScaled);
137 }
138
139 // Quaternion normalizeSAFE from ECS math.normalizesafe()
140 // => useful to produce valid quaternions even if client sends invalid
141 // data
142 [MethodImpl(MethodImplOptions.AggressiveInlining)]
143 static Quaternion QuaternionNormalizeSafe(Quaternion value)
144 {
145 // The smallest positive normal number representable in a float.
146 const float FLT_MIN_NORMAL = 1.175494351e-38F;
147
148 Vector4 v = new Vector4(value.x, value.y, value.z, value.w);
149 float length = Vector4.Dot(v, v);
150 return length > FLT_MIN_NORMAL
151 ? value.normalized
152 : Quaternion.identity;
153 }
154
155 // note: gives normalized quaternions
156 public static Quaternion DecompressQuaternion(uint data)
157 {
158 // get cScaled which is at 0..10 and ignore the rest
159 ushort cScaled = (ushort)(data & TenBitsMax);
160
161 // get bScaled which is at 10..20 and ignore the rest
162 ushort bScaled = (ushort)((data >> 10) & TenBitsMax);
163
164 // get aScaled which is at 20..30 and ignore the rest
165 ushort aScaled = (ushort)((data >> 20) & TenBitsMax);
166
167 // get 2 bit largest index, which is at 31..32
168 int largestIndex = (int)(data >> 30);
169
170 // scale back to floats
171 float a = ScaleUShortToFloat(aScaled, 0, TenBitsMax, QuaternionMinRange, QuaternionMaxRange);
172 float b = ScaleUShortToFloat(bScaled, 0, TenBitsMax, QuaternionMinRange, QuaternionMaxRange);
173 float c = ScaleUShortToFloat(cScaled, 0, TenBitsMax, QuaternionMinRange, QuaternionMaxRange);
174
175 // calculate the omitted component based on a²+b²+c²+d²=1
176 float d = Mathf.Sqrt(1 - a*a - b*b - c*c);
177
178 // reconstruct based on largest index
179 Vector4 value;
180 switch (largestIndex)
181 {
182 case 0: value = new Vector4(d, a, b, c); break;
183 case 1: value = new Vector4(a, d, b, c); break;
184 case 2: value = new Vector4(a, b, d, c); break;
185 default: value = new Vector4(a, b, c, d); break;
186 }
187
188 // ECS Rotation only works with normalized quaternions.
189 // make sure that's always the case here to avoid ECS bugs where
190 // everything stops moving if the quaternion isn't normalized.
191 // => NormalizeSafe returns a normalized quaternion even if we pass
192 // in NaN from deserializing invalid values!
193 return QuaternionNormalizeSafe(new Quaternion(value.x, value.y, value.z, value.w));
194 }
195
196 // varint compression //////////////////////////////////////////////////
197 // compress ulong varint.
198 // same result for int, short and byte. only need one function.
199 // NOT an extension. otherwise weaver might accidentally use it.
200 public static void CompressVarUInt(NetworkWriter writer, ulong value)
201 {
202 if (value <= 240)
203 {
204 writer.WriteByte((byte)value);
205 return;
206 }
207 if (value <= 2287)
208 {
209 writer.WriteByte((byte)(((value - 240) >> 8) + 241));
210 writer.WriteByte((byte)((value - 240) & 0xFF));
211 return;
212 }
213 if (value <= 67823)
214 {
215 writer.WriteByte((byte)249);
216 writer.WriteByte((byte)((value - 2288) >> 8));
217 writer.WriteByte((byte)((value - 2288) & 0xFF));
218 return;
219 }
220 if (value <= 16777215)
221 {
222 writer.WriteByte((byte)250);
223 writer.WriteByte((byte)(value & 0xFF));
224 writer.WriteByte((byte)((value >> 8) & 0xFF));
225 writer.WriteByte((byte)((value >> 16) & 0xFF));
226 return;
227 }
228 if (value <= 4294967295)
229 {
230 writer.WriteByte((byte)251);
231 writer.WriteByte((byte)(value & 0xFF));
232 writer.WriteByte((byte)((value >> 8) & 0xFF));
233 writer.WriteByte((byte)((value >> 16) & 0xFF));
234 writer.WriteByte((byte)((value >> 24) & 0xFF));
235 return;
236 }
237 if (value <= 1099511627775)
238 {
239 writer.WriteByte((byte)252);
240 writer.WriteByte((byte)(value & 0xFF));
241 writer.WriteByte((byte)((value >> 8) & 0xFF));
242 writer.WriteByte((byte)((value >> 16) & 0xFF));
243 writer.WriteByte((byte)((value >> 24) & 0xFF));
244 writer.WriteByte((byte)((value >> 32) & 0xFF));
245 return;
246 }
247 if (value <= 281474976710655)
248 {
249 writer.WriteByte((byte)253);
250 writer.WriteByte((byte)(value & 0xFF));
251 writer.WriteByte((byte)((value >> 8) & 0xFF));
252 writer.WriteByte((byte)((value >> 16) & 0xFF));
253 writer.WriteByte((byte)((value >> 24) & 0xFF));
254 writer.WriteByte((byte)((value >> 32) & 0xFF));
255 writer.WriteByte((byte)((value >> 40) & 0xFF));
256 return;
257 }
258 if (value <= 72057594037927935)
259 {
260 writer.WriteByte((byte)254);
261 writer.WriteByte((byte)(value & 0xFF));
262 writer.WriteByte((byte)((value >> 8) & 0xFF));
263 writer.WriteByte((byte)((value >> 16) & 0xFF));
264 writer.WriteByte((byte)((value >> 24) & 0xFF));
265 writer.WriteByte((byte)((value >> 32) & 0xFF));
266 writer.WriteByte((byte)((value >> 40) & 0xFF));
267 writer.WriteByte((byte)((value >> 48) & 0xFF));
268 return;
269 }
270
271 // all others
272 {
273 writer.WriteByte((byte)255);
274 writer.WriteByte((byte)(value & 0xFF));
275 writer.WriteByte((byte)((value >> 8) & 0xFF));
276 writer.WriteByte((byte)((value >> 16) & 0xFF));
277 writer.WriteByte((byte)((value >> 24) & 0xFF));
278 writer.WriteByte((byte)((value >> 32) & 0xFF));
279 writer.WriteByte((byte)((value >> 40) & 0xFF));
280 writer.WriteByte((byte)((value >> 48) & 0xFF));
281 writer.WriteByte((byte)((value >> 56) & 0xFF));
282 }
283 }
284
285 // zigzag encoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba
286 [MethodImpl(MethodImplOptions.AggressiveInlining)]
287 public static void CompressVarInt(NetworkWriter writer, long i)
288 {
289 ulong zigzagged = (ulong)((i >> 63) ^ (i << 1));
290 CompressVarUInt(writer, zigzagged);
291 }
292
293 // NOT an extension. otherwise weaver might accidentally use it.
294 public static ulong DecompressVarUInt(NetworkReader reader)
295 {
296 byte a0 = reader.ReadByte();
297 if (a0 < 241)
298 {
299 return a0;
300 }
301
302 byte a1 = reader.ReadByte();
303 if (a0 <= 248)
304 {
305 return 240 + ((a0 - (ulong)241) << 8) + a1;
306 }
307
308 byte a2 = reader.ReadByte();
309 if (a0 == 249)
310 {
311 return 2288 + ((ulong)a1 << 8) + a2;
312 }
313
314 byte a3 = reader.ReadByte();
315 if (a0 == 250)
316 {
317 return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16);
318 }
319
320 byte a4 = reader.ReadByte();
321 if (a0 == 251)
322 {
323 return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24);
324 }
325
326 byte a5 = reader.ReadByte();
327 if (a0 == 252)
328 {
329 return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32);
330 }
331
332 byte a6 = reader.ReadByte();
333 if (a0 == 253)
334 {
335 return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40);
336 }
337
338 byte a7 = reader.ReadByte();
339 if (a0 == 254)
340 {
341 return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40) + (((ulong)a7) << 48);
342 }
343
344 byte a8 = reader.ReadByte();
345 if (a0 == 255)
346 {
347 return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40) + (((ulong)a7) << 48) + (((ulong)a8) << 56);
348 }
349
350 throw new IndexOutOfRangeException($"DecompressVarInt failure: {a0}");
351 }
352
353 // zigzag decoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba
354 [MethodImpl(MethodImplOptions.AggressiveInlining)]
355 public static long DecompressVarInt(NetworkReader reader)
356 {
357 ulong data = DecompressVarUInt(reader);
358 return ((long)(data >> 1)) ^ -((long)data & 1);
359 }
360 }
361}
Functions to Compress Quaternions and Floats
Definition: Compression.cs:10
Network Reader for most simple types like floats, ints, buffers, structs, etc. Use NetworkReaderPool....
Network Writer for most simple types like floats, ints, buffers, structs, etc. Use NetworkWriterPool....