August 15, 2024

This plugins offers means to perform (lossy) compression (and decompression) of floating point numbers. And means to compress (and restore) rotation quaternions using the *smallest three* method. All functions of this class are *static*, meaning that no instance of it is required.

This class was originally created to help compress data related to object orientation for networking purposes. In several use cases the introduced error is small enough to not pose a problem. While *storing* the full float should not be a problem in modern days, this kind of compression is still useful to help reduce bandwidth requirements in networking code.

The functionality is accessed through the `Quantize`

class. The important thing to keep in mind is that the expected range of the number must be known, otherwise the compression will fail.

Now suppose we have a number that is in range [0..1]. We can compress it using the `quantize_unit_float()`

function. It requires two arguments, which are the value to be compressed and the number of bits to be used in the compression. The number of bits must be positive and at most 32. Depending on how Godot is compiled it doesn't make much sense to use 32 bits to compress the float number, as it *might* be the exact same amount of bits used by the float itself. That said, this function returns an integer. Note that in Godot (GDScript) the value returned is a full integer (32 or 64 bits) plus the Variant header. In this state it's not exactly useful, but the remaining bits can be safely discarded or maybe used to incorporate additional quantized floats through bit masking.

To restore a float that was originally in the [0..1] range and compressed with `quantize_unit_float()`

there is the `restore_unit_float()`

function. It requires the quantized integer data and the number of bits used to compress the original number. The return value is a float that should be close enough to the original one.

In practice it looks like this:

```
# Quantize a float in range [0..1] using 10 bits
var quantized: int = Quantize.quantize_unit_float(0.45, 10)
# ... some code
# Restore that quantized float
var restored: float = Quantize.restore_unit_float(quantized, 10)
```

In this case `restored = 0.449658`

. It does incorporate an error, which becomes smaller if the number of bits is increased.

What about different ranges? For that, there is the `quantize_float()`

function, which requires four arguments. The first one is the number to be compressed. Then there are the two ends of the range (minimum and maximum values, respectively). The last argument is the number of bits to be used in the compression. Much like `quantize_unit_float()`

, an integer is returned.

To restore that float there is the `restore_float()`

function, which also requires four arguments. First the integer containing the compressed data. Then the range. Finally the number of bits used to compress. Remember that minimum, maximum and bit count *must the exact same* used during the compression.

In practice, suppose we want to compress a number that is in range [-1..1], using 16 bits this time:

```
# Quantize a float in range [-1..1] using 16 bits
var quantized: int = Quantize.quantize_float(-0.35, -1.0, 1.0, 16)
# ... some code
# Restore the quantized float
var restored: float = Quantize.restore_float(quantized, -1.0, 1.0, 16)
```

This should result in `restored = -0.349996`

. That is basically all for simple floating point number quantization (compression)!

Then, as mentioned in the introduction, the plugin offers means to compress and restore rotation quaternions. The compression uses the *smallest three* method, which consists in finding the biggest component in the quaternion, "dropping" it then compressing the three remaining components. This information is required to understand the returned data used during the compression. What exactly happens behind the scenes is not required to understand and use this functionality.

First there is a "general" function, `compress_rotation_quat()`

. It requires two arguments, the quaternion to be compressed and the number of bits per component. The return value is a `Dictionary`

containing the following fields:

`a`

,`b`

and`c`

: those are the remaining compressed components. Basically each one stores the return value of`quantize_float()`

.`index`

: indicate which component of the quaternion was "dropped", (`0 = x`

,`1 = y`

,`2 = z`

and`3 = w`

).`sig`

: while not entirely necessary, indicate the original signal of the component that got "dropped" (1 = positive, 0 = negative).

To restore a rotation quaternion the `restore_rotation_quat()`

function is provided. This requires two arguments. The first one is the `Dictionary`

containing the quantized data, as it was returned by `compress_rotation_quat()`

. The second argument indicates the number of bits used to compress the rotation quaternion. The function returns the restored `Quaternion`

.

In practice, suppose we have a rotation quaternion named `rquat`

and want to compress it using 10 bits per component.

```
# Compress a rotation quaternion using 10 bits per component
var compressed: Dictionary = Quantize.compress_rotation_quat(rquat, 10)
# ... in here we could pack the components using bit masking, discarding unused bits
# Restore the quaternion
var restored: Quaternion = Quantize.restore_rotation_quat(compressed, 10)
```

Again the returned `Dictionary`

might not be very useful. Indeed, each field of it is using a full variant object, representing a full integer. However this is meant to serve as intermediary data. The compression only becomes useful when the result is packed into integers using bit masking.

To facilitate this packing, there are 3 "wrappers" to compress rotation quaternions. Those use 9, 10 or 15 bits per component. The first two cases result in data that can be packed into a single integer, which is the return value of those two cases. However 15 bits per component requires two integers to store the data. To that end the function returns a `PackedIntArray`

containing two integers, one that is fully used and the other that can discard 16 bits. The mentioned functions are `compress_rquat_9bits()`

, `compress_rquat_10bits()`

and `compress_rquat_15bits()`

. In all 3 functions a single argument is required, which is the original rotation quaternion.

Then, to restore those quaternions, `restore_rquat_9bits()`

, `restore_rquat_10bits()`

and `restore_rquat_15bits()`

. In the first two functions a single argument is required, which should match the returned value from the corresponding compression functions. For the 15 bits case however, two arguments are required, which should match the two elements of the array returned by the compression function. In practice:

```
# Compress a rotation quaternion using 9 bits per component
var compressed9: int = Quantize.compress_rquat_9bits(rquat)
# Compress a rotation quaternion using 10 bits per component
var compressed10: int = Quantize.compress_rquat_10bits(rquat)
# Compress a rotation quaternion using 15 bits per component - element [1] can discard 16 bits
var compressed15: PackedIntArray = Quantize.compress_rquat_15bits(rquat)
# Restore a rotation quaternion that was compressed using 9 bits per component
var restored9: Quaternion = Quantize.restore_rquat_9bits(compressed9)
# Restore a rotation quaternion that was compressed using 10 bits per component
var restored10: Quaternion = Quantize.restore_rquat_10bits(compressed10)
# Restore a rotation quaternion that was compressed using 15 bits per component
var restored15: Quaternion = Quantize.restore_rquat_15bits(compressed15[0], compressed15[1])
```

- Please note that the compression of quaternions will fail if those are not representing rotations. Or more specifically, if the quaternion is not a "unit". Unit quaternions are those in which the length equals to one.
- Rotation quaternions can be flipped. That is $q = -q$, and they will still represent the exact same orientation. As part of the compression process, it's possible the signal of the components will be flipped in order to obtain the correct orientation. However, while not entirely necessary, the 9 bits and the 15 bits compression functions use the bits that are still "free" to encode the original signal and restore them. However, the 10 bits compression does not have room for this and occasionally will result in "flipper quaternions". Again, they represent the same orientation but this information is somewhat necessary in case you want to directly compare restored quaternions against the original ones.
- As mentioned multiple times, the returned data from most of the quantization functions will be rather useless if directly used. The thing is, those will still use the "full integer" bits, not to mention they are returned as GDScript variants, containing extra 4 bytes (the headers). However bit masking can be used to pack/store the values as desired.