<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><title>Fernando García</title><subtitle>Computer graphics, embedded, C++, Rust, Emacs and more</subtitle><link rel="self" type="application/atom+xml" href="https://fgarlin.com/atom.xml"/><link rel="alternate" type="text/html" href="https://fgarlin.com"/><generator uri="https://www.getzola.org/">Zola</generator><updated>2026-01-09T00:00:00+00:00</updated><id>https://fgarlin.com/atom.xml</id><entry xml:lang="en"><title>The quest for the "best" random numbers on the GPU</title><published>2025-01-17T00:00:00+00:00</published><updated>2025-01-17T00:00:00+00:00</updated><author><name>
Fernando García Liñán</name></author><link rel="alternate" type="text/html" href="https://fgarlin.com/blog/gpu-rng/"/><id>https://fgarlin.com/blog/gpu-rng/</id><content type="html" xml:base="https://fgarlin.com/blog/gpu-rng/">&lt;p&gt;While working with Perlin noise for procedural terrain generation, I noticed that the resulting terrain exhibited some unpleasant repeating patterns. It didn’t take me long to realize that the culprit was the infamous one-liner we have all been guilty of using at some point to get quick and dirty random numbers in shaders:&lt;/p&gt;
&lt;pre data-lang="glsl" style="background-color:#1c1e1f;color:#d6d6d4;" class="language-glsl "&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;float &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;rand&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;vec2 &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;co&lt;/span&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;return &lt;/span&gt;&lt;span style="color:#66d9ef;"&gt;fract&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef;"&gt;sin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef;"&gt;dot&lt;/span&gt;&lt;span&gt;(co.xy, &lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;vec2&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;12.9898&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;78.233&lt;/span&gt;&lt;span&gt;))) &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;* &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;43758.5453&lt;/span&gt;&lt;span&gt;);
&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function conveniently returns a floating point value between 0 and 1 for a given 2D coordinate. However, its behavior is machine dependent due to the implementation of the sine function being a black box and variable among vendors. It also tends to produce moiré patterns and breaks down when the input is large enough. So, can we do better? Are there more robust techniques that offer better quality with a similar (or even lower) performance cost?&lt;/p&gt;
&lt;h2 id="what-we-are-looking-for"&gt;What we are looking for&lt;a class="zola-anchor" href="#what-we-are-looking-for" aria-label="Anchor link for: what-we-are-looking-for"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;We are mainly interested in generating random numbers for the purpose of computer graphics. The literature on pseudorandom number generators (PRNGs) clearly distinguishes between cryptographically secure and non-cryptographic generators. We care about the latter ones, because we are only interested in producing random-looking output, and don’t care too much about the statistical properties of the generated numbers. In contrast, performance is a high priority - the chosen algorithm must have a similar cost as the infamous one-liner.&lt;/p&gt;
&lt;p&gt;Interestingly, achieving random-looking output is not straightforward. The human brain is great at recognizing patterns, and many common PRNGs fail when doing a simple 2D plot of their output. So, even if we are not aiming for statistical quality, we might need to keep it in mind anyway.&lt;/p&gt;
&lt;p&gt;Another restriction I imposed to myself is that the chosen approach must be self-contained and implemented entirely on GPU hardware, i.e. doing texture lookups is not a valid option. A single texture would not provide a sufficient amount of random values for many applications, and it can also be argued that modern GPUs handle multiple ALU instructions more efficiently than a single texture lookup.&lt;/p&gt;
&lt;p&gt;The terms “wide” and “deep”, which to my knowledge were first coined by Nathan Reed in &lt;a href="https://www.reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11/#wide-and-deep"&gt;this blog post&lt;/a&gt;, are also something to take into account. PRNGs are designed to work by going “deep”: a single instance of the PRNG is initialized with a fixed seed, and its last output is used as the seed for the next call. These generators rely on sequentially updating their internal state after each call to produce “randomness”, so they do not yield good results when only the first number in the sequence is used, particularly if the initial seeds are highly correlated. This is specially problematic on the GPU, since we cannot share state between threads&lt;sup class="footnote-reference" id="fr-1-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;, and we must be able to use adjacent initial seeds. For example, in GPGPU programming, the invocation id is usually used as the initial seed. For shaders in the graphics pipeline, any value that is unique to the shader invocation is used, such as the vertex or pixel position, UV coordinate, instance id, etc.&lt;/p&gt;
&lt;p&gt;Hash functions, on the other hand, are stateless and designed to go “wide”. They do not depend on a single instance that updates its state after each sequential call, and produce random values that are well-distributed when using neighboring seeds. Unlike PRNGs, a hash function allows us to independently generate a random number for each shader invocation that is sufficiently random with respect to the ones produced by other invocations, and without sharing any state.&lt;/p&gt;
&lt;h2 id="what-are-our-options"&gt;What are our options&lt;a class="zola-anchor" href="#what-are-our-options" aria-label="Anchor link for: what-are-our-options"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;So we need to come up with a hash function. Coming up with one from scratch is a bit pointless since there are many existing ones to choose from. Ideally, we are looking for a hash function that has the following properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Very performant and simple (as few instructions as possible).&lt;/li&gt;
&lt;li&gt;Good distribution: there shouldn’t be a tendency for certain outputs to occur more frequently than others when given a uniform distribution of inputs.&lt;/li&gt;
&lt;li&gt;Good avalanche behavior, i.e. whenever a single input bit is flipped, each of the output bits changes with a 50% probability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A great starting point is &lt;a href="https://jcgt.org/published/0009/03/02/"&gt;this great 2020 paper&lt;/a&gt; by Mark Jarzynski and Marc Olano. Quoting the paper:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[…] many of the GPU hashes in use today have only appeared in blog posts or shadertoy code, and have never been formally evaluated or compared to other alternatives. This leaves anyone looking for a good GPU hash with little guidance to decide which to use.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Indeed, the state-of-the-art regarding GPU hash functions seems to be scattered across different sources. The paper provides a useful baseline to work with, and any of the recommended hashes can be a good choice. A specially popular one, probably because of &lt;a href="https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/"&gt;Nathan Reed’s article&lt;/a&gt;, is the 32-bit PCG hash.&lt;/p&gt;
&lt;p&gt;I also came across &lt;a href="https://nullprogram.com/blog/2018/07/31/"&gt;this amazing article by Chris Wellons&lt;/a&gt;, which describes a clever Monte Carlo-based approach to find the best candidate from a pool of billions of potential integer hash functions. Essentially, the program has a list of possible operations that serve as the building blocks of the hash function, and randomly combines five (or a configurable amount) of them to evaluate their resulting avalanche behavior.&lt;/p&gt;
&lt;p&gt;Although it uses pure brute force to find good hashes, the prospector was quite successful, and found that the construction “XOR-right-shift, multiply, XOR-right-shift, multiply, XOR-right-shift” delivered pretty good results. This aligns with more &lt;a href="https://www.pcg-random.org/posts/developing-a-seed_seq-alternative.html#multiplyxorshift"&gt;theoretical/empirical findings&lt;/a&gt; which claim that the multiply-XORshift technique creates good avalanche. &lt;a href="https://github.com/skeeto/hash-prospector/issues/19"&gt;TheIronBorn used some optimization techniques&lt;/a&gt; to discover the best parameters currently known for the XORshift-multiply-XORshift construction that minimize the bias. This is the resulting hash, which will be referred to as lowbias32 from now on:&lt;/p&gt;
&lt;pre data-lang="glsl" style="background-color:#1c1e1f;color:#d6d6d4;" class="language-glsl "&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;hash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;x&lt;/span&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;^=&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;16&lt;/span&gt;&lt;span&gt;;
&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;*= &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;0x21F0AAADu&lt;/span&gt;&lt;span&gt;;
&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;^=&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;15&lt;/span&gt;&lt;span&gt;;
&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;*= &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;0xD35A2D97u&lt;/span&gt;&lt;span&gt;;
&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;^=&lt;/span&gt;&lt;span&gt; x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;15&lt;/span&gt;&lt;span&gt;;
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;return&lt;/span&gt;&lt;span&gt; x;
&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It might be worth mentioning that a user named fp64 described &lt;a href="https://github.com/skeeto/hash-prospector/issues/23"&gt;another potential operation called “xorsquare”&lt;/a&gt;, which might yield better theoretical results at the cost of a bit of performance. I decided to ignore it for now since the current “best” construction seems to have enough quality for all my use-cases.&lt;/p&gt;
&lt;h2 id="hash-comparison"&gt;Hash comparison&lt;a class="zola-anchor" href="#hash-comparison" aria-label="Anchor link for: hash-comparison"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Choosing between lowbias32 and other, more theoretically-based hashes is mostly a matter of personal preference.&lt;sup class="footnote-reference" id="fr-2-1"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt; When comparing their output visually, they all have sufficient quality that it’s very hard (at least for me) to find any obvious patterns. Still, for completion, I used &lt;a href="https://pracrand.sourceforge.net/"&gt;PractRand&lt;/a&gt; to compare several common 32-bit integer hashes. PractRand, which seems to be the state-of-the-art when it comes to PRNG statistical testing, reads a random stream of bytes from standard input and performs statistical tests on it. Quoting an &lt;a href="https://www.pcg-random.org/posts/pcg-passes-practrand.html"&gt;article by the PCG author&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Much like experiments in other fields of science, these tests typically produce a p-value, but whereas scientists usually desire results where the null hypothesis—that the observations were merely due to chance—is ruled out, we desire the opposite: a result confirming that the observed behavior is consistent with chance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If PractRand detects that the observed output is not consistent with chance, it means that there is a clear bias in the PRNG algorithm, and the test fails. The number of bytes read up to that point indicates the “strength” of the PRNG algorithm, so reading more bytes usually means that the PRNG has a higher statistical quality. I also calculated the bias of each hash function using the hash prospector. Bias measures the correlation between which output bits flip for a particular flipped input bit, so lower is better.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;32-bit Hash Function&lt;/th&gt;&lt;th&gt;PractRand&lt;/th&gt;&lt;th&gt;Bias&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a href="https://www.shadertoy.com/view/llGSzw"&gt;iqint1&lt;/a&gt;&lt;/td&gt;&lt;td&gt;1KB&lt;/td&gt;&lt;td&gt;704.6363…&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a href="https://xxhash.com"&gt;xxHash&lt;/a&gt;&lt;/td&gt;&lt;td&gt;16KB&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a href="https://www.burtleburtle.net/bob/hash/integer.html"&gt;Wang Hash&lt;/a&gt;&lt;/td&gt;&lt;td&gt;16KB&lt;/td&gt;&lt;td&gt;36.00093…&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a href="https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp"&gt;MurmurHash3&lt;/a&gt;&lt;/td&gt;&lt;td&gt;16KB&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a href="https://nullprogram.com/blog/2018/07/31/"&gt;lowbias32 (with TheIronBorn’s parameters)&lt;/a&gt;&lt;/td&gt;&lt;td&gt;64KB&lt;/td&gt;&lt;td&gt;0.107602…&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a href="https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/"&gt;PCG (version from Jarzynski and Olano)&lt;/a&gt;&lt;/td&gt;&lt;td&gt;4MB&lt;/td&gt;&lt;td&gt;7.511839…&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Some popular hashes have not been considered for the table above because they have very poor visual quality.&lt;/p&gt;
&lt;p&gt;PractRand was designed with PRNGs in mind, not hash functions. Since hash functions lack an internal state, we use a counter as the input, with the additional advantage that it allows us to check how the hash performs with adjacent seeds. It’s important to note that chaining the hash calls, i.e. using the previous output as the seed for the next call, would yield better PractRand results. I use the following C code to output the random stream for PractRand:&lt;/p&gt;
&lt;pre data-lang="c" style="background-color:#1c1e1f;color:#d6d6d4;" class="language-c "&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="color:#fb2874;"&gt;#include &lt;/span&gt;&lt;span style="color:#e2c770;"&gt;&amp;lt;stdio.h&amp;gt;
&lt;/span&gt;&lt;span style="color:#fb2874;"&gt;#include &lt;/span&gt;&lt;span style="color:#e2c770;"&gt;&amp;lt;stdint.h&amp;gt;
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint32_t &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;hash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint32_t &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;x&lt;/span&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#999999;"&gt;/* Hash to test */
&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;int &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;main&lt;/span&gt;&lt;span&gt;() {
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint32_t&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;= &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;0&lt;/span&gt;&lt;span&gt;;
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;while &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;1&lt;/span&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint32_t&lt;/span&gt;&lt;span&gt; random &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;= &lt;/span&gt;&lt;span&gt;hash(i&lt;/span&gt;&lt;span style="color:#fb2874;"&gt;++&lt;/span&gt;&lt;span&gt;);
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#66d9ef;"&gt;fwrite&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;void&lt;/span&gt;&lt;span style="color:#fb2874;"&gt;*&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color:#fb2874;"&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;random, &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;sizeof&lt;/span&gt;&lt;span&gt;(random), &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;1&lt;/span&gt;&lt;span&gt;, stdout);
&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;return &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;0&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span style="color:#999999;"&gt;/* Never reached */
&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In terms of performance, they cannot be compared fairly. PCG and lowbias32 take almost the same time, while MurmurHash3 takes longer. This is to be expected: the Xorshift-multiply scheme used by lowbias32 serves as the final mix operation in MurmurHash3, meaning MurmurHash3 will always perform worse due to having more operations.&lt;/p&gt;
&lt;p&gt;I personally decided to go for lowbias32. The bias of this hash function is significantly better than the alternatives, while also performing well-enough in PractRand. This makes sense as the hash prospector was explicitly optimizing for low bias, while PCG isn’t. PCG seemed to be perform better in formal statistical testing, but I wanted to prioritize a low bias rather than statistical robustness. It is also &lt;em&gt;really&lt;/em&gt; fast, as it only involves 2 integer multiplications, 3 bit shifts, and 3 bitwise XORs.&lt;/p&gt;
&lt;h2 id="input-dimensionality"&gt;Input dimensionality&lt;a class="zola-anchor" href="#input-dimensionality" aria-label="Anchor link for: input-dimensionality"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The methods we have discussed so far accept just one dimension of input (a single 32-bit unsigned integer). There are multidimensional hash functions that can handle vectors (multiple 32-bit integers) for both input and output. These functions intertwine their inputs to ensure that a change in any input component affects all outputs.&lt;/p&gt;
&lt;p&gt;For now, let’s focus on N -&amp;gt; 1 hash functions, i.e. functions that convert N-dimensional input to a single output. These functions are useful when our seed is a 2D or 3D vector, like a vertex position or a UV coordinate. A common way to convert a 1 -&amp;gt; 1 hash function to N -&amp;gt; 1 is to do a linear combination of the input components using a prime multiplier on each dimension:&lt;/p&gt;
&lt;script type="math/tex;mode=display"&gt;\text{hash}(X \cdot a + Y \cdot b + Z),&lt;/script&gt;
&lt;p&gt;where &lt;script type="math/tex"&gt;X&lt;/script&gt;
, &lt;script type="math/tex"&gt;Y&lt;/script&gt;
, &lt;script type="math/tex"&gt;Z&lt;/script&gt;
are the input components, and &lt;script type="math/tex"&gt;a&lt;/script&gt;
and &lt;script type="math/tex"&gt;b&lt;/script&gt;
are large prime numbers.&lt;/p&gt;
&lt;p&gt;This method works fine in general, but the output might exhibit some repeating patterns depending on the chosen prime, and the general quality of the hash suffers greatly, as shown by the &lt;a href="https://jcgt.org/published/0009/03/02/"&gt;Jarzynski and Olano paper&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;An alternative method, first shown by Perlin on his classic 1985 paper&lt;sup class="footnote-reference" id="fr-3-1"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;, involves nesting the hash:&lt;/p&gt;
&lt;script type="math/tex;mode=display"&gt;\text{hash}(X + \text{hash}(Y + \text{hash}(Z))),&lt;/script&gt;
&lt;p&gt;where &lt;script type="math/tex"&gt;X&lt;/script&gt;
, &lt;script type="math/tex"&gt;Y&lt;/script&gt;
, &lt;script type="math/tex"&gt;Z&lt;/script&gt;
are the input components.&lt;/p&gt;
&lt;p&gt;While this method avoids the quality issues of the linear combination approach, it requires multiple calls to the hash function, which negatively impacts performance. This may not be a problem if the hash is cheap enough, and the significant quality improvement usually justifies the extra cost. I would personally default to the nested method in most cases and use the linear combination method only when the target hardware is limited and known not to handle integer arithmetic well.&lt;/p&gt;
&lt;p&gt;In GLSL we can take advantage of function overloading to implement the different variants:&lt;/p&gt;
&lt;pre data-lang="glsl" style="background-color:#1c1e1f;color:#d6d6d4;" class="language-glsl "&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;hash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;x&lt;/span&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#999999;"&gt;// ...
&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;hash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uvec2 &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;x&lt;/span&gt;&lt;span&gt;) { &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;return &lt;/span&gt;&lt;span&gt;hash(x.x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;+ &lt;/span&gt;&lt;span&gt;hash(x.y)); }
&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;hash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uvec3 &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;x&lt;/span&gt;&lt;span&gt;) { &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;return &lt;/span&gt;&lt;span&gt;hash(x.x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;+ &lt;/span&gt;&lt;span&gt;hash(x.y &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;+ &lt;/span&gt;&lt;span&gt;hash(x.z))); }
&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;hash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uvec4 &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;x&lt;/span&gt;&lt;span&gt;) { &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;return &lt;/span&gt;&lt;span&gt;hash(x.x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;+ &lt;/span&gt;&lt;span&gt;hash(x.y &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;+ &lt;/span&gt;&lt;span&gt;hash(x.z &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;+ &lt;/span&gt;&lt;span&gt;hash(x.w)))); }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="output-dimensionality"&gt;Output dimensionality&lt;a class="zola-anchor" href="#output-dimensionality" aria-label="Anchor link for: output-dimensionality"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Sometimes we need to generate multiple random numbers on a single shader invocation. For instance, in path tracing, we require many independent random numbers per pixel to sample BRDFs. Similarly, in procedural terrain generation we usually combine several layers of noise, and each one must be independent of the others to avoid self-similarity.&lt;/p&gt;
&lt;p&gt;If the number of independent random numbers needed per invocation is low, a common way to convert a 1 -&amp;gt; 1 hash to 1 -&amp;gt; N is to apply a translation to the input by offsetting it with an arbitrary number:&lt;/p&gt;
&lt;script type="math/tex;mode=display"&gt;[\text{hash}(X), \text{hash}(X + a), \text{hash}(X + b)],&lt;/script&gt;
&lt;p&gt;where &lt;script type="math/tex"&gt;X&lt;/script&gt;
is the input or seed, and &lt;script type="math/tex"&gt;a&lt;/script&gt;
and &lt;script type="math/tex"&gt;b&lt;/script&gt;
are the arbitrary numbers.&lt;/p&gt;
&lt;p&gt;If we need a larger quantity of uncorrelated random numbers per invocation with better quality, we can use PRNGs. The approach involves using one of the previously mentioned hash functions to generate an initial seed for the PRNG. Each shader invocation has its own instance of the PRNG, eliminating the need for tracking a global state, as the state is maintained locally. As we mentioned earlier, PRNGs are not designed to go “wide”, but hashing their initial seeds ensures that their output remains uncorrelated.&lt;/p&gt;
&lt;p&gt;Some sources, like &lt;a href="https://chasethedevil.github.io/post/war-of-the-random-number-generators/"&gt;this one&lt;/a&gt;, recommend against using different seeds when parallelizing PRNGs, and that jump-ahead should be preferred when possible. I guess this might apply to more serious simulations where the statistical quality of the generated numbers is very important. In my own experience, for computer graphics, chaining a hash and a PRNG delivers good results and I have yet to find any easy to spot patterns when using this method.&lt;/p&gt;
&lt;h2 id="chaining-prng"&gt;Choosing a PRNG for chaining&lt;a class="zola-anchor" href="#chaining-prng" aria-label="Anchor link for: chaining-prng"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I will not go too in-depth in choosing a non-cryptographic PRNG. &lt;a href="https://en.wikipedia.org/wiki/Mersenne_Twister"&gt;Mersenne Twister&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Xorshift"&gt;xorshift&lt;/a&gt;, &lt;a href="https://www.pcg-random.org/index.html"&gt;PCG&lt;/a&gt;, and &lt;a href="https://prng.di.unimi.it/"&gt;xoshiro/xoroshiro&lt;/a&gt; are all great options. The last two (PCG and xoshiro) are more modern, so they are usually recommended nowadays. Their authors were involved in an interesting technical feud over which algorithm is superior, throwing statistical tests at each other until one emerged victorious. I accept the importance of mathematical theory and rigorous analysis, but I think that this choice is mostly a matter of fanaticism.&lt;/p&gt;
&lt;p&gt;I personally use PCG, but I don’t have a special preference for one or another.&lt;/p&gt;
&lt;h2 id="floating-point"&gt;Random floating point numbers&lt;a class="zola-anchor" href="#floating-point" aria-label="Anchor link for: floating-point"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;In shaders we usually need random floats, not integers. High-quality random floating point numbers are difficult to obtain directly just using floating point operations. As we saw earlier, integer arithmetic has some great properties that are leveraged by PRNGs and hashes to obtain high-quality random sequences with good statistical properties.&lt;/p&gt;
&lt;p&gt;Fortunately, modern GPU hardware provides great support for integer arithmetic, achieving similar performance to that of floating point operations. In the case of Nvidia, here is &lt;a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#arithmetic-instructions"&gt;a nice table&lt;/a&gt; comparing the throughput of arithmetic instructions. In the past, this wasn’t the case, as int32 math on the GPU was either prohibitively expensive or not possible at all. At that time, it made sense to consider using the &lt;a href="https://www.humus.name/Articles/Persson_LowlevelShaderOptimization.pdf"&gt;24-bit integer multiplication intrinsics&lt;/a&gt;, or the one-liner from the beginning of the article that only involves fp32 operations.&lt;/p&gt;
&lt;p&gt;GLSL, HLSL, Cuda and OpenCL all use the &lt;a href="https://en.wikipedia.org/wiki/Single-precision_floating-point_format"&gt;IEEE 754 binary32 format&lt;/a&gt; for single-precision floating point numbers.&lt;sup class="footnote-reference" id="fr-4-1"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt; A well-known technique exploits this format to construct a random floating point number in the half-open range &lt;script type="math/tex"&gt;[0,1)&lt;/script&gt;
starting from a random 32-bit integer.&lt;/p&gt;
&lt;p&gt;The real value represented by a given 32-bit IEEE 754 binary32 data is&lt;/p&gt;
&lt;script type="math/tex;mode=display"&gt;(-1)^{\text{sign}} \cdot 2^{\text{exponent}-127} \cdot (1 + \text{significand}).&lt;/script&gt;
&lt;p&gt;To construct the random float, we select any 23 bits from the random integer to use as the significand.&lt;sup class="footnote-reference" id="fr-5-1"&gt;&lt;a href="#fn-5"&gt;5&lt;/a&gt;&lt;/sup&gt; We then set the sign bit to 0, the exponent to 127, and subtract 1 from the final result. In GLSL:&lt;/p&gt;
&lt;pre data-lang="glsl" style="background-color:#1c1e1f;color:#d6d6d4;" class="language-glsl "&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;float &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;uint_to_normalized_float&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;x&lt;/span&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;return &lt;/span&gt;&lt;span&gt;uintBitsToFloat(x &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;9 &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;| &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;0x3F800000u&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;- &lt;/span&gt;&lt;span style="color:#9c91e4;"&gt;1.0&lt;/span&gt;&lt;span&gt;;
&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use floating point numbers as the input for the integer hash, we can simply interpret the bits of the input single-precision float as a 32-bit unsigned integer. In GLSL:&lt;/p&gt;
&lt;pre data-lang="glsl" style="background-color:#1c1e1f;color:#d6d6d4;" class="language-glsl "&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;uint &lt;/span&gt;&lt;span style="color:#b6e63e;"&gt;float_to_uint&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="font-style:italic;color:#66d9ef;"&gt;float &lt;/span&gt;&lt;span style="font-style:italic;color:#fd971f;"&gt;x&lt;/span&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style="color:#fb2874;"&gt;return &lt;/span&gt;&lt;span&gt;floatBitsToUint(x);
&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="zola-anchor" href="#conclusion" aria-label="Anchor link for: conclusion"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;My current setup employs the lowbias32 hash function as the primary RNG mechanism in shaders. It uses the &lt;a href="https://fgarlin.com/blog/gpu-rng/#input-dimensionality"&gt;nesting method&lt;/a&gt; to accept 1 to 4 input parameters based on the dimensionality of the shader invocation’s unique identifier. For example, in compute shaders the unique identifier is the thread id, while in vertex shaders I usually use the vertex position. If more than one random number is required per invocation, I first try to use the &lt;a href="https://fgarlin.com/blog/gpu-rng/#output-dimensionality"&gt;translation trick&lt;/a&gt;. Sometimes I need more than 4 numbers or better quality in general, so I create an instance of the &lt;a href="https://fgarlin.com/blog/gpu-rng/#chaining-prng"&gt;PCG PRNG&lt;/a&gt; that is local to the invocation and feed it the output of lowbias32 as the initial seed. The &lt;a href="https://fgarlin.com/blog/gpu-rng/#floating-point"&gt;floating point conversion&lt;/a&gt; described just now is almost always used at the end to obtain an useful floating point value.&lt;/p&gt;
&lt;p&gt;There are many options when it comes to random number generation, and most are objectively better than the “fract sin” one-liner, so there is little reason to use it nowadays. Diving into the RNG rabbit hole has been a great learning experience, but hopefully this article saves valuable time for those who are busy exploring other rabbit holes!&lt;/p&gt;
&lt;footer class="footnotes"&gt;
&lt;ol class="footnotes-list"&gt;
&lt;li id="fn-1"&gt;
&lt;p&gt;Even if we can, it would involve using shared memory and atomic operations, which is a big no-no in terms of performance. &lt;a href="#fr-1-1"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn-2"&gt;
&lt;p&gt;Mathematical rigor and empirical justifications of statistical quality are much more important in “serious” applications - as I said, we only care about the hash &lt;em&gt;looking&lt;/em&gt; random. &lt;a href="#fr-2-1"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn-3"&gt;
&lt;p&gt;Perlin, K. (1985). An image synthesizer. ACM SIGGRAPH Computer Graphics, 19(3), 287–296. &lt;a href="https://doi.org/10.1145/325165.325247"&gt;https://doi.org/10.1145/325165.325247&lt;/a&gt; &lt;a href="#fr-3-1"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn-4"&gt;
&lt;p&gt;Some hardware and older versions of these frameworks might not support the IEEE 754 standard, but it is safe to say that most do. They might also not adhere strictly to the standard because they add &lt;a href="https://learn.microsoft.com/en-us/windows/win32/direct3d11/floating-point-rules#deviations-or-additional-requirements-from-ieee-754-rules"&gt;additional caveats or constraints&lt;/a&gt; that are not found in the original IEEE 754, but they certainly use the same internal bit representation, which is what matters to us. &lt;a href="#fr-4-1"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn-5"&gt;
&lt;p&gt;It is possible to use any combination of the 23 bits because, theoretically, the chosen PRNG produces each bit as if it were a perfect coin toss, i.e. each bit has a 50% chance of being 0 or 1 and is independent of the others. However, in practice, there will always be a small bias. It is usually a good idea to pick the higher bits. &lt;a href="#fr-5-1"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/footer&gt;</content></entry><entry xml:lang="en"><title>Real-time spectral rendering of the atmospheric medium</title><published>2024-12-06T00:00:00+00:00</published><updated>2026-01-09T00:00:00+00:00</updated><author><name>
Fernando García Liñán</name></author><link rel="alternate" type="text/html" href="https://fgarlin.com/blog/spectral-sky/"/><id>https://fgarlin.com/blog/spectral-sky/</id><content type="html" xml:base="https://fgarlin.com/blog/spectral-sky/">&lt;p&gt;&lt;em&gt;Update: This work has been &lt;a href="https://docs.blender.org/manual/en/latest/render/shader_nodes/textures/sky.html"&gt;adapted&lt;/a&gt; to be used in &lt;a href="https://blender.org"&gt;Blender&lt;/a&gt;! Thanks a lot to &lt;a href="https://marcopavanello.art/blog/2025/09-15/"&gt;Marco Pavanello&lt;/a&gt; for bringing it to life! I’ve been a Blender user for 15+ years now, so it’s great to finally give back to it in some way. Isn’t free software great?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This post is an excerpt from my Master’s Thesis, which explains the formal tone and the use of the editorial “we”.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;An often ignored aspect of sky and atmosphere rendering is the strong wavelength dependency of the atmospheric medium. This is caused by the optical properties of both molecular scatterers and ozone being highly variable depending on the wavelength of light interacting with them. To take this relationship into account in a light transport simulation, offline renderers usually resort to uniform spectral sampling or more elaborated spectral sampling methods like &lt;a href="https://cgg.mff.cuni.cz/publications/hero-wavelength-spectral-sampling/"&gt;Hero Wavelength Sampling&lt;/a&gt;, while real-time renderers often choose to completely ignore this behaviour and perform traditional RGB rendering.&lt;/p&gt;
&lt;p&gt;In uniform spectral sampling, we pick &lt;script type="math/tex"&gt;n_\lambda&lt;/script&gt;
uniformly distributed samples in a portion of the electromagnetic spectrum (usually the visible spectrum). For each pixel in the rendered image, &lt;script type="math/tex"&gt;n_\lambda&lt;/script&gt;
rays are fired from the camera, each with an associated wavelength, making all the interactions between the rays and the scene wavelength-dependent. Consequently, compared to traditional RGB rendering, spectral rendering requires substantially more data to represent the optical properties of the scene, which can be challenging to obtain in most cases. Luckily, in the case of the atmosphere of Earth, there is plenty of available data thanks to atmospheric sciences research.&lt;/p&gt;
&lt;p&gt;Spectral rendering can sometimes offer more accurate results since it is able to capture optical phenomena that would otherwise be impossible to recreate, like iridescence. However, spectral rendering is quite sensitive to which portion of the electromagnetic spectrum is used to perform the simulation. For example, if we have a scene that contains a single light source that does not emit any electromagnetic radiation in the near-UV range, and we simulate light transport for the near-UV range in an attempt to improve image fidelity, we are not going to observe any improvements. Due to cases like this, widening the range of simulated wavelengths or increasing the number of sampled wavelengths will usually, but not always, yield a better final result.&lt;/p&gt;
&lt;p&gt;The prohibitively high computational cost of offline spectral rendering techniques remains the most significant problem though, making them unsuitable for real-time applications. We propose a method to greatly decrease the overall computational cost while still offering similar results to a fully-fledged spectral renderer.&lt;/p&gt;
&lt;h2 id="tristimulus-color-from-spectral-data"&gt;Tristimulus color from spectral data&lt;a class="zola-anchor" href="#tristimulus-color-from-spectral-data" aria-label="Anchor link for: tristimulus-color-from-spectral-data"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;For a given spectral power distribution (SPD), &lt;script type="math/tex"&gt;L(\lambda)&lt;/script&gt;
, the XYZ tristimulus values can be computed as:&lt;/p&gt;
&lt;p&gt;&lt;script type="math/tex;mode=display"&gt;X = \int \limits_\lambda \bar{x}(\lambda) L(\lambda) d \lambda&lt;/script&gt;
&lt;script type="math/tex;mode=display"&gt;Y = \int \limits_\lambda \bar{y}(\lambda) L(\lambda) d \lambda&lt;/script&gt;
&lt;script type="math/tex;mode=display"&gt;Z = \int \limits_\lambda \bar{z}(\lambda) L(\lambda) d \lambda&lt;/script&gt;
&lt;/p&gt;
&lt;p&gt;where &lt;script type="math/tex"&gt;\bar{x}(\lambda)&lt;/script&gt;
, &lt;script type="math/tex"&gt;\bar{y}(\lambda)&lt;/script&gt;
, &lt;script type="math/tex"&gt;\bar{z}(\lambda)&lt;/script&gt;
are the CIE standard observer functions, also known as the &lt;a href="https://cie.co.at/datatable/cie-1931-colour-matching-functions-2-degree-observer"&gt;CIE color matching functions (CMFs)&lt;/a&gt;. The integrals are computed over the visible spectrum. If it has not been specified otherwise, we will consider the visible spectrum to range from 390nm to 780nm.&lt;/p&gt;
&lt;p&gt;As discussed in previous sections, &lt;script type="math/tex"&gt;L(\lambda)&lt;/script&gt;
does not have a closed form analytical expression that represents it. Instead, the function exists as discrete samples. Numerical integration must then used to solve the integrals in the equations above. It is important to note that the spans and spacings of the standard observer functions must match those of the spectral power distribution. Resampling and interpolation is used to accomplish this.&lt;/p&gt;
&lt;p&gt;Now it is clear why &lt;script type="math/tex"&gt;n_\lambda&lt;/script&gt;
needs to be sufficiently high to correctly approximate &lt;script type="math/tex"&gt;L(\lambda)&lt;/script&gt;
. If too few samples are selected, the result of the numerical integration will not be accurate. We are looking for a successful approximation which uses the least amount of spectral samples while still maintaining visual fidelity. We also take advantage of the fact that we are integrating the product of &lt;script type="math/tex"&gt;L(\lambda)&lt;/script&gt;
and the CMF, thus some samples of &lt;script type="math/tex"&gt;L(\lambda)&lt;/script&gt;
will be weighted down significantly by the CMF, making their impact on the final image unimportant.&lt;/p&gt;
&lt;h2 id="numerical-integration"&gt;Numerical integration&lt;a class="zola-anchor" href="#numerical-integration" aria-label="Anchor link for: numerical-integration"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;We can perform numerical integration of the CIE integrals using a Riemann sum. For clarity purposes, we will use the tristimulus value X, although the same criteria also applies to Y and Z. The visible spectrum is divided into &lt;script type="math/tex"&gt;n_\lambda&lt;/script&gt;
rectangular partitions that together form a region that is similar to the region below the function inside the integral. The height of each rectangle corresponds to the value of &lt;script type="math/tex"&gt;L(\lambda) \cdot \bar{x}(\lambda)&lt;/script&gt;
at any &lt;script type="math/tex"&gt;\lambda&lt;/script&gt;
belonging to the partition, while the width corresponds to the wavelength interval covered by the partition. The sum of the areas of the partitions approximates the integral we are trying to compute.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://fgarlin.com/blog/spectral-sky/cajas.png" alt="Relative wavelength" /&gt;
&lt;em&gt;Riemann sum of four partitions, each described by &lt;script type="math/tex"&gt;\lambda_i&lt;/script&gt;
and &lt;script type="math/tex"&gt;w_i&lt;/script&gt;
. The sum of the areas of all four partitions approximates the integral of &lt;script type="math/tex"&gt;L(\lambda) \cdot \bar{x}(\lambda)&lt;/script&gt;
. The same idea can be applied to the Y and Z values.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Riemann sum for each tristimulus value can be expressed as:&lt;/p&gt;
&lt;script type="math/tex;mode=display"&gt;X = \sum_{i=1}^{n_{\lambda}} \bar{x}_i L_i w_i&lt;/script&gt;
&lt;script type="math/tex;mode=display"&gt;Y = \sum_{i=1}^{n_{\lambda}} \bar{y}_i L_i w_i&lt;/script&gt;
&lt;script type="math/tex;mode=display"&gt;Z = \sum_{i=1}^{n_{\lambda}} \bar{z}_i L_i w_i&lt;/script&gt;
&lt;p&gt;where &lt;script type="math/tex"&gt;w_i&lt;/script&gt;
is the wavelength interval &lt;script type="math/tex"&gt;\Delta \lambda&lt;/script&gt;
covered by each partition, which we will call the weight of the partition. Going back to uniform spectral sampling, &lt;script type="math/tex"&gt;w_i&lt;/script&gt;
is constant in this case as the interval covered by each partition remains constant. In our solution we allow the intervals to change, giving us four more degrees of freedom.&lt;/p&gt;
&lt;p&gt;These summations of products can also be written as a matrix multiplication, which is specially useful in order to perform the computations efficiently on the GPU:&lt;/p&gt;
&lt;script type="math/tex;mode=display"&gt;\begin{bmatrix} X \\\ Y \\\ Z \end{bmatrix}
=
\begin{bmatrix}
\bar{x}_1 w_1 &amp; \bar{x}_2 w_2 &amp; \dots &amp; \bar{x}_n w_n \\
\bar{y}_1 w_1 &amp; \bar{y}_2 w_2 &amp; \dots &amp; \bar{y}_n w_n \\
\bar{z}_1 w_1 &amp; \bar{z}_2 w_2 &amp; \dots &amp; \bar{z}_n w_n
\end{bmatrix}
\begin{bmatrix} L_1 \\ L_2 \\ \vdots \\ L_n \end{bmatrix}&lt;/script&gt;
&lt;h2 id="optimization"&gt;Optimization&lt;a class="zola-anchor" href="#optimization" aria-label="Anchor link for: optimization"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;We have seen how using more spectral samples tends to yield a more accurate image, but due to limitations inherent to real-time rendering, we cannot sample dozens of wavelengths like offline renderers do. First, we have to settle for a number of wavelengths to sample.&lt;/p&gt;
&lt;p&gt;We have chosen four (&lt;script type="math/tex"&gt;n_\lambda=4&lt;/script&gt;
) to take advantage of the SIMD instructions available in most modern hardware. The data paths and registers supporting SIMD operations are usually at least 128-bits wide in both CPUs and GPUs, corresponding to four full 32-bit floats. Our algorithm greatly benefits from SIMD vectorization since the light transport simulation is identical for each wavelength; only the data varies.&lt;/p&gt;
&lt;p&gt;Four wavelengths might not seem sufficient, but we found that if we choose the best four wavelengths for which to sample &lt;script type="math/tex"&gt;L(\lambda)&lt;/script&gt;
, and the correct weight for each partition &lt;script type="math/tex"&gt;w_i&lt;/script&gt;
, we are able to have all the perks of a spectral renderer with just a single SIMD instruction.&lt;/p&gt;
&lt;p&gt;We are left with four partitions, each with a representative wavelength &lt;script type="math/tex"&gt;\lambda&lt;/script&gt;
, and a weight &lt;script type="math/tex"&gt;w&lt;/script&gt;
, totalling eight parameters. The resulting spectrum to tristimulus conversion is:&lt;/p&gt;
&lt;script type="math/tex;mode=display"&gt;\begin{bmatrix} X \\ Y \\ Z \end{bmatrix}
=
\begin{bmatrix}
\bar{x}_1 w_1 &amp; \bar{x}_2 w_2 &amp; \bar{x}_3 w_3 &amp; \bar{x}_4 w_4 \\
\bar{y}_1 w_1 &amp; \bar{y}_2 w_2 &amp; \bar{y}_3 w_3 &amp; \bar{y}_4 w_4 \\
\bar{z}_1 w_1 &amp; \bar{z}_2 w_2 &amp; \bar{z}_3 w_3 &amp; \bar{z}_4 w_4
\end{bmatrix}
\begin{bmatrix} L(\lambda_1) \\ L(\lambda_2) \\ L(\lambda_3) \\ L(\lambda_4) \end{bmatrix}&lt;/script&gt;
&lt;p&gt;To find the values of the eight parameters that offer the best results when compared to a ground-truth offline simulation, we pose an optimization problem. Mean squared error (MSE) is used as the loss function due its simplicity and adequate results, and is defined as follows:&lt;/p&gt;
&lt;script type="math/tex;mode=display"&gt;\mathcal{L} = \frac{1}{n} \sum_{\phi,\theta_v,\theta_s} (L_{\phi,\theta_v,\theta_s} - \hat{L}_{\phi,\theta_v,\theta_s})^2&lt;/script&gt;
&lt;p&gt;where &lt;script type="math/tex"&gt;L_{\phi,\theta_v,\theta_s}&lt;/script&gt;
, &lt;script type="math/tex"&gt;\hat{L}_{\phi,\theta_v,\theta_s}&lt;/script&gt;
, are the ground truth and approximated radiance values at a given view direction &lt;script type="math/tex"&gt;(\phi, \theta_v)&lt;/script&gt;
and Sun elevation angle &lt;script type="math/tex"&gt;\theta_s&lt;/script&gt;
, and &lt;script type="math/tex"&gt;n&lt;/script&gt;
is the total number of combinations of &lt;script type="math/tex"&gt;\phi&lt;/script&gt;
, &lt;script type="math/tex"&gt;\theta_v&lt;/script&gt;
, &lt;script type="math/tex"&gt;\theta_s&lt;/script&gt;
.&lt;/p&gt;
&lt;p&gt;This pixel-wise loss function asserts that all view directions and Sun elevation angles are equally important, thus it is important to note that their distribution will affect the optimization. We have deliberately skipped higher Sun elevation angles to put more emphasis on sunrise/sunset conditions, in order to avoid overfitting to the clearly dominant blue color present in daylight.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://fgarlin.com/blog/spectral-sky/average_radiance_sun_elevation.png" alt="Relative wavelength" /&gt;
&lt;em&gt;Normalized spectral radiance for several Sun elevations at sea level. The values are averaged across many view directions. Note how long wavelengths (yellows, oranges, reds) become more relevant the lower the Sun elevation angle (sunrise and sunset).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For the optimization process itself, first we performed a brute force grid search of the eight parameters (&lt;script type="math/tex"&gt;\lambda_1&lt;/script&gt;
, &lt;script type="math/tex"&gt;\lambda_2&lt;/script&gt;
, &lt;script type="math/tex"&gt;\lambda_3&lt;/script&gt;
, &lt;script type="math/tex"&gt;\lambda_4&lt;/script&gt;
, &lt;script type="math/tex"&gt;w_1&lt;/script&gt;
, &lt;script type="math/tex"&gt;w_2&lt;/script&gt;
, &lt;script type="math/tex"&gt;w_3&lt;/script&gt;
, &lt;script type="math/tex"&gt;w_4&lt;/script&gt;
), and then we refined the results using Nelder–Mead.&lt;/p&gt;
&lt;p&gt;It is important to note that this optimization only works for a particular atmospheric model. If we optimize for a model, but attempt to use the resulting optimized parameters with another model, the end result will not be correct. In our case, we use the Rayleigh scattering and ozone absorption parts of our atmospheric model, and ignore the aerosols. Aerosols can vary dramatically with the viewer’s position, making them unsuitable for obtaining a general solution to the eight optimization parameters. Luckily for us, they are not as dependent on wavelength as molecules due to their relatively large size, so we found no practical difference between optimizing with and without aerosols.&lt;/p&gt;
&lt;h2 id="rendering-and-results"&gt;Rendering and results&lt;a class="zola-anchor" href="#rendering-and-results" aria-label="Anchor link for: rendering-and-results"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;During actual real-time rendering we compute the spectral radiance &lt;script type="math/tex"&gt;L(\lambda)&lt;/script&gt;
for each of the optimized wavelengths (&lt;script type="math/tex"&gt;\lambda_1&lt;/script&gt;
, &lt;script type="math/tex"&gt;\lambda_2&lt;/script&gt;
, &lt;script type="math/tex"&gt;\lambda_3&lt;/script&gt;
, &lt;script type="math/tex"&gt;\lambda_4&lt;/script&gt;
), and then apply the spectrum to tristimulus conversion. The optimization process we described in the previous section has been performed offline, which means that, at runtime, our method solely requires four spectral samples and a matrix multiplication. Now it is clear why we chose four spectral samples instead of any other number. We can compute &lt;script type="math/tex"&gt;L(\lambda)&lt;/script&gt;
for the four samples at the same time thanks to SIMD, which makes our method as cheap as traditional RGB rendering.&lt;/p&gt;
&lt;p&gt;The end result is a highly accurate render comparable to fully-fledged spectral renderers that require dozens of spectral samples. The image fidelity is also greatly improved in contrast to state to the art algorithms that incorrectly use three spectral radiance values directly as an RGB triplet.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://fgarlin.com/blog/spectral-sky/rgb_vs_spectrum.png" alt="RGB vs spectrum" /&gt;
&lt;em&gt;Traditional RGB rendering with 3 wavelengths (680nm, 550nm, 440nm) (left), compared to our spectral rendering method (right). To highlight the differences between both approaches we have removed aerosols as they do not depend on wavelength as much as other constituents.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="zola-anchor" href="#conclusion" aria-label="Anchor link for: conclusion"&gt;
&lt;svg width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="M7.05025 1.53553C8.03344 0.552348 9.36692 0 10.7574 0C13.6528 0 16 2.34721 16 5.24264C16 6.63308 15.4477 7.96656 14.4645 8.94975L12.4142 11L11 9.58579L13.0503 7.53553C13.6584 6.92742 14 6.10264 14 5.24264C14 3.45178 12.5482 2 10.7574 2C9.89736 2 9.07258 2.34163 8.46447 2.94975L6.41421 5L5 3.58579L7.05025 1.53553Z" fill="currentColor"/&gt;
&lt;path d="M7.53553 13.0503L9.58579 11L11 12.4142L8.94975 14.4645C7.96656 15.4477 6.63308 16 5.24264 16C2.34721 16 0 13.6528 0 10.7574C0 9.36693 0.552347 8.03344 1.53553 7.05025L3.58579 5L5 6.41421L2.94975 8.46447C2.34163 9.07258 2 9.89736 2 10.7574C2 12.5482 3.45178 14 5.24264 14C6.10264 14 6.92742 13.6584 7.53553 13.0503Z" fill="currentColor"/&gt;
&lt;path d="M5.70711 11.7071L11.7071 5.70711L10.2929 4.29289L4.29289 10.2929L5.70711 11.7071Z" fill="currentColor"/&gt;
&lt;/svg&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;This technique adds accurate spectral rendering capabilities to any real-time atmosphere rendering framework, like &lt;a href="https://sebh.github.io/publications/egsr2020.pdf"&gt;Sébastien Hillaire’s Production Ready Atmosphere&lt;/a&gt; or &lt;a href="https://ebruneton.github.io/precomputed_atmospheric_scattering/"&gt;Bruneton’s Precomputed Atmospheric Scattering&lt;/a&gt;. It is also not limited to the atmosphere: any participating media which has optical properties that are strongly dependent on wavelength can benefit from this approach, such as human skin and hair, which feature prominent subsurface scattering. All of the methods and the general workflow described above can be applied to these volumetric media.&lt;/p&gt;
&lt;p&gt;A working implementation of this technique can be found in this &lt;a href="https://www.shadertoy.com/view/msXXDS"&gt;Shadertoy&lt;/a&gt;. The spectral path tracer used to obtain ground-truth images to fit the real-time approximation is located in &lt;a href="https://github.com/fgarlin/skytracer"&gt;this repo&lt;/a&gt;.&lt;/p&gt;</content></entry></feed>