2026
TL;DR
I wrote a single GLSL shader that renders a physically accurate spinning diamond in real time. It traces two wavelengths of light per pixel, but there are 65,536 different wavelength combinations spread across the image. It never averages anything, over time or between neighboring pixels.
It’s also on Shadertoy if you want to read the code.
Diamonds are fun to render because almost none of the light you see went straight through. It bends on the way in, bounces around inside off the facets, then bends again on the way out. Every color bends by a slightly different amount, which is dispersion, and it’s why a diamond throws rainbows. Doing that properly normally means tracing a lot of rays and waiting for the noise to average out.
One sample per pixel
Every wavelength takes a different path through the stone. So to get the color right you’d trace red, then orange, then yellow, hundreds of wavelengths, and average them. A path tracer approximates that by picking a random wavelength per sample and averaging thousands of samples. It works, but it’s noisy until it converges, and it can’t run clean at one sample per pixel. You sit and wait.
So I stopped picking wavelengths at random and started picking them on a schedule.
Every pixel reads a value from a 16 by 16 Bayer dither pattern. That’s 256 different values tiled across the screen, and the value decides which wavelength that pixel traces. One pixel traces deep red, its neighbor orange, the next one green. Each pixel is a single pure color. Step back and your eye blends the neighborhood into white. The averaging still happens, it just happens on your monitor instead of in an accumulation buffer.
A normal spectral renderer converges over time. This one is already converged in space, because the dither guarantees every little neighborhood covers the spectrum evenly. One sample per pixel, deterministic, no waiting, 256 wavelengths across the image.
It looks better at two samples, so that’s what the demo runs. The second sample isn’t a second wavelength from the same set. It’s offset by the same 256-value dither, so the two samples in one pixel land on a different pair than the two next door. Across the screen that’s 256 times 256, or 65,536 distinct wavelengths, with each pixel only ever tracing two. The whites clean right up.
The shape is just planes
It started as a raymarcher, which is where the name comes from, and the name is wrong now.
The shape is an icosahedron, and an icosahedron is 20 flat faces. That makes it the overlap of 20 flat cuts through space. When the shape is only planes you don’t have to march toward it in careful steps. You solve for exactly where the ray crosses each one.
The 20 faces come out of the golden ratio. The directions the faces point are all sign flips of four base directions built from that ratio. The golden ratio shows up whenever something has fivefold symmetry, and an icosahedron is about as fivefold-symmetric as a shape gets. So the code carries four directions, flips their signs eight ways each to get every face, and intersects the ray with all of them. Entry is the last plane you cross going in. Exit is the first one going out. The ray can’t step past the surface at a glancing angle, because it isn’t stepping.
You get the surface normal for free too. The face a point sits on is whichever face direction it lines up with most. A raymarcher has to sample the shape in six directions to estimate which way the surface faces. This doesn’t.
Light that never gets averaged
Inside the stone, every time a ray hits a facet it splits. Some light bounces back in, some passes through and leaves, and Fresnel’s equations decide how much goes each way based on the angle. A path tracer flips a weighted coin at each bounce, follows one path, and averages a thousand coin flips to land on the real ratio.
This one doesn’t flip a coin. At each facet it does both. It takes the fraction of light that escapes right then, sends it out to sample whatever’s around, and adds that in. Then it keeps the fraction that stayed inside, follows only that reflected ray onward, and does the same thing at the next facet. It carries a running number for how much light is still bouncing around, and every bounce peels off the part that leaks out.
This works in one ray instead of a branching tree because the escaping light doesn’t need to be traced. It leaves and reads whatever’s out there. Only the reflected part continues, and that’s a single chain. So you get the exact energy split at every bounce with no coin flips to average out, using the real dielectric Fresnel equations rather than the cheap approximation most shaders use.
What makes it fast
None of the individual tricks are exotic. Stacked up, they’re the difference between a render that crawls and one that runs live in a browser tab.
The unit of work is a plane test. Finding where a ray hits the stone means checking it against all 20 faces, done as four base directions flipped eight ways, so 32 tests per intersection. Every bounce inside is another 32. Left alone, 16 bounces times two wavelengths is over a thousand plane tests per pixel. At a few million pixels that stops being real time fast.
So it doesn’t get left alone. Each bounce keeps a running tally of how much light is still trapped inside and peels off the part that escapes. The moment the trapped fraction drops below about four percent, the ray quits. A diamond dumps most of its light out quickly, so on a real frame most rays finish in a handful of bounces rather than sixteen. There’s a hard cap at 16 as a backstop, but the energy cutoff is what actually keeps the loop short.
It also never reads memory it doesn’t have. There’s no accumulation buffer, nothing kept from the last frame, and nothing that reads neighboring pixels to denoise. Every pixel is a self-contained calculation that starts and finishes inside one shader invocation. Millions of them run at once with nothing to coordinate, which is how a GPU wants to work.
The two knobs are wavelengths per pixel and that bounce cutoff. Drop to one wavelength and you halve the work. The whites get a touch noisier and nothing else changes. Building quality into space instead of into time means you can trade sharpness for speed and the picture degrades gracefully instead of dissolving into static.
Real prism colors
Turning a wavelength into a color on screen is its own rabbit hole, and getting it wrong tints the whole gem.
A single wavelength, say 500 nanometers, has a real color to a human eye. That mapping is measured and standardized, and it’s called the CIE color matching functions. The shader uses a compact math fit of those curves to go from wavelength to the eye’s raw response, then a matrix to turn that into the red, green and blue your monitor speaks.
White was the part I had to get right. If you sample a bunch of wavelengths evenly and add up their colors you’d hope for white, and you don’t get it, because the curves aren’t balanced evenly across the three channels. So I pre-scale the matrix so an even spread of all wavelengths lands exactly on white. That one adjustment is why a flat-lit facet reads as clean white instead of muddy yellow. It holds no matter how many samples per pixel you use, so the white point doesn’t drift when you change the quality setting.
Why the faces are flat
I tried to make it fancier and it taught me why the flat shape was right all along.
The one-sample-per-pixel approach depends on flat faces, and I didn’t see that until I broke it. I tried a torus, a rounded box, a squircle, all smooth curved shapes. They all did the same thing. Flat parts stayed clean and curved parts exploded into colored static. I chased that noise for a while assuming my intersection math had a bug. It didn’t.
A curved surface at diamond’s index of refraction acts like a lens. It focuses rays hard, so two rays that started one pixel apart can end up somewhere completely different after a few bounces. Neighboring pixels genuinely have wildly different brightness, and one sample per pixel can’t capture something that changes that fast, so it shows up as speckle. Dropping the index of refraction down toward plain glass calmed it right down, which confirmed the diagnosis. It’s real physics that needs a lot of samples to resolve, and a lot of samples is the thing this approach exists to avoid.
Flat faces don’t focus light. Parallel rays going in stay roughly parallel, so a pixel and its neighbor stay similar, so one sample is enough. The icosahedron wasn’t only a nice-looking choice. The flatness is load-bearing. I’d like to say I knew that going in.
The small stuff
A few things that didn’t need their own section.
The camera sits far back with a long lens, which is how people actually photograph jewelry. It flattens the perspective and keeps the whole stone in a tight frame. The background is pure black, which is also the jewelry-photo move.
The gem spins on three axes at once, at speeds set by powers of the golden ratio so the rates never line up. The tumble never repeats. And you can grab it. Click and drag rotates it like a turntable, left-right spins it, up-down tips it toward you, and it goes back to spinning on its own when you let go.
All the tracing happens in the gem’s own frame. Instead of rotating the stone, the shader rotates the camera ray and the light into the stone’s coordinates once, up front, and traces against a gem that’s sitting still. Small thing, but it means the spin costs almost nothing.
Looking back
This one turned into a long argument with myself about noise. Every feature I wanted, dispersion and internal reflections and curved shapes, was really a question of where the noise was allowed to live. If you refuse to average over time or space, the noise has to go somewhere, so the craft is putting it where you can’t see it. The Bayer dither puts it in a pattern your eye blends away. The deterministic Fresnel takes it out of the light bounces. And the flat faces sidestep the one place it can’t be hidden at all, which is curved focusing.
I came in wanting a pretty diamond. The thing I’d actually defend is the shape choice, which I made for the wrong reasons and only understood later.