UV warp with sine
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
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).
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);
}