Overview
You have seen this effect on agency sites and in Spline demos: giant type across a black page, a slab of frosted glass floating in front of it, and the letters visibly bending, smearing and splitting into colour fringes as they pass through the glass.
It is usually a 3D tool export, or Three.js with a transmission
material. Neither is necessary. Below is a single prompt that produces
it as one index.html file — no build step,
no npm, no Three.js, raw WebGL — where the refraction is genuinely
raymarched rather than faked with a blur.
What it produces: full-viewport black page, "AVI VASHISHTA" in ultra-bold condensed caps with a stack of ghosted outline copies bleeding behind it, film grain over everything, and a rounded frosted cube floating dead centre that refracts the type through two surfaces. Drag to rotate, release and it carries momentum.
Stack: nothing. One HTML file, one 2D canvas for the background, one fullscreen fragment shader for the cube. Paste it into Replit, Claude, or any single-file host and it runs.
The prompt
Paste the whole thing at once. It is long on purpose — the specificity is what makes it a one-shot rather than a twenty-round negotiation. Swap the text string for your own name.
Why the prompt is written this way
Most of the length is doing one job: closing off the cheap shortcuts a model will otherwise take. Five clauses are carrying the result.
"Do not fake it with a blur"
Ask for "frosted glass" and you will get a blurred copy of the background clipped to a cube silhouette. It looks fine in a still and completely wrong in motion, because a blur does not displace anything — rotate the cube and the letters underneath stay exactly where they were.
Real glass moves them. That needs the light path traced through both surfaces: refract on the way in, march to the back wall, refract on the way out. Naming the five steps explicitly is what stops the model collapsing it into a one-surface approximation.
The projection step is the whole trick
Step 5 is the part people miss. After the second refraction you have a ray leaving the glass in some new direction — but a 2D background texture has no depth, so there is nothing for that ray to hit.
The fix is to give it something: put a plane behind everything at
z = -3.2, intersect the exit ray with it, then
project that hit point back through the same camera to
get a texture coordinate. Because the exit ray is bent, the coordinate
you land on is not the one under the pixel — and that displacement,
pixel by pixel, is the type visibly bending. Any wrong-looking
refraction almost always traces back to this projection rather than to
the refract() calls.
Frost is normal noise, not blur radius
Ground glass scatters because its surface is microscopically rough. The prompt reproduces that by perturbing both surface normals with 3D value noise before refracting, so neighbouring pixels bend slightly differently. The 16-tap blur is a secondary softening on top. Turning the blur up without the normal noise gives you smooth frosted plastic; the noise is what makes it read as glass.
The golden-angle spiral with a per-pixel hash jitter matters for the same reason a dither pattern matters — 16 taps in a fixed ring produce visible rings, and 16 jittered taps produce grain your eye reads as texture.
Fresnel drives three things at once
Rather than tuning rim brightness, blur width and colour fringing independently, all three are driven off the same Fresnel term. Grazing angles automatically get a brighter edge, a wider blur, and stronger chromatic aberration — which is physically what happens and, more usefully, means there is only one number to tune when it looks off.
The chromatic aberration is deliberately offset along the refraction displacement vector rather than in a fixed screen direction. That is why the fringes follow the bend of the letters instead of sitting as a flat red/blue ghost.
Grain twice, on purpose
There is film grain baked into the background canvas
(±27 per channel in the ImageData) and a second pass over
the whole frame in the shader (±0.027). This is not
redundant. The first gets refracted along with the type, so the
grain inside the glass smears with everything else. The second sits flat
on the final image and ties the cube and the page together. Drop either
and the cube starts to look pasted on.
Knobs to turn first
The prompt asks for a config object at the top of the script, which is the difference between iterating in seconds and re-prompting for every tweak. The four worth reaching for:
- IOR (1.48). Water is ~1.33, glass ~1.5, diamond ~2.4. Push past 1.7 and the bending gets so aggressive the type stops being readable through the cube.
- Cube half-extent (0.62). Controls how much of the word it covers. Larger reads as a slab, smaller as a gem.
- Frost blur amount. Low values look like clear glass and make the refraction obvious; high values obscure the type. The interesting range is narrow.
- Ghost stack opacity (0.055). Easy to overdo. The outline copies should be nearly subliminal — you want to feel depth behind the word, not read four words.
If it comes out wrong
- The cube is a flat grey blob. The two-bounce march collapsed to one surface. Check that the inside march steps on the negated SDF — inside the solid the distance field is negative, and stepping on the raw value walks backwards forever.
- The type does not move when the cube rotates. The projection in step 5 is being skipped and the shader is sampling the background at the plain screen UV. That is exactly the blur-fake the prompt is written to prevent.
- Concentric rings inside the glass. The blur taps are not jittered. Add the per-pixel hash to the spiral's starting angle.
- The blur is oval near the screen edges. The tap offsets are not aspect-corrected — divide the x offset by the aspect ratio.
- Everything is black. Check the console. A shader that fails to compile silently produces nothing, which is why the prompt asks for compile and link errors to be logged.
-
It is slow on a laptop. The cost is the 16 taps
multiplied by the device pixel ratio. The
min(dpr, 2)clamp is in the prompt for this reason; drop the tap count to 8 before you touch anything else.
Recap
-
The effect does not need a 3D tool or Three.js. A rounded-box SDF, two
refract()calls and a background texture get you there in one file. - Say "do not fake it with a blur" out loud in the prompt, then enumerate the march steps. Otherwise you will get the blur.
- Refract to a plane, then project back through the camera. That step is what makes the letters bend.
- Roughen the normals with noise for frost; the blur alone reads as plastic.
- Drive rim, blur width and colour fringing from one Fresnel term so there is one number to tune.
- Ask for a config object up front. Every minute spent on that is ten minutes not spent re-prompting.
If you want more prompts in this shape, there is a drivable 3D car game, a 3D Flappy Bird, and a wind chime app in one HTML file.