Shader Lab
06.UV warp

UV warp with sine

Arc 2 · 2D Screen-Space · Lesson 6 of 35
coordinate signal color
0.02
1.20
20.0

Purpose

Adding a time-driven sine wave to the UV before sampling produces a wobbling, rippling, drifting image — the first demonstration that the coordinate layer plus a clock makes motion, with nothing else needed.

Key insight

The coordinate layer doesn't have to be static. If the offset you add to each UV is itself a function of time, the image moves — even though the source scene is a single still frame. A sine of uv.y plus time pushes each row of pixels left and right on a schedule; the whole image appears to wobble horizontally.

Change what's inside the sine and you change the shape of the motion: sine of distance-from-center gives ripple rings; sine of UV plus a scrolling noise gives heat haze. The source of the wave is what directs the motion; the sine just turns that source into oscillation.

Break it

Crank wobble to the maximum and let speed run. UVs now regularly exceed 0 and 1, so you see whatever happens past the scene's edge — stretched pixels, wrapped copies, or dead space. Teaches: UV space is a bounded rectangle. A warp doesn't invent new image content; it just asks the scene about locations the scene may or may not have answers for. Knowing your sampler's edge behavior is part of directing any distortion effect.

Direct Claude

"gentle ripple / underwater" wobble low, speed low "heat haze above the fire" wobble low, speed medium, plus a mask limiting it to the hot region "broken-signal shake / glitch" wobble higher, speed high "vertical ripple instead of horizontal" swap which UV axis the sine reads from "more waves across the screen" increase the multiplier inside the sine (frequency)
Meta-phrase you gain here: "time-driven coordinate offset." The general class this lesson teaches, encompassing every ripple, wobble, shimmer, and drift you'll ever need to brief.
Combines with: L3 (sin primitive, generalized), L7 (chromatic aberration is often applied to a wobbling image for extra analog feel), L12 (a CRT's tiny scanline-synced wobble is a UV warp on the horizontal scanline phase).
the fragment shader running above
uniform float u_wobble;
uniform float u_speed;
uniform float u_frequency;

vec3 scene(vec2 uv) {
  // Off-screen reads return flat space so the warp edges are visible.
  if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
    return vec3(0.04, 0.03, 0.08);
  }
  vec3 col = mix(vec3(0.18, 0.10, 0.28), vec3(0.95, 0.48, 0.32), uv.y);
  col = mix(col, vec3(0.25, 0.60, 0.85), smoothstep(0.55, 0.98, uv.y));
  float sun = 1.0 - smoothstep(0.09, 0.11, length(uv - vec2(0.5, 0.55)));
  col = mix(col, vec3(1.0, 0.93, 0.68), sun);
  float mntn = 0.40 + 0.05 * sin(uv.x * 18.0) + 0.08 * sin(uv.x * 5.0 + 2.0);
  col = mix(col, vec3(0.10, 0.06, 0.20), step(uv.y, mntn) * step(uv.y, 0.5));
  if (uv.y < 0.5) {
    float yy = (0.5 - uv.y);
    float hz = smoothstep(0.004, 0.0, abs(fract(pow(yy, 0.7) * 8.0) - 0.5));
    float vl = smoothstep(0.004, 0.0, abs(fract((uv.x - 0.5) / max(yy, 0.02) * 4.0) - 0.5));
    col = mix(col, vec3(0.95, 0.30, 0.75), max(hz, vl) * 0.9);
  }
  return col;
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;

  // COORDINATE LAYER — offset x by a sine whose phase depends on y + time.
  // Rows at different y values wobble out of sync, producing the ripple.
  float phase = uv.y * u_frequency + u_time * u_speed * 2.0;
  uv.x += sin(phase) * u_wobble;

  gl_FragColor = vec4(scene(uv), 1.0);
}