Fractional bits in a packed rotation
A unit quaternion is four numbers whose squares add up to one. Ken Shoemake brought them into computer graphics in 1985 to store and interpolate rotations, and they now turn up wherever a rotation has to be kept. An animated skeleton stores one for every joint in every keyframe. A mesh can store the tangent frame of each vertex as one, for normal mapping. A Gaussian splatting scene gives each of its millions of Gaussians an orientation, and that is a quaternion too. Memory and bandwidth, more than arithmetic, are what hold these programs back. A byte saved on one rotation is saved millions of times.
The four numbers are not independent. Once three are known, the fourth is fixed up to its sign. A compressor therefore stores three numbers and a small index, called a chart, that says how to rebuild the fourth. The usual choice is the smallest-three method: drop the component that is largest in magnitude, and record which one it was and its sign. ozz-animation works this way, and so do the glTF extension EXT_meshopt_compression and the splat formats SPZ and SOG.
In Compression of Uniformly Distributed Unit Quaternions, posted to arXiv in October 2026, Carsten Faber, Bastian Kuth, Rinaldo Schuster, Daniel Pauls and Quirin Meyer, of Coburg University of Applied Sciences and Arts and of AMD, divide by the largest component instead. That puts the rotation on the surface of a four-dimensional cube. A cubic warp, a·x3 + (1 − a)·x with a = 0.373964, then spreads the samples more evenly than the plain cube would. The authors call the result ℓ∞cub. Here it is the cube codec. Four choices of largest component times two signs make eight charts, and eight charts fit in three bits.
Bits that will not split by three
Put such a rotation in a 32-bit word. The chart takes three bits and leaves 29 for three numbers, about 9.67 bits each. Bit fields deal in whole bits. Each number gets 9 bits, which is 512 levels, and two bits of the word sit idle. The other way out is to give one number 10 bits and the rest fewer, so that one axis of the rotation is stored more finely than the others.
The paper packs the word as a single integer in a mixed-radix number system, where each digit has its own base. The chart is a digit in base 8. Each of the three numbers is a digit in base U. Packing multiplies by U and adds, three times over. Unpacking takes a remainder and divides, three times, and what is left at the end is the chart. The largest U for which 8·U3 still fits under 232 is 812. Every axis gets 812 levels, not 512, and the gap between neighbouring samples shrinks to about 0.63 of the bit-field gap. Bastian Kuth and Quirin Meyer had used the same packing in 2021 for vertex blend weights. The authors compare it to asymmetric numeral systems with a uniform distribution.
What the division costs
Division sounds slow for a decoder. Here every base is a constant, so the compiler turns each divide and remainder into a multiply and a shift. When the bits left after the chart split evenly by three, as at 24 and 48 bits with three chart bits, no integer division is needed at all. The authors timed single decodes with the hardware cycle counter on an AMD Radeon RX 7900 XTX. Their 32-bit decoder took 111 cycles. One spherical linear interpolation between two quaternions took 171 on the same card. The MPEG-4 method, which warps the cube with a tangent, took 273, because the tangent compiles to a sine over a cosine. The cubic warp merges with the step that turns a level back into a number, and the two together cost three fused multiply-adds.
The sign that carries nothing
A quaternion and its negative describe the same rotation. A codec can flip every rotation so that the largest component is positive, and then it never has to write the sign. Some animation libraries do that. ozz-animation does not. It spends 48 bits on a key: 15 bits for each of three numbers, two bits for the chart and one for the sign. Without the sign, 46 bits remain. Three equal bit fields cannot share 46 bits, so the library keeps the sign and its 15 bits per number. As one integer, the same 46 bits give each axis 41,285 levels instead of 32,768, which is 246/3 rounded down. One recovered bit, shared by three axes, is worth a factor of 21/3, about 1.26, on each.
The test set was the Mixamo corpus, all 34,884,287 keyframes of it. The original ozz-animation codec had a largest error of 0.00909°. The cube codec, at the same 15 bits per number with the sign still stored, brought that down to 0.00203°. Dropping the sign brought it to 0.00161°, 5.6 times smaller than where it started. A 40-bit key without the sign reached 0.01020°, 12 percent above the 48-bit original, and saved a byte on every key, 33.3 MiB across the corpus. Renders of a dragon animated both ways showed comparable image error. Sampling 1024 skinned characters ran 12 to 16 percent faster than with the original codec.
A tangent frame is a rotation that may also be mirrored, because texture coordinates can be flipped across a seam. The bit the sign no longer needs can store that handedness. CryEngine's QTangents, presented in 2011, kept four 16-bit components and hid the handedness in the sign of the fourth, nudging that component away from zero so the sign would survive rounding. With the cube codec at 16 bits per frame, eight charts leave 13 bits for three numbers. Bit fields would give each number 16 levels. One integer gives 20, and uses 64,000 of the 65,536 possible codes. The paper reports that as 15.97 bits used.
| What is stored | Bits | Charts | Bit fields | One integer |
|---|---|---|---|---|
| Rotation with its sign | 32 | 8 | 512 | 812 |
| Animation key without the sign | 48 | 4 | 32,768 | 41,285 |
| Animation key without the sign | 40 | 4 | 4,096 | 6,501 |
| Tangent frame with handedness | 16 | 8 | 16 | 20 |
Gaussian splats get the same treatment, since a quaternion and its negative give the same Gaussian. The renderer of Rijsdijk and colleagues packs the geometry of each Gaussian into 128 bits, 30 of them for the rotation, and compresses that rotation with an exponential map. On three Mip-NeRF 360 scenes the exponential map at 30 bits came to 45.90 dB of peak signal-to-noise ratio. The cube codec reached 45.83 dB in 24 bits. At 16 bits it was 6.67 dB ahead. Decoding the rotations took about 0.5 ms a frame either way, against 11.8 ms for the rendering.
Odd counts and the floor
A bit field always holds a power of two levels, which is an even number. The paper's quantizer puts its samples at the centres of equal steps across the range from −1 to 1, so with an even count no sample lands on zero. Zero is where the identity rotation sits, in the middle of its chart, and animations are full of identity rotations. A bit field never brings one back exactly. An odd count puts a sample on zero, and one integer lets the codec pick an odd count. The authors call that trivial to enforce. Their error analysis does not cover it.
There is a floor under all of this. The paper shows that no code with 2k states can keep its largest error below about 96° times 2−k/3. At large budgets the cube codec needs about 1.5 bits more than that bound. The bound assumes rotations spread evenly over every orientation. An animation track that only turns a little can do better with a range fitted to that track, which is what the Animation Compression Library does. Every code here has the same length, unlike the Huffman stage of JPEG compression, so a shader can read any one rotation without decoding the ones before it.
Source
Carsten Faber, Bastian Kuth, Rinaldo Schuster, Daniel Pauls and Quirin Meyer, Compression of Uniformly Distributed Unit Quaternions, arXiv, October 2026, DOI 10.48550/arXiv.2610.22052, published under the CC BY 4.0 licence. Every number, timing and bound in this note is taken from it.
