Houston Stratified Noise: Texture-Free Sampling for Screen-Space Effects
Houston Stratified Noise (HSN) is a texture-free noise function for real-time rendering. It stratifies each 8×8 tile hierarchically and advances each dimension with its own low-discrepancy step. Here is how it works, what it is built from, where it beats IGN and white noise, and how it improved SSR and VXGI in Three.js.
Ben Houston • • 10 min read
In my GPU Stratified Annealing post, the best samplers came from optimized blue noise textures. They also came with a catch: someone has to download them. The FAST textures we tried for Three.js totalled about 6 MB, which is too much to ship with a small sampling utility, and GSA takes about 31 seconds to generate, which rules out building it at startup.
So I ran the same kind of AI-guided search with a stricter goal: no texture, no startup cost, just arithmetic in the shader. The result is Houston Stratified Noise (HSN). It is a few integer hashes and adds per sample, it needs no download, and its dimensions are independent, which turned out to matter more than I expected.
HSN came out of bhouston/three.js#117 and is now used by screen-space reflections and voxel cone traced global illumination in bhouston/three.js#121.

The bug that taught me the most. Left: two HSN dimensions sharing one temporal step stay on a diagonal line forever. Right: separate R2 steps fill the square.
Why screen-space effects care about noise#
Screen-space reflections, ambient occlusion, global illumination and volumetric fog all take a few random samples per pixel, then hand the noisy result to a spatial filter and a temporal accumulator such as TRAA. The noise pattern decides how much of the error those filters can remove.
White noise is cheap but clumps: neighboring pixels and consecutive frames often pick similar values, which leaves blotchy low-frequency error that filters cannot remove. Better samplers spread the values out in space, over time, or both.
Two texture-free options are already common in real-time rendering:
- Interleaved gradient noise (IGN), from Jorge Jimenez's Call of Duty work, is a one-line formula with excellent local stratification. Animating it by scrolling 5.588238 pixels per frame, as Alan Wolfe describes, makes it converge over time.
- The R2 sequence, from Martin Roberts' quasirandom sequences, steps by the generalized golden ratio and is very well distributed.
Both are strongly structured. In the #117 runtime study, the single largest Fourier bin held about 17.8% of the averaged threshold-mask power for IGN and 24.6% for R2. That shows up as diagonal or lattice patterns. HSN puts about 0.044% of the power in its largest bin, against 0.014% for FAST's optimized blue noise texture.
IGN has a second problem that only shows up in effects that need more than one value per pixel. More on that below.
How HSN works#
HSN works on integer pixel coordinates and an integer frame index. It has three parts.
1. A scrambled hierarchy inside each 8×8 tile. At each of three levels, a pixel's position inside its 2×2 parent block picks one of four child ranks. The ranks follow an ordered-dither pattern, and each parent block hashes its own permutation of them: a swap of orientation and an XOR of the two rank bits. Concatenating the three 2-bit digits gives a 6-bit rank. Every aligned 2×2 block contains all four leading ranks, every 4×4 all sixteen, and every 8×8 all sixty-four. The pattern is stratified at every scale, but the per-parent scrambling breaks up the regular look of a Bayer matrix.
2. A hashed phase per tile. Each 8×8 tile adds its own hashed offset, a toroidal rotation of the tile's values. This hides repetition between tiles while keeping each tile's stratification intact.
3. A Weyl step over time. Each frame adds frame × step in 32-bit fixed point. With the golden ratio as the step, each pixel visits the unit interval in a low-discrepancy order. Because this is integer arithmetic that wraps at 2³², it never loses precision. Animated IGN multiplies the frame index by a float, so it drifts after long runs and must be wrapped.
In outline, as TSL (the full version is in the PR):
const rank = uint( 0 ).toVar(); for ( let level = 0; level < 3; level ++ ) { const scramble = pixelHash( x.shiftRight( level + 1 ), y.shiftRight( level + 1 ), seed ^ levelSalt[ level ] ); const a = x.shiftRight( level ).bitAnd( 1 ), b = y.shiftRight( level ).bitAnd( 1 ); const low = scramble.bitAnd( 4 ).notEqual( 0 ).select( a, b ); const child = a.bitXor( b ).shiftLeft( 1 ).bitOr( low ); rank.assign( rank.bitOr( child.bitXor( scramble.bitAnd( 3 ) ).shiftLeft( 30 - 2 * level ) ) ); } const phase = pixelHash( x.shiftRight( 3 ), y.shiftRight( 3 ), seed ^ 0x02e5be93 ); return rank.add( phase ).add( frame.mul( step ) ).shiftRight( 8 ).toFloat().div( 16777216 );
None of these ingredients is new. Hierarchical ordered dithering goes back to Bayer, scrambling digits per parent is the idea behind Owen scrambling, Ahmed and Wonka showed how much hierarchical screen-space ordering can do, per-tile rotations are Cranley–Patterson rotations, and Weyl sequences are older than computers. HSN is a particular, cheap combination of them that the search found useful.
The failure that made it better#
HSN started as a scalar function. Effects like SSR need four values per pixel, two for a GGX direction and more for ray jitter, so the obvious extension is to call it with four different seeds. Different seeds give independently scrambled spatial patterns, so the dimensions look independent.
They are not. When I tried HSN in the Three.js light map baker, which accumulates 64 samples per texel with no spatial filter, the result was 2.4× grainier than before. Real-time VXGI had looked fine, because TRAA blends neighboring pixels and hides the problem.
The cause is the temporal step. With the same golden-ratio step in every dimension, each frame moves both values of a pixel by the same amount. Over time, a pixel's pairs trace a diagonal line through the unit square, as the hero image shows. Averaging along a line does not integrate a 2D function, so per-pixel error stops falling after a few frames.
The fix is to give each dimension its own step from a sequence designed for that dimension count: the R2 constants for two values and R4 for four, the same generalized golden ratios that make the R2 sequence well distributed. The spatial pattern is unchanged; only the temporal step differs per dimension.

Per-pixel RMSE over 14 known 2D integrals, including step edges at the exact diagonals, on 64×64 pixels with no spatial filter.
With a shared step, HSN's per-pixel error stalls at 84.2×10⁻³ after 256 samples, four times worse than white noise's 19.3×10⁻³. With R2 steps it keeps falling to 4.1×10⁻³, about 20× better than the shared step and 4.7× better than white noise. Under an exponential moving average with a light blur, error drops from 31.7 to 11.3×10⁻³. Single-frame spatial error does not change at all.
The same plot exposes a problem Three.js already had. VXGI derived its second cone dimension from IGN at a fixed pixel offset. IGN's formula makes that second value nearly a constant shift of the first, so the pair stalls at 90×10⁻³, the worst curve on the chart. I also tried pairing the dimensions within each tile using bit-reversed ranks. Per-pixel error barely changed and single-frame error nearly doubled, so that idea was dropped.
How HSN compares on single values#
For a single scalar per pixel, HSN is not a universal winner. The #117 runtime study scores samplers against FAST's optimized blue noise texture on 160 diagnostics. Lower ratios are better:
| Method | Threshold, 1 frame | Threshold, 32 frames | Threshold, EMA | Largest spectral peak |
|---|---|---|---|---|
| White noise | 2.167× | 3.575× | 3.167× | — |
| Animated IGN | 0.880× | 0.622× | 0.660× | 17.8% |
| R2 + Weyl | 0.808× | 0.856× | 1.012× | 24.6% |
| HSN | 1.328× | 0.734× | 0.971× | 0.044% |
| FAST texture | 1.000× | 1.000× | 1.000× | 0.014% |

Animated IGN has lower aggregate threshold error than HSN, and HSN's single-frame error is 33% higher than FAST's. Its worst individual diagnostic is 2.3× FAST's. What HSN offers instead is a spectrum without strong peaks, which avoids visible diagonal patterns, together with integer time and independent seeds.
That shaped how Three.js uses the two. For one value per pixel, such as a ray-march dither, animated IGN remains the better choice. For two or more values per pixel, HSN with R2 or R4 steps is the better choice.
What it changed in Three.js#
bhouston/three.js#121 consolidates the screen-space noise in the Three.js examples. A shared animated IGN helper replaces several hand-written copies, and HSN supplies the multi-dimensional samples. Every change was measured on hardware WebGPU against the previous code. The measurements use a stationary camera and linear TRAA output, and compare against a 1,024-frame white-noise reference.

| Effect | Noise | Noise change | RMSE | Bias |
|---|---|---|---|---|
| VXGI light map bake, 64 samples | HSN, R2 steps | −12.6% grain | −52.5% | −33.9% |
| VXGI, real time | HSN, R2 steps | +6.8% flicker | −23.4% | −31.1% |
| Volume lighting with TRAA | animated IGN | −19.3% flicker | −3.8% | −2.6% |
| SSR with denoiser | HSN, R4 steps | −4.8% flicker | −3.9% | −3.7% |
The light map bake benefits most: halved error and less grain at the same sample count. Real-time VXGI trades a little more frame-to-frame flicker for much lower bias. The old samples looked calmer partly because they stayed on a line, which was itself the source of the bias. SSR improves on every metric.
Not everything changed. GTAO and SSGI use the temporal rotation tables from Activision's GTAO paper, which cycle through a few fixed offsets. Animated IGN and HSN reduced bias there but increased flicker, so those effects keep their tables. A noise that converges better in the long run can still look worse under a short TRAA history.
Using it#
In Three.js, HSN lives in examples/jsm/tsl/utils/HoustonStratifiedNoise.js in the PR. Give each dimension its own seed and its own step:
import { R2_STEPS, houstonStratifiedNoise } from 'three/addons/tsl/utils/HoustonStratifiedNoise.js'; const rotation = houstonStratifiedNoise( screenCoordinate, frameIndex, 1, R2_STEPS[ 0 ] ); const elevation = houstonStratifiedNoise( screenCoordinate, frameIndex, 2, R2_STEPS[ 1 ] );
For four values, use R4_STEPS. For a single value, the default golden-ratio step is fine.
The stratified noise example compares HSN, animated IGN and other schedules against white noise on a dithered sphere, with spatial and temporal accumulation controls.
Limits#
These measurements use static cameras. Motion, disocclusion and history rejection change how much any sampler helps. The three.js numbers come from one Apple GPU; the #117 study used NVIDIA hardware and its CPU models match the GPU output bit for bit. HSN repeats its stratification every 8 pixels, and its single-frame quality is below a good blue noise texture. If you can afford the download, an optimized texture such as FAST or GSA is still better for single values.
HSN is what you can get for a few lines of integer arithmetic. The most useful lesson for me was the failure: a sampler can look independent per dimension and still collapse when you watch a single pixel over time. A per-pixel convergence test catches that, and in this case it also found an older bug in Three.js.