3D Model Compression 2: Introduction to rANS

2025-11-21

3D Model Compression 2: Introduction to rANS

Hi, I am Yadokoro from Eukarya.

This is the second in the series of articles on 3D graphics compression techniques, and it will be about the ranged Asymmetric Numerical System.

The ranged Asymmetric Numerical System, or rANS, is a type of entropy codec โ€” a compression algorithm that compresses strings using the probability distribution of their symbols.

Compared to other entropy codecs, rANS is known for its speed and compression ratio: it matches the speed of Huffman coding while providing guaranteed optimality for a given probability distribution.

Polish computer scientist J. Duda introduced the algorithm in 2013 as part of a series of papers on a new family of entropy codecs called asymmetric numerical systems.

The rANS codec quickly became one of the most widely used compression algorithms due to its mathematically guaranteed optimality in compression ratio and computational efficiency.

In fact, it's used by

  1. Draco (3D mesh codec by Google)
  2. JPEG XL, AV1 (image/video codec)
  3. Opus (audio codec)
  4. Zstd and LZFSE (general compression algorithms by Meta and Apple, respectively)

โ€ฆto name a few.

It is also known to be embedded in fundamental systems like operating systems and browsers. Yes, rANS is much more widely used than you might think โ€” even now, as you read this article, an rANS coder is likely at work behind the screen of your device!

Although rANS is a general compression technique rather than a 3D model compression technique, we're including it in our journey through 3D model compression algorithms because these algorithms often make heavy use of rANS codecs.

For example, in the first article of the series, we looked at the Edgebreaker algorithm โ€” an algorithm that converts mesh connectivity into a string of symbols. Since rANS can compress strings, we can feed the resulting string to an rANS coder to achieve a better compression ratio.

Moreover, rANS can also compress attribute values such as vertex coordinates and texture coordinates.

In this article, we will cover the fundamentals of the rANS algorithm.

๐Ÿ’ก At Eukarya, we're developing draco-oxide, a Rust rewrite of Google's Draco 3D model compression library. Check it out if you're interested!

The rANS Algorithm

Settings and Notations

Suppose for a finite set SS of alphabets. We fix a strict ordering << of SS.

We may know the probability distribution p:Sโ†’[0,1]p: S \to [0,1] of these alphabets so that โˆ‘sโˆˆSp(s)=1\sum_{s \in S} p(s)=1.

Now for a fixed positive integer MM, we come up with the discretized probability distribution P:Sโ†’NP:S \to \N in the sense that โˆ‘sโˆˆSP(s)=M\sum_{s \in S} P(s) = M and P(s)โˆผp(s)MP(s) \sim p(s)M for each sโˆˆSs \in S.

Furthermore, let C(s)=โˆ‘tโˆˆS:t<sP(t)C(s) = \sum_{t\in S:t < s} P(t). We call C(s)C(s) the offset to the range of ss.

Figure 1: An example of a setting for a rANS coder with the set S={a,b,c,d} (in this order) of four symbols, and the discretized probability ditribution is given by P (a)=4, P (b)=3, and so on. This means that the probability the alphabet a shows up at a random index of the input string is about P(a) / M * 100 = 40%. Offsets to ranges are also computed in the figure. From this, we see a has its range from 0 to (but not including) 4, b has its range from 4 to (but not including) 7, and so on.
Figure 1: An example of a setting for a rANS coder with the set S={a,b,c,d} (in this order) of four symbols, and the discretized probability ditribution is given by P (a)=4, P (b)=3, and so on. This means that the probability the alphabet a shows up at a random index of the input string is about P(a) / M * 100 = 40%. Offsets to ranges are also computed in the figure. From this, we see a has its range from 0 to (but not including) 4, b has its range from 4 to (but not including) 7, and so on.

Encoding

Now we present the encoding formula. Suppose we want to encode a sequence (s1,s2,โ‹ฏโ€‰,sm)(s_1, s_2, \cdots, s_m) of symbols from SS, where mm is the sequence length.

We set the initial state X0=0X_0=0. Then rANS recursively computes the state values (X1,X2,โ‹ฏโ€‰,Xm)(X_1, X_2, \cdots, X_m), and the final integer XmX_m will be our encoded data.

Here's how each recursive step works: Let E:Zร—Sโ†’ZE:\Z\times S \to \Z denote the function representing the encoding step, so that the encoding step can be written as Xi=E(Xiโˆ’1,si)X_i = E(X_{i-1},s_i) for each time step iโˆˆ{1,โ‹ฏโ€‰,m}i \in \{1,\cdots, m\}.

We occasionally write Xi=Es(Xiโˆ’1)X_i = E_s(X_{i-1}) when sโˆˆSs\in S is fixed.

First, compute the quotient qq and remainder rr from the Euclidean division of Xiโˆ’1X_{i-1} by P(si)P(s_i). That is, find positive integers qq and r<P(si)r<P(s_i) such that

Xiโˆ’1=P(si)q+r.\begin{align} X_{i-1} = P(s_i) q + r. \end{align}

Then XiX_i is computed by the formula:

Xi=E(Xiโˆ’1,s)=qM+C(si)+r\begin{align} X_i = E(X_{i-1},s) = q M + C(s_i) + r \end{align}

The value qq represents a compressed version of the previous state Xiโˆ’1X_{i-1}.

When integers are represented in the MM-ary (or MM-decimal) number system, multiplying by MM shifts each digit one place to the left and inserts a zero in the least significant position, thereby creating space to encode another integer from the set {0,โ€ฆ,Mโˆ’1}\{0,\dots,M-1\} โ€” this is where the range information is encoded.

We first encode the offset C(si)C(s_i) to specify which range we're using, then encode the value rr, which is guaranteed to lie within the range since r<P(si)r < P(s_i).

The total range information C(si)+rC(s_i) + r is also bounded above by MM due to the design of the ranges.

Decoding

Now we give the formula for the decompression algorithm. Decoding a rANS-encoded data involves reversing the encoding process, that is, the symbol that is encoded at last will be decoded first.

Given the state value XiX_i after encoding ii symbols, we need to recover sis_i and then compute Xiโˆ’1X_{i-1}.

Similarly to the case of encoding, we denote by D:Zโ†’Zร—SD:\Z\to\Z\times S the decoding function, i.e., D(Xi)=(Xiโˆ’1,si)D(X_i) = (X_{i-1},s_i) for each time step iโˆˆ{1,โ‹ฏโ€‰,m}i\in\{1,\cdots,m\}.

We will first compute the Euclidean division of XiX_i by MM, that is, we compute positive integers QQ and R<MR<M such that

Xi=MQ+R.X_i = M Q + R.

Since RR is a positive integer less than MM, there must be a unique symbol siโˆˆSs_i\in S whose range contains RR, that is,

si=maxโก{s:C(s)โ‰คR}s_{i} = \max \left\{s : C(s) \leq R \right \}

This can be efficiently implemented using a lookup table that maps each value in {0,1,โ‹ฏโ€‰,Mโˆ’1}\{0,1,\cdots,M-1\} to a symbol whose range contains it.

Once we have identified sis_i, we can compute Xiโˆ’1X_{i-1} by reversing the encoding operation. Note that we have the equations

R=C(si)+rQ=q\begin{align} R &= C(s_i)+r \\ Q &= q \end{align}

where qq and rr are the values defined in the encoding function EE in the previous section.

Rewriting the variables qq and rr from equation (1) using equations (3) and (4) gives the final decoding formula:

Xiโˆ’1=P(si)Q+Rโˆ’C(si).X_{i-1} = P(s_i)Q + R - C(s_i).

The decoding process continues recursively until all symbols until we hit Xi=0X_i = 0.

Note that decoding proceeds in reverse order compared to encoding. If we encoded (s1,s2,...,sm)(s_1, s_2, ..., s_m) in this order, then the symbols are decoded in the order (sm,smโˆ’1,โ‹ฏโ€‰,s1)(s_m,s_{m-1},\cdots,s_1).

Example

Letโ€™s do a quick example. Suppose that we have the setting of Figure 1 and that we have encoded up to X5=691X_{5}=691.

Our job now is to encode the 6th symbol, say, โ€œcโ€. Following the arithmetic, we see that P(c)=2P(c) = 2, q=345q = 345, r=1r= 1, so we obtain X6=qM+C(c)+r=3458X_6=qM+C(c)+r=3458.

Fine, but how can we decode the state X5X_5 and the symbol โ€œcโ€ back from X6X_6?

Since C(c)+rC(c)+r is less than MM, dividing X6X_6 by MM will give the quotient Q=q=345Q=q=345 and the remainder R=8R=8.

To decode the symbol and the remainder rr from RR, see Figure 2. Because C(c)โ‰คR<C(d)C(c) \leq R < C(d) (concretely, 7โ‰ค8<97\leq 8 < 9), we know that RR lies in the range of the symbol โ€œccโ€. Also, we see that RR has value 11 in the range of โ€œcโ€, so we figure out r=1r=1.

Thus we obtain X5=P(c)q+r=691X_{5}=P(c) q + r=691.

Figure 2: An example step of the algorithm using the setting of Figure 1.
Figure 2: An example step of the algorithm using the setting of Figure 1.

The Streaming rANS Algorithm

The previous section introduced the rANS codec, whose main idea is to create a single large integer containing all information about the string.

While the rANS coder achieves optimal entropy coding for a given probability distribution, it quickly becomes impractical as string size increases โ€” we must perform Euclidean division on increasingly large integers, which is computationally expensive.

To resolve this issue, we decompose each resulting integer XiX_i to keep it reasonably small.

This is accomplished by the streaming variant of the algorithm, called the streaming rANS algorithm, which we introduce in this section.

Here's how the streaming rANS coder works:

Streaming Encoding

We choose positive integers kk and ll.

The number kk indicates how many bits each transfer sends in the stream โ€” set this to 11 for the simplest implementation, or to 88 for a byte-stream.

The number ll is an algorithm parameter; a higher value of ll makes the algorithm slower but improves compression efficiency.

In this section, we will present a streaming variant where, at each time step, the state value lies between lMlM and 2klMโˆ’12^k lM-1, i.e., we make sure that XiโˆˆIX_i \in I with:

I={lM,โ‹ฏโ€‰,2klMโˆ’1}.I=\{lM,\cdots,2^k l M-1\}.

To keep the state value inside II at each time step, we simply divide the state value Xiโˆ’1X_{i-1} by 2k2^k whenever E(Xiโˆ’1,s)E(X_{i-1},s) would go out of range II.

Ok, but how can we know when the state value is about to overflow? Said another way, what state value XX and a symbol sโˆˆSs \in S will be such that E(X,s)โˆˆIE(X,s) \in I?

To answer this question, we need to know what the set IsI_s defined by

Is=Esโˆ’1(I)I_s=E_s^{-1}(I)

looks like.

One fact useful to compute this set is that the function EsE_s is monotonically increasing, that is, as the input gets bigger and bigger, so does the output.

Indeed, the two numbers LsL_s and HsH_s with Ls=minโก{L:Es(L)โ‰ฅlM}L_s = \min \{L: E_s(L)\geq lM \} and Hs=maxโก{H:Es(H)โ‰ค2klMโˆ’1}H_s = \max\{H:E_s(H)\leq2^klM-1\} are all we need to know, as the monotonicity will tell us that any number XX with Lsโ‰คXโ‰คHsL_s \leq X \leq H_s will be in IsI_s as well.

So what are the values of LsL_s and HsH_s? This is in fact a great exercise; if you have extra time, I recommend you close the article now and try to find the answers by yourself.

โ€ฆwelcome back! We should have now arrived at

Is={Ls,โ‹ฏโ€‰,Hs}={lP(s),โ‹ฏโ€‰,2klP(s)โˆ’1}.I_s=\{L_s,\cdots, H_s\} = \{lP(s),\cdots,2^k lP(s)-1\}.

This clean formula for IsI_s is in fact thanks to the design of II! Thus we now know that if the state value is inside IsI_s when trying to encode ss, then the resulting state value XiX_i would never overflow.

We are finally ready to explain the algorithm. Given the state Xiโˆ’1X_{i-1}, a symbol sis_i can be encoded by

whileย Xiโˆ’1โˆˆฬธIsi:โ€…โ€Šโ€…โ€Šโ€…โ€Šโ€…โ€Šoutputย Xiโˆ’1modโ€‰โ€‰2kโ€…โ€Šโ€…โ€Šโ€…โ€Šโ€…โ€ŠXiโˆ’1โ†โŒŠXiโˆ’1/2kโŒ‹Xi=E(si,Xiโˆ’1)\begin{align} &\text{while } X_{i-1} \not \in I_{s_i} : \\ &\;\;\;\;\text{output } X_{i-1} \mod 2^k \\ &\;\;\;\;X_{i-1} \leftarrow \lfloor X_{i-1} / 2^k \rfloor \\ &X_i = E(s_i,X_{i-1}) \end{align}

One question that remains is: how can we be sure that the while loop terminates in a finite step? Isnโ€™t there a case that division by 2k2^k overshoots the IsiI_{s_i}?

Well, the division by 2k2^k indeed never overshoots the interval IsiI_{s_i}, thanks to the design of II.

If there is a state value that overshoots the interval IsiI_{s_i}, then there must be an integer XX with IsiโŠŠ{X,โ‹ฏโ€‰,2kX}I_{s_i}\subsetneq\{X,\cdots,2^kX\}.

This means that Xโ‰คlP(si)X \leq lP(s_i) and X>lP(si)X > lP(s_i), or that X<lP(si)X < lP(s_i) and Xโ‰ฅlP(si)X \geq lP(s_i), but either case can never happen.

Streaming Decoding

For decoding, we perform the reverse operations. We start with the last encoded state XmX_m.

At each time step iโˆˆ{1,โ‹ฏโ€‰,m}i\in\{1,\cdots,m\}, we start by decoding a symbol sis_i and Xiโˆ’1X_{i-1} from XiX_i using the standard rANS decoding process explained in the previous section.

After decoding a symbol, if the state Xiโˆ’1X_{i-1} falls below the lower bound kMkM of II, we need to read in additional kk-bits from the encoded bitstream:

(Xiโˆ’1,s)=D(Xi)whileย Xiโˆ’1<kM:โ€…โ€Šโ€…โ€Šโ€…โ€Šโ€…โ€ŠXiโˆ’1โ†Xiโˆ’1โ‹…2n+nextย kย bitsย fromย bitstream\begin{align} &(X_{i-1},s) = D(X_i)\\ &\text{while } X_{i-1} < kM: \\ &\;\;\;\;X_{i-1} \leftarrow X_{i-1} \cdot 2^n + \text{next $k$ bits from bitstream} \end{align}

You may be wondering whether the number of kk-bit transfers made during encoding might differ from that during decoding, causing the decoding to fail.

That is, we stop reading the kk-bit transfers once XiโˆˆIX_i \in I, but isnโ€™t it too early? โ€” isnโ€™t it possible that 2kXiโˆˆI2^kX_i\in I as well?

Well, once again, the design of II guarantees that there is no integer XX such that both XX and 2kX2^k X are in II.

This in turn guarantees that the number of transfers is uniquely determined, so the algorithm does not encounter the ambiguous situation described above.

That is all! We now have a practical rANS algorithm.

Introduction to tANS Algorithm

In this section, we will take a peek at another variant from the ANS family that is closely related to rANS.

In the previous section, we saw the streaming variant of the rANS coder, which limits the state value XiX_i to a certain range at each time step ii.

Since E(X,s)E(X,s) is a one-to-one mapping, it means only a finite number of inputs are fed to E(X,s)E(X,s) throughout the entire rANS encoding process.

More specifically, in the streaming variant of rANS, it is sufficient to define E(X,s)E(X,s) for sโˆˆSs \in S and Xโˆˆ{lP(s),โ‹ฏโ€‰,2klP(s)โˆ’1}X \in \{lP(s),\cdots,2^klP(s)-1\} โ€” there are only finitely many such pairs (X,s)(X,s).

The tANS algorithm (short for tabled Asymmetric Numerical System) is yet another variant of the ANS algorithm that improves speed by creating a table of E(X,s)E(X,s) at the beginning of the encoding/decoding process.

The table reduces the computations of E(X,s)E(X,s), which include Euclidean divisions and integer multiplications, to a single table lookup.

The challenge lies in the table creation step. We could compute all values of E(X,s)E(X,s), but this would defeat the purpose of tANS, which aims to avoid these computations.

Jarek Duda's paper [1] presents a method for creating a table for E(X,s)E(X,s) without explicitly computing it, but it is way beyond the scope of this article, so let us stop here for this article.

Conclusion

In this article, we've explored the rANS (ranged Asymmetric Numerical System) algorithm and its streaming variant, which serve as powerful tools for data compression in 3D graphics applications.

We've covered both the theoretical foundations and practical implementations of these algorithms.

The basic rANS coder provides optimal entropy coding based on symbol probabilities, but it faces practical limitations due to the growing size of the state value.

The streaming rANS algorithm elegantly solves this problem by introducing a mechanism to bound the state value, making it computationally efficient while preserving compression effectiveness.

We've also briefly touched on tANS (table-based ANS), another variant of the ANS family that offers different trade-offs between compression ratio and computational complexity.

These entropy coding techniques are crucial in modern 3D model compression pipelines. When combined with other techniques like quantization, prediction, and transform coding, they enable significant reductions in file size while maintaining visual quality.

References

  1. Duda, J. (2013). Asymmetric numeral systems: entropy coding combining speed of Huffman coding with compression rate of arithmetic coding. arXiv preprint arXiv:1311.2540. https://arxiv.org/pdf/1311.2540
English

Eukaryaใงใฏๆง˜ใ€…ใช่ท็จฎใงๆŽก็”จใ‚’่กŒใฃใฆใ„ใพใ™๏ผOSSใซใ‚ณใƒณใƒˆใƒชใƒ“ใƒฅใƒผใƒˆใ—ใฆใ„ใŸใ ใ‘ใ‚‹็š†ๆง˜ใ‹ใ‚‰ใฎๅฟœๅ‹Ÿใ‚’ใŠๅพ…ใกใ—ใฆใŠใ‚Šใพใ™๏ผ

โž” Eukarya ๆŽก็”จใƒšใƒผใ‚ธ

Eukarya is hiring for various positions! We are looking forward to your application from everyone who can contribute to OSS!

โž” Eukarya Careers

Eukaryaใฏใ€Re:Earthใจๅ‘ผใฐใ‚Œใ‚‹WebGISใฎSaaSใฎ้–‹็™บ้‹ๅ–ถใƒป็ ”็ฉถ้–‹็™บใ‚’่กŒใฃใฆใ„ใพใ™ใ€‚WebไธŠใง3Dใ‚’ๅซใ‚€GIS๏ผˆๅœฐๅ›ณใ‚ขใƒ—ใƒชใฎๅ…ฌ้–‹ใ€ใƒ‡ใƒผใ‚ฟ็ฎก็†ใ€ใƒ‡ใƒผใ‚ฟๅค‰ๆ›็ญ‰๏ผ‰ใซ้–ขใ™ใ‚‹ใ‚ใ‚‰ใ‚†ใ‚‹ๆฅญๅ‹™ใ‚’ๅฎŒ็ตใงใใ‚‹ใ“ใจใ‚’็›ฎๆŒ‡ใ—ใฆใ„ใพใ™ใ€‚ใ‚ฝใƒผใ‚นใ‚ณใƒผใƒ‰ใฏใปใจใ‚“ใฉOSSใจใ—ใฆGitHubใงๅ…ฌ้–‹ใ•ใ‚Œใฆใ„ใพใ™ใ€‚

โž” Re:Earth / โž” Eukarya / โž” note / โž” GitHub

Eukarya is developing and operating a WebGIS SaaS called Re:Earth. We aim to complete all GIS-related tasks including 3D (such as publishing map applications, data management, and data conversion) on the web. Most of the source code is published on GitHub as OSS.

โž” Re:Earth / โž” Eukarya / โž” Medium / โž” GitHub