---
title: "The Prompt I Used to Build a Punch-Through Ice Wall in Three.js (Voronoi Shatter + Refracted Hero Reveal)"
description: "A copy-pasteable prompt for a landing page sealed behind frosted mint ice — punch it five times and the wall fractures into ~90 real Voronoi shards that tumble past the camera, refracting the hero text as they fly. Two-pass render target refraction, hand-rolled fracture, no physics engine."
date: "2026-08-10"
type: "prompt"
reel: "Db3VNa3SQsb"
tags: []
legacy: true
---
<h2>Overview</h2>
      <p>
        You land on the page and there is a sheet of frosted mint-green ice
        between you and it. Behind the frost, blurred and bent, you can
        <em>almost</em> read a name. So you click. A hole punches through with
        straight cracks snapping outward, the camera kicks, and glass cracks in
        your speakers. Four more hits and the whole wall lets go — roughly ninety
        real polygonal shards tumbling toward the lens, growing as they come,
        flying past your head and out of frame. Behind them, the hero finally
        resolves sharp.
      </p>
      <p>
        The idea is the fourth wall. The glass isn't decoration sitting on top of
        the page, it's <em>in front of</em> the page, and the visitor breaks it
        themselves. Everything in the prompt below serves that one sentence.
      </p>
      <p>
        The whole thing is <a href="https://threejs.org" rel="nofollow">three.js</a>
        — real meshes, shaders and render targets. No physics engine, no CSG
        library, no textures loaded from disk.
      </p>

      <div class="highlight">
        <p>
          <strong>What it produces:</strong> a full-viewport frosted pane that
          genuinely refracts the scene behind it, five escalating punches that
          paint holes and cracks into a damage texture, a hand-rolled Voronoi
          fracture into ~90 shards launched at the camera, WebAudio glass sounds
          synthesised from noise buffers, and a DOM hero that cross-fades in once
          the wall is gone.
        </p>
        <p style="margin-bottom: 0">
          <strong>Stack:</strong> three.js, <code>troika-three-text</code>,
          <code>EffectComposer</code> with bloom and a custom grain/barrel pass.
          Five files. Every tunable in one <code>CONFIG</code> object.
        </p>
      </div>

      <h2>The one idea that makes it work</h2>
      <p>
        The instinct is to float a transparent WebGL canvas over your normal HTML
        and let the glass sit on top. That cannot work, because WebGL cannot
        refract DOM. You end up reaching for <code>backdrop-filter</code>, and
        <code>backdrop-filter</code> gives you a blur — not refraction, not
        chromatic aberration, and definitely not per-shard distortion.
      </p>
      <p>
        So the page renders <strong>twice</strong>. The hero text and background
        live in their own scene, rendered into a render target. Then the ice is
        rendered to the screen, and its fragment shader samples that render target,
        offsetting the lookup by the frost's surface normal. Now the glass is
        bending an actual image of the actual page. Aberration and blur-behind-glass
        come free once that path exists.
      </p>
      <p>
        The corollary is the part worth protecting: <strong>keep sampling the live
        render target after the wall breaks.</strong> The shards are still running
        the ice shader, so as they tumble away they refract the newly-revealed hero
        in real time. It's tempting to freeze a snapshot for performance. Don't —
        that one detail is the best frame in the whole sequence.
      </p>

      <h2>The prompt</h2>
      <p>
        Paste everything below into Replit Agent, Lovable, v0, Bolt or Cursor as a
        single message. It is written to be executed rather than interpreted — the
        shader code, the fracture algorithm and the specific numbers are what stop
        an agent handing you a grey box with a <code>console.log</code> in it. If
        your tool has a planning step, let it plan, then tell it to build the whole
        thing in one pass.
      </p>
      <p>
        <strong>One thing to change before you paste:</strong> the
        <em>Hero content</em> block near the end has my name and copy in it. Swap
        it for yours.
      </p>

      <div class="prompt">Build a WebGL landing page with **three.js**. Real 3D — meshes, shaders, render targets,
post-processing. Do **not** implement this with a 2D canvas, CSS filters, or an SVG fallback.

## Stack

- `three` (latest), ES modules, `import * as THREE from 'three'`
- From `three/addons/`: `EffectComposer`, `RenderPass`, `UnrealBloomPass`, `ShaderPass`, `OutputPass`
- `troika-three-text` for the hero type (so the glass can actually refract it)
- Vite if the environment supports it; otherwise a single `index.html` with a
  `&lt;script type="importmap"&gt;` pointing at `https://unpkg.com/three@latest/build/three.module.js`
  and `https://unpkg.com/three@latest/examples/jsm/`
- **No physics library.** No `cannon-es`, no `rapier`, no `ammo`. The motion is 40 lines of
  hand-rolled Euler integration and a physics engine will only fight the art direction.
- **No CSG library.** The fracture is 2D Voronoi + triangulation, described below.
- No images, no textures loaded from disk. Everything procedural.

Files: `index.html`, `main.js`, `ice.glsl.js` (exported vertex/fragment strings),
`fracture.js`, `style.css`. Every tunable number lives in one `CONFIG` object at the top of
`main.js`.

## The concept

The visitor lands facing a full-screen sheet of frosted mint-green ice. Behind it, blurred and
refracted, they can *almost* read a name. They click to punch the ice. Each punch opens a hole
with straight angular cracks radiating out. After five punches the entire wall shatters into
real Voronoi shards that tumble **toward the camera and past it**, out of frame — and the hero
resolves into focus behind them.

The whole point is the fourth wall. The glass is between the viewer and the page, and they
break it with their own hand. Every choice below serves that.

## Architecture — two-pass render (this is the important part)

Naive approach: transparent WebGL canvas over DOM content. **Don't.** WebGL can't refract DOM,
so you'd end up faking it with `backdrop-filter` and the whole thing falls flat.

Instead:

    sceneWorld  →  renderTarget (half resolution is fine)
       ├─ background plane, procedural aurora shader
       ├─ hero text (troika-three-text)
       └─ ambient + one soft point light

    sceneIce  →  screen (via EffectComposer)
       ├─ fullscreen quad sampling renderTarget  (the world, undistorted)
       └─ the ice: one PlaneGeometry, then N shard meshes after the break
          → their fragment shader samples renderTarget

So: render `sceneWorld` into `rtWorld` each frame. Then render `sceneIce`, whose ice material
reads `rtWorld` as `uScene` and offsets its lookup by the ice surface normal. That gives you
genuine refraction, chromatic aberration, and blur-behind-glass for free.

**Keep sampling the live render target after the shatter.** As the shards tumble away they'll
refract the revealed hero text in real time. That is the money shot of the whole page — do not
freeze a snapshot to "optimise" it.

Camera: `PerspectiveCamera(50, aspect, 0.1, 100)` at `z = 5`. Size the ice plane to exactly
fill the frustum at `z = 0` (compute it: `h = 2 * tan(fov/2) * 5`, `w = h * aspect`) and
recompute on resize. Perspective, not orthographic — the shards need real depth to fly past
the lens.

## The ice material

`ShaderMaterial`, `transparent: true`, `depthWrite: false`.

Uniforms: `uScene` (the world RT), `uDamage` (damage texture, below), `uTime`,
`uResolution`, `uMouse`, `uOpacity`, `uTintA` `#dff3ec`, `uTintB` `#7fd6ac`,
`uTintC` `#5fc0a2`, `uRefract` 0.045, `uAberration` 0.006, `uFrostScale` 3.2.

The **damage texture** is the one place a 2D canvas is legitimate: keep an offscreen
`canvas` at viewport resolution wrapped in a `THREE.CanvasTexture`, and paint damage into it
on each punch. Channel layout:

- **R** = hole mask (white where the ice is gone → shader alpha goes to 0)
- **G** = crack intensity (drives a specular ridge and a normal kink)
- **B** = bruise / micro-fracture haze around impacts (whitens the frost, no hole yet)

Set `texture.needsUpdate = true` only on the frames a punch happens.

Fragment shader — implement essentially this:

    varying vec2 vUv;
    uniform sampler2D uScene, uDamage;
    uniform float uTime, uRefract, uAberration, uFrostScale, uOpacity;
    uniform vec3 uTintA, uTintB, uTintC;

    // hash / value noise / fbm — standard 3-octave fbm, write it inline
    float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
    float noise(vec2 p){ /* smoothstep-interpolated value noise */ }
    float fbm(vec2 p){ float a=.5, s=0.; for(int i=0;i&lt;4;i++){ s+=a*noise(p); p*=2.03; a*=.5; } return s; }

    void main() {
      vec3 dmg = texture2D(uDamage, vUv).rgb;

      // ---- frost surface normal ----
      // two fbm samples at slightly offset uvs give a cheap gradient
      float e = 1.5 / 1000.0;
      float f  = fbm(vUv * uFrostScale * 40.0);
      float fx = fbm((vUv + vec2(e,0.)) * uFrostScale * 40.0);
      float fy = fbm((vUv + vec2(0.,e)) * uFrostScale * 40.0);
      vec2 grad = vec2(fx - f, fy - f) * 40.0;

      // cracks kink the normal hard — that's what makes them catch light like glass
      grad += (dmg.g * 2.2) * normalize(grad + 0.001);

      // ---- refraction with chromatic aberration ----
      vec2 off = grad * uRefract;
      float r = texture2D(uScene, vUv + off * (1.0 + uAberration)).r;
      float g = texture2D(uScene, vUv + off).g;
      float b = texture2D(uScene, vUv + off * (1.0 - uAberration)).b;
      vec3 behind = vec3(r, g, b);

      // blur-behind-glass: mix in 4 wide taps, weighted by how frosted this pixel is
      vec3 diffuse = vec3(0.0);
      for (int i = 0; i &lt; 4; i++) {
        float a = float(i) * 1.5707 + uTime * 0.02;
        diffuse += texture2D(uScene, vUv + off + vec2(cos(a), sin(a)) * 0.012).rgb;
      }
      behind = mix(behind, diffuse * 0.25, 0.75);

      // ---- the ice itself ----
      float marble = fbm(vUv * uFrostScale * 1.6 + uTime * 0.01);
      vec3 ice = mix(uTintC, uTintB, marble);
      ice = mix(ice, uTintA, pow(f, 1.6));
      ice = mix(ice, vec3(1.0), dmg.b * 0.5);            // bruised = whiter

      vec3 col = mix(behind, ice, 0.78);                  // 0.78 = how opaque the frost reads

      // long thin skate scratches
      float scr = smoothstep(0.975, 1.0, fbm(vUv * vec2(2.0, 90.0) + 5.0));
      col += scr * 0.16;

      // crack specular: bright core, mint bloom shoulder
      col += dmg.g * vec3(0.85, 1.0, 0.94) * 1.6;

      // rim light around holes so broken edges catch the light
      float rim = smoothstep(0.35, 0.5, dmg.r) - smoothstep(0.5, 0.72, dmg.r);
      col += rim * vec3(0.8, 1.0, 0.92) * 1.4;

      // pane vignette
      col += pow(length(vUv - 0.5) * 1.4, 3.0) * 0.22;

      float alpha = (1.0 - smoothstep(0.42, 0.58, dmg.r)) * uOpacity;
      gl_FragColor = vec4(col, alpha);
    }

Vertex shader: pass `vUv`, and additionally displace `position.z` by
`-dmg.b * 0.06` so bruised regions dish inward slightly. Sample the damage texture in the
vertex shader for that (needs enough plane subdivision — use `PlaneGeometry(w, h, 120, 120)`).

## Punching

Raycast from the pointer onto the ice plane. On `pointerup`:

1. **Paint the damage texture** at the hit UV:
   - Hole: an irregular 16-vertex blob, radius `CONFIG.holeRadius * (0.75 + hits*0.16)`,
     vertex radius jitter only `0.80–1.14` of nominal. Fill into the **R** channel with a
     3px blur. Deep notches make it look like a starfish, not broken glass.
   - Bruise: a soft radial gradient ~2.2× the hole radius into **B**.
   - Cracks into **G** — read the next section, it's the detail everyone gets wrong.
2. **Displace the mesh**: push the plane vertices near the impact backward by
   `-0.12 * falloff` for one frame, then spring them back over ~180ms. The wall should
   visibly *take* the hit.
3. **Spawn debris**: ~18 small shard meshes (random 3–6 gon `ShapeGeometry`, ~0.02–0.09 world
   units) flying outward from the impact, plus ~30 additive point sprites as sparks.
4. **Shake the camera**: offset `camera.position` by random ±`0.02 * strength` and roll
   `camera.rotation.z` by ±`0.004`, decaying over 400ms. Shake the *camera*, never a CSS
   transform on the canvas.
5. **Flash + sound**: bump a `uFlash` uniform in the final pass to 0.4 and decay it. Synthesise
   the sound with WebAudio, no files: a 0.4s white-noise buffer with `(1-t)^5` decay through a
   highpass sweeping 900→3500 Hz as damage accumulates, layered with a sine thud
   150 Hz → 42 Hz. Rising pitch each hit. Mute toggle in the corner.
6. Escalate the centred hint text: `PUNCH THE ICE` → `AGAIN` → `HARDER` → `ALMOST THROUGH`.
   Tick an `INTEGRITY 100%` readout down toward zero.

## Cracks — get this right or the illusion dies

Glass cracks are **straight and angular**. Organic wandering lines read as lightning, roots, or
veins, and instantly kill it.

- ~13 radial arms from the impact point.
- Each arm is built from long rigid segments of **26–54 px**, with only **±0.14 rad** of
  direction drift per segment. Long and stiff.
- With ~22% probability per segment, a splinter forks off at a **sharp 0.45–1.0 rad** angle and
  dies within half the parent arm's remaining length. Thinner stroke.
- Then **3 concentric rings** joining the arms — this is the actual signature of impacted glass.
  20–28 segments each, radius jitter ±7%. **They must stay inside the radial reach**
  (`ring_radius ≈ arm_length * (0.26 + i * 0.21)`). Rings drawn larger than the arms look like
  giant floating hexagons and destroy the effect.
- Stroke into the **G** channel with a small blur so the shader gets a soft ridge, not aliased
  hairlines.

## The collapse — Voronoi fracture

Trigger after `CONFIG.hitsToBreak` (5) punches.

In `fracture.js`, implement Voronoi by **half-plane clipping**, not a library:

    // for each seed i: start with the full plane rect as a polygon,
    // then clip it by the perpendicular bisector against every other seed j
    function clipHalfPlane(poly, normal, midpoint) { /* Sutherland–Hodgman */ }

~90 seeds, biased toward the final impact point (sample `r = pow(random(), 0.6)` around it) so
the break originates where they hit. 90 seeds × 89 clips is instant — do it once, on the click.

Turn each cell polygon into a mesh:

- Fan-triangulate the convex cell from its centroid.
- **UVs must be the original plane UVs** (`uv = (localXY / planeSize) + 0.5`), so every shard
  carries the exact patch of wall it came from and keeps refracting correctly. Getting this
  wrong is the #1 way this looks cheap.
- Recentre each shard's geometry on its centroid and put the centroid in the mesh's `position`,
  so it rotates about itself.
- Same ice `ShaderMaterial`, cloned, with a per-shard `uOpacity`.
- Optional and worth it: `ExtrudeGeometry` with `depth: 0.012` and a tiny bevel so shard edges
  catch the light as they tumble.

Then hide the intact plane and animate every shard:

    direction   = normalize(shardCentroid - impactPoint)
    boost       = clamp(0.9 / distance, 0.5, 3.2)     // near the fist = launched hardest
    velocity.xy = direction * rand(0.6, 2.2) * boost
    velocity.z  = rand(0.9, 3.4) * boost              // POSITIVE — toward the camera
    angularVel  = random on all three axes, ±3 rad/s
    gravity     = -3.2 units/s² on y

Shards get **bigger as they approach the lens** — that's the perspective camera doing the work,
and it's what makes it feel like the wall broke *at you* rather than away from you. Let them
fly past `camera.z` and out of frame; cull each shard once `z &gt; 6` or its opacity hits 0. Fade
`uOpacity` over ~1.1s.

Simultaneously: hard camera shake, bloom intensity spikes to 2.2 and eases back to 0.6, and a
low WebAudio rumble (1.6s brown-noise buffer through a 320 Hz lowpass).

## Hero reveal

While the wall is intact, the hero already exists in `sceneWorld` as `troika-three-text` — that
is *why* the glass has something to refract, and why the visitor can half-read a name and wants
to break through. Keep it slightly out of focus by rendering `rtWorld` at 0.5× and letting the
shader's blur taps do the rest.

On collapse:

1. Ease `rtWorld` scale to 1.0 and the shader's diffuse mix to 0 — the world sharpens.
2. Animate the text material opacity and a per-glyph `translateY` in, staggered ~35 ms apart.
3. Ease the background plane's scale 1.06 → 1.0, a slow settling exhale.
4. **Then cross-fade the troika text out and a real DOM hero in**, absolutely positioned over
   the canvas. WebGL text is not selectable, not readable by a screen reader, and not
   crawlable. The DOM version is the one that ships — the WebGL one exists only to be
   refracted. Do this; don't skip it because it's two implementations of the same words.
5. Show a `REFREEZE` button that resets damage texture, shard list, hit count and material
   uniforms so it can be replayed.

### Hero content

    eyebrow   PORTFOLIO // 2026
    h1        Avi Vashishta          light weight, clamp(38px, 10.5vw, 150px), per-letter stagger
    rule      thin mint gradient line, scales in from centre
    role      SOFTWARE DEVELOPMENT ENGINEER      uppercase, letter-spacing .5em, mint
    tagline   one muted line
    buttons   "View work" / "Get in touch" as pill outlines

Type: `Space Grotesk` for display, `JetBrains Mono` for the HUD. Palette: void `#04110d`,
deep `#0a2a22`, mint accent `#7cf0b4`, ice `#dff3ec`.

## Post-processing

`EffectComposer`: `RenderPass` → `UnrealBloomPass(strength 0.6, radius 0.8, threshold 0.72)`
→ custom `ShaderPass` (barrel distortion 0.015, RGB shift 0.0012, film grain 0.035, vignette,
plus the `uFlash` white additive) → `OutputPass`.

Grain and a hair of barrel distortion are what make it read as *looking through something*
rather than at a flat render. Don't drop them.

## Details that matter

- **No native cursor over the canvas.** Draw a reticle: a thin ring mesh (or an additive
  sprite) tracking the pointer at `z = 0.1`, radius easing down ~35% on `pointerdown` so it
  reads as a fist closing. Add four small tick marks that slowly rotate.
- `renderer.setPixelRatio(Math.min(devicePixelRatio, 2))`. Half-res render target.
- Resize: rebuild the damage canvas, re-stamp stored impacts (keep `{uv, strength}` for each),
  recompute plane size from the frustum, resize the RT and the composer.
- Touch: `touchend` punches. On mobile drop to 45 shards, bloom off, no barrel pass.
- `prefers-reduced-motion`: no camera shake, no pulsing hint, one click breaks it.
- Target 60fps on an M1 MacBook Air and ≥30 on a 3-year-old Android.
- The hint, integrity meter and mute toggle live in a DOM `#hud` overlay, not in WebGL.

## Do not

- Do not use `MeshPhysicalMaterial` with `transmission` for the wall. It's the obvious
  reach and it can't do the crack normals, the hole mask, or the frost marbling. (It *is* fine
  for the small debris shards if you want them cheap.)
- Do not add a physics engine, a CSG library, or GSAP-driven DOM animation of the shards.
- Do not let the shards fly *away* from the camera. The direction is the entire idea.
- Do not make the cracks curve.
- Do not ship WebGL-only text.

## Acceptance checklist

Before you tell me it's done, verify each of these yourself:

1. On load: a frosted mint wall fills the viewport, and a name is faintly legible behind it.
2. Moving the pointer shows a custom reticle; the OS cursor is hidden.
3. First click: a hole appears with **straight** radial cracks and visible concentric rings
   inside the crack reach. Camera shakes. A crack sound plays.
4. Through the hole, the hero text is visibly **sharper** than through the ice — proving the
   refraction path is wired to the render target and not faked.
5. Clicks 2–4 escalate: bigger holes, longer cracks, higher-pitched crack sound, integrity
   dropping.
6. Click 5: the full wall becomes ~90 distinct polygonal shards. They tumble, **grow as they
   approach**, and leave the frame. Each shard visibly refracts what's behind it while flying.
7. Hero animates in sharp, letters staggered, and the final text is selectable with the mouse.
8. `REFREEZE` fully restores the intact wall and it's replayable.
9. Resizing the window doesn't stretch, blur, or misalign the ice, and existing damage survives.
10. No console errors. No WebGL warnings about render target feedback loops.</div>

      <h2>Why each part is in there</h2>

      <h3>The damage texture, and why a 2D canvas is allowed here</h3>
      <p>
        Everything else in this build is explicitly not-a-2D-canvas, but damage is
        the exception. An offscreen canvas wrapped in a
        <code>CanvasTexture</code> is the right tool: you get
        <code>arc</code>, <code>lineTo</code> and blur for free, and you only pay
        for it on the handful of frames where a punch actually lands.
      </p>
      <p>
        Packing three separate maps into one texture's channels is what keeps it
        cheap — <strong>R</strong> is the hole mask that drives alpha to zero,
        <strong>G</strong> is crack intensity that kinks the surface normal and
        lights a specular ridge, <strong>B</strong> is the bruise haze that whitens
        the frost and dishes the geometry inward without cutting a hole yet. One
        texture fetch in the shader, three different jobs.
      </p>

      <h3>Cracks must be straight, and this is the whole illusion</h3>
      <p>
        This is the clause I would fight hardest for. An agent left to its own
        devices draws cracks as wandering organic curves, and the instant they
        curve they stop reading as glass — they read as lightning, or roots, or
        veins. Real impacted glass is <strong>long, rigid and angular</strong>.
      </p>
      <p>
        Hence the very specific numbers: segments of 26–54px with only ±0.14 rad of
        drift each, splinters forking at a sharp 0.45–1.0 rad. And then the detail
        that sells it more than anything — <strong>three concentric rings</strong>
        joining the radial arms. That ladder pattern is the actual signature of a
        point impact. Miss it and you have a starburst; include it and people
        recognise it without knowing why.
      </p>
      <p>
        The prompt also constrains the rings to stay <em>inside</em> the radial
        reach, because the obvious implementation draws them at whatever radius and
        you end up with giant floating hexagons sitting outside the cracks.
      </p>

      <h3>Voronoi by half-plane clipping, in about 20 lines</h3>
      <p>
        You do not need a CSG library to break a flat plane. For each seed, start
        with the full rectangle as a polygon and clip it by the perpendicular
        bisector against every other seed — Sutherland–Hodgman, once per pair. 90
        seeds against 89 others is 8,000 clips of tiny polygons, which is
        instantaneous, so it runs on the click with no pre-bake.
      </p>
      <p>
        Biasing the seeds toward the final impact point with
        <code>pow(random(), 0.6)</code> is a one-line touch that matters a lot: the
        break visibly originates where the last punch landed, with fine shards near
        the fist and bigger slabs out at the edges.
      </p>

      <h3>The shard UVs are the #1 way this looks cheap</h3>
      <p>
        Each shard must carry the <em>original plane UVs</em>, not fresh 0–1 UVs of
        its own outline. Get it wrong and every shard maps the whole wall onto
        itself, so the frost pattern and the refraction visibly pop the moment the
        wall breaks. Get it right and each piece keeps refracting exactly the patch
        of world it was covering a frame earlier, and the transition is invisible.
      </p>

      <h3>Toward the camera, never away</h3>
      <p>
        <code>velocity.z</code> is positive. The shards fly at the lens and past
        it, and because the camera is perspective they grow on approach. That's
        what makes it feel like the wall broke <em>at you</em>. Send them away
        instead and you get a tidy, entirely forgettable dissolve — same code, all
        the impact gone. It's in the "Do not" list for a reason.
      </p>

      <h3>Two implementations of the same words, on purpose</h3>
      <p>
        The hero exists twice: as <code>troika-three-text</code> inside the render
        target, which is the thing the glass has to refract, and as real DOM that
        cross-fades in after the collapse. That looks redundant and agents will try
        to save you the trouble by shipping only the WebGL one.
      </p>
      <p>
        Don't let it. WebGL text can't be selected, isn't read by a screen reader
        and isn't crawlable — which for a portfolio landing page is the entire
        point of having a name on it. The WebGL copy exists to be broken; the DOM
        copy is what ships.
      </p>

      <h3>Grain and barrel distortion are not garnish</h3>
      <p>
        A hair of barrel distortion (0.015) plus film grain (0.035) is the
        difference between "looking through something" and "looking at a flat
        render". They're the cheapest credibility in the whole pipeline and the
        first things an agent drops when trimming.
      </p>

      <h2>Things to watch for</h2>
      <ul>
        <li>
          <strong>Render target feedback loops.</strong> The ice samples
          <code>rtWorld</code>, so the ice must never be in
          <code>sceneWorld</code>. If WebGL warns about reading and writing the
          same target in one pass, something drifted between the two scenes.
        </li>
        <li>
          <strong>The vertex displacement needs subdivision.</strong>
          <code>PlaneGeometry(w, h)</code> defaults to a single quad, so sampling
          damage in the vertex shader does nothing at all. The
          <code>120 × 120</code> in the prompt is load-bearing.
        </li>
        <li>
          <strong>Resize has to re-stamp the damage.</strong> The damage canvas is
          viewport-sized, so a resize wipes it unless you keep every
          <code>{uv, strength}</code> and repaint. Cheapest correct approach is to
          store the impacts, not the pixels.
        </li>
        <li>
          <strong>Shake the camera, not the canvas.</strong> A CSS transform on the
          canvas shakes the post-processing and the DOM HUD along with it, which
          instantly reads as a webpage wobbling rather than an impact.
        </li>
        <li>
          <strong>Hole blobs want low jitter.</strong> 0.80–1.14 of nominal radius.
          Any more and the deep notches make it a starfish instead of broken glass.
        </li>
      </ul>

      <h2>Recap</h2>
      <ul>
        <li>
          Render the world into a target and let the ice shader sample it. That is
          the difference between refraction and a blur filter.
        </li>
        <li>
          Keep sampling it live after the break, so the shards refract the hero as
          they fly.
        </li>
        <li>
          Straight cracks with concentric rings. Curved cracks are lightning.
        </li>
        <li>
          Voronoi is half-plane clipping and about 20 lines. Skip the library.
        </li>
        <li>
          Shard UVs come from the original plane, or the illusion pops on the first
          frame of the collapse.
        </li>
        <li>
          Positive Z. The wall breaks at the viewer, not away from them.
        </li>
        <li>
          Ship the DOM hero even though the WebGL one already says the same thing.
        </li>
      </ul>

      <p>
        If you want more prompts in this shape, there is
        <a href="/blog/prompt-kinetic-wind-wall-threejs-vertex-shader"
          >a kinetic wind wall of 2,000 blades moved entirely in the vertex
          shader</a>,
        <a href="/blog/prompt-frosted-glass-refraction-cube-webgl"
          >a frosted-glass refraction cube in raw WebGL</a>,
        <a href="/blog/prompt-fluid-ink-reveal-wordmark-higgsfield"
          >a fluid ink-reveal homepage with a Higgsfield-generated wordmark
          video</a>, and
        <a href="/blog/prompt-hover-reveal-spaceship-scanner-higgsfield"
          >a hover-reveal spaceship hull scanner</a>.
      </p>
