← Learn · Prompt · 27 July 2026 · 9 min

The Prompt I Used to Build a Frosted-Glass Refraction Cube in Raw WebGL (One HTML File)

A copy-pasteable prompt for a frosted-glass cube floating in front of giant type, where the letters genuinely refract and smear through the glass — raymarched two-bounce refraction, chromatic aberration and film grain in a single index.html with no build step, no npm and no Three.js.

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.

Build a single-file HTML page (index.html, no build step, no npm packages, no Three.js — raw WebGL only) that renders a frosted-glass 3D cube floating in front of giant type, where the type visibly refracts and smears through the glass. LAYOUT & TYPE - Full-viewport black background (#060606). No scrolling, no UI chrome. - Background layer is drawn on an offscreen 2D canvas, then uploaded as a WebGL texture: - The words "AVI VASHISHTA" in one line, centered, uppercase, ultra-bold condensed sans (900 weight, Helvetica Neue / Arial / system sans fallback), fill #d2d2d2, scaled so the word spans ~92% of the viewport width. - Behind it, a "ghost stack": 3 outline-only copies of the same word (transparent fill, 1–2px stroke, rgba(255,255,255,0.055)), each scaled 4–8% larger than the solid word and offset on BOTH axes (e.g. up-left, down-right, far-up) so they bleed past the letterforms. - Add fine monochrome film grain to the whole background canvas by walking the ImageData and adding ±27 to each RGB channel. - Redraw and re-upload this canvas on resize. THE CUBE (all in one fullscreen fragment shader, drawn on a single fullscreen triangle) - Raymarch a rounded-box SDF: half-extent 0.62, corner radius 0.03, centered exactly at world origin (0,0,0) so it sits dead center of the viewport. Camera at (0,0,3.0) looking down -Z, focal 1.9. - Rotate the cube by transforming the sample point with an inverse rotation mat3 (Rx(-pitch) * Ry(-yaw)) passed in as a uniform. Start at yaw 0.62, pitch 0.46 so a corner faces the viewer. - TWO-BOUNCE REFRACTION (this is the important part — do not fake it with a blur): 1. March from the camera to the front surface, compute the normal via central differences. 2. refract() the view ray into the glass with IOR 1.48. 3. March INSIDE the solid (step on the negated SDF) until you reach the back surface, compute that normal. 4. refract() out again (fall back to reflect() on total internal reflection). 5. Intersect that exit ray with a plane at z = -3.2, then project the hit point back through the same camera to get a UV into the background texture. That projected UV is what makes the type genuinely bend. - FROSTED look: - Perturb both surface normals with 3D value noise (frequency ~26, amplitude ~0.10) so the refraction wobbles like ground glass. - Sample the background at the refracted UV with a 16-tap golden-angle spiral blur, radius 0.010 + 0.055 * fresnel, jittered per pixel by a hash so it dithers instead of banding. Correct the offsets for aspect ratio so the blur stays circular. - Fresnel = pow(1 - dot(-rayDir, normal), 3.5). Use it to: widen the blur, brighten the rim (+0.14), and drive chromatic aberration — offset the R and B taps in opposite directions along the refraction displacement vector by 0.004 + 0.020 * fresnel, so corners get iridescent color fringes. - Tint the transmitted color vec3(0.90, 0.93, 0.97), add a purple-ish iridescent bloom on grazing angles, and one specular highlight: pow(max(dot(reflect(rayDir, normal), normalize(vec3(-0.55,0.85,0.55))), 0.), 34.) * 0.55. - Finally, over the entire frame (cube and background alike): hash-based film grain at ±0.027 and a subtle radial vignette. MOTION & INTERACTION - Idle: slow continuous yaw drift (~0.0038 rad/frame) plus a gentle sinusoidal pitch bob. - Pointer drag rotates the cube (pointerdown/move/up with pointer capture); on release, momentum carries and decays at 0.94 per frame. Clamp pitch to ±1.25. Cursor grab / grabbing. - Small monospace "DRAG TO ROTATE" label bottom-left, letter-spaced, rgba(255,255,255,0.32). QUALITY - Size the canvas to clientWidth/Height * min(devicePixelRatio, 2); update viewport and re-render the text texture on resize. - Expose a JS config object at the top of the script for: text string, ghost copy count, ghost offset/scale/opacity, cube size, IOR, frost blur amount, chromatic aberration amount, grain strength, and rotation speed — so all of it is tunable in one place. - Log shader compile/link errors to the console. If WebGL is unavailable, show a plain fallback message.

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.