{"slug": "houston-stratified-noise-texture-free-sampling-for-screen-space-effects", "title": "Houston Stratified Noise: Texture-Free Sampling for Screen-Space Effects", "summary": "Ben Houston released Houston Stratified Noise (HSN), a texture-free noise function for real-time rendering that stratifies each 8×8 tile hierarchically and advances each dimension with its own low-discrepancy step, landing in bhouston/three.js pull requests #117 and #121 where it now drives screen-space reflections and voxel cone traced global illumination. In the #117 runtime study, HSN placed about 0.044% of averaged threshold-mask power in its single largest Fourier bin, versus 17.8% for interleaved gradient noise, 24.6% for the R2 sequence and 0.014% for FAST's optimized blue noise texture. Houston built HSN to avoid the roughly 6 MB download of the FAST textures used for Three.js and the about 31 seconds GPU Stratified Annealing takes to generate, replacing both with a few integer hashes and adds per sample in the shader.", "body_md": "# Houston Stratified Noise: Texture-Free Sampling for Screen-Space Effects\n\nHouston 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.\n\n[Ben Houston](https://ben3d.ca/about) •  • 10 min read\n\nIn my [GPU Stratified Annealing post](https://ben3d.ca/blog/gpu-stratified-annealing), 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.\n\nSo 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.\n\nHSN came out of [bhouston/three.js#117](https://github.com/bhouston/three.js/pull/117) and is now used by screen-space reflections and voxel cone traced global illumination in [bhouston/three.js#121](https://github.com/bhouston/three.js/pull/121).\n\n*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.*\n\n## Why screen-space effects care about noise[#](#why-screen-space-effects-care-about-noise)\n\nScreen-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.\n\nWhite 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.\n\nTwo texture-free options are already common in real-time rendering:\n\n- **Interleaved gradient noise (IGN)** , from[Jorge Jimenez's Call of Duty work](https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/) , is a one-line formula with excellent local stratification. Animating it by scrolling 5.588238 pixels per frame, as[Alan Wolfe describes](https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/) , makes it converge over time.\n- **The R2 sequence** , from[Martin Roberts' quasirandom sequences](https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) , steps by the generalized golden ratio and is very well distributed.\n\nBoth 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.\n\nIGN has a second problem that only shows up in effects that need more than one value per pixel. More on that below.\n\n## How HSN works[#](#how-hsn-works)\n\nHSN works on integer pixel coordinates and an integer frame index. It has three parts.\n\n**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.\n\n**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.\n\n**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.\n\nIn outline, as TSL (the full version is in the PR):\n\n``` js\nconst rank = uint( 0 ).toVar();\n\nfor ( let level = 0; level < 3; level ++ ) {\n\n\tconst scramble = pixelHash( x.shiftRight( level + 1 ), y.shiftRight( level + 1 ), seed ^ levelSalt[ level ] );\n\tconst a = x.shiftRight( level ).bitAnd( 1 ), b = y.shiftRight( level ).bitAnd( 1 );\n\tconst low = scramble.bitAnd( 4 ).notEqual( 0 ).select( a, b );\n\tconst child = a.bitXor( b ).shiftLeft( 1 ).bitOr( low );\n\trank.assign( rank.bitOr( child.bitXor( scramble.bitAnd( 3 ) ).shiftLeft( 30 - 2 * level ) ) );\n\n}\n\nconst phase = pixelHash( x.shiftRight( 3 ), y.shiftRight( 3 ), seed ^ 0x02e5be93 );\nreturn rank.add( phase ).add( frame.mul( step ) ).shiftRight( 8 ).toFloat().div( 16777216 );\n```\n\nNone 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](https://doi.org/10.1145/3414685.3417881) 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.\n\n## The failure that made it better[#](#the-failure-that-made-it-better)\n\nHSN 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.\n\nThey 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.\n\nThe 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.\n\nThe 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.\n\n*Per-pixel RMSE over 14 known 2D integrals, including step edges at the exact diagonals, on 64×64 pixels with no spatial filter.*\n\nWith 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.\n\nThe 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.\n\n## How HSN compares on single values[#](#how-hsn-compares-on-single-values)\n\nFor 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:\n\n| Method | Threshold, 1 frame | Threshold, 32 frames | Threshold, EMA | Largest spectral peak | \n|---|---|---|---|---|\n| White noise | 2.167× | 3.575× | 3.167× | — | \n| Animated IGN | 0.880× | 0.622× | 0.660× | 17.8% | \n| R2 + Weyl | 0.808× | 0.856× | 1.012× | 24.6% | \n| **HSN** | 1.328× | 0.734× | 0.971× | **0.044%** | \n| FAST texture | 1.000× | 1.000× | 1.000× | 0.014% | \n\nAnimated 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.\n\nThat 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.\n\n## What it changed in Three.js[#](#what-it-changed-in-threejs)\n\n[bhouston/three.js#121](https://github.com/bhouston/three.js/pull/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.\n\n| Effect | Noise | Noise change | RMSE | Bias | \n|---|---|---|---|---|\n| VXGI light map bake, 64 samples | HSN, R2 steps | −12.6% grain | −52.5% | −33.9% | \n| VXGI, real time | HSN, R2 steps | +6.8% flicker | −23.4% | −31.1% | \n| Volume lighting with TRAA | animated IGN | −19.3% flicker | −3.8% | −2.6% | \n| SSR with denoiser | HSN, R4 steps | −4.8% flicker | −3.9% | −3.7% | \n\nThe 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.\n\nNot 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.\n\n## Using it[#](#using-it)\n\nIn Three.js, HSN lives in `examples/jsm/tsl/utils/HoustonStratifiedNoise.js` in the PR. Give each dimension its own seed and its own step:\n\n``` js\nimport { R2_STEPS, houstonStratifiedNoise } from 'three/addons/tsl/utils/HoustonStratifiedNoise.js';\n\nconst rotation = houstonStratifiedNoise( screenCoordinate, frameIndex, 1, R2_STEPS[ 0 ] );\nconst elevation = houstonStratifiedNoise( screenCoordinate, frameIndex, 2, R2_STEPS[ 1 ] );\n```\n\nFor four values, use `R4_STEPS`. For a single value, the default golden-ratio step is fine.\n\nThe [stratified noise example](https://raw.githack.com/bhouston/three.js/ign-adoption/examples/webgpu_tsl_stratified_noise.html) compares HSN, animated IGN and other schedules against white noise on a dithered sphere, with spatial and temporal accumulation controls.\n\n## Limits[#](#limits)\n\nThese 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.\n\nHSN 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.", "url": "https://wpnews.pro/news/houston-stratified-noise-texture-free-sampling-for-screen-space-effects", "canonical_source": "https://ben3d.ca/blog/houston-stratified-noise", "published_at": "2026-10-28 00:00:00+00:00", "updated_at": "2026-10-29 02:42:10.644773+00:00", "lang": "en", "topics": ["computer-vision", "ai-research", "generative-ai"], "entities": ["Ben Houston", "Houston Stratified Noise", "Three.js", "Interleaved gradient noise", "R2 sequence", "GPU Stratified Annealing", "Jorge Jimenez", "Martin Roberts"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/houston-stratified-noise-texture-free-sampling-for-screen-space-effects", "markdown": "https://wpnews.pro/news/houston-stratified-noise-texture-free-sampling-for-screen-space-effects.md", "text": "https://wpnews.pro/news/houston-stratified-noise-texture-free-sampling-for-screen-space-effects.txt", "jsonld": "https://wpnews.pro/news/houston-stratified-noise-texture-free-sampling-for-screen-space-effects.jsonld"}}