Cobra Developers
Blog

How our homepage draws a field of snake scales with one WebGPU shader

The background of our homepage is a field of snake scales. A serpent swims through it, and scales lift under your pointer. There are no images, no video, and no 3D library. It is one fragment shader written in WGSL, about ninety lines, running on your graphics card through WebGPU.

We built it partly because it suits the name and partly to learn WebGPU properly on something we would have to ship. This is how it works.

One triangle, every pixel

The setup is the simplest thing WebGPU can do. We draw a single triangle large enough to cover the whole canvas, and a fragment shader runs once for each pixel inside it. The shader gets the pixel's position and returns a color. Everything you see comes out of that one function.

The vertex stage is three hardcoded points:

@vertex
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
  var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
  return vec4f(p[i], 0.0, 1.0);
}

No vertex buffers, no meshes. The only data we send each frame is a 32 byte uniform block: canvas size, pointer position, time, scale size, and whether the page is in dark mode.

Tiling the scales

Scales overlap like roof shingles. Each one is a disc, the rows are half a scale apart, and every other row is shifted sideways by half a scale. A higher row always sits on top of the row below it.

For a given pixel, the question is which disc owns it. The answer is the highest row whose disc contains the pixel, so the shader checks at most four candidate rows from the top down and stops at the first hit:

let j0 = i32(ceil((q.y - r) / 0.5));
for (var n = 0; n < 4; n++) {
  let j = j0 + n;
  let off = 0.5 * f32(((j % 2) + 2) % 2);
  let cx = round(q.x - off) + off;
  let dv = q - vec2f(cx, f32(j) * 0.5);
  if (length(dv) < r) { /* this scale owns the pixel */ break; }
}

The disc radius is slightly more than half a scale, which guarantees there are no gaps. Once we know the owning scale, we know the pixel's position inside it, and that is enough to draw the outline, a ridge down the middle, and a gradient toward the tip.

A serpent with no geometry

The snake is not an object. It is a function that answers one question for each scale: how lifted are you, from zero to one?

The spine is two sine waves added together and moving through time. A scale's distance from that curve decides whether it is part of the body. The width of the body changes along its length: a flare just behind the head for the hood, then a long taper to the tail.

let hood = exp(-pow((a - 0.07) / 0.06, 2.0));
let taper = mix(1.0, 0.12, smoothstep(0.45, 1.0, a));
let hw = u.s * (1.5 + 2.4 * hood) * taper;

Here a runs from zero at the head to one at the tail. The important detail is that lift is evaluated at the center of each scale, not at each pixel. That makes whole scales light up together, which is why it reads as a body moving under a skin rather than a blurry glow.

The pointer works the same way. It adds a soft disc of lift around the cursor plus one ring that breathes in and out. The final lift is the largest of the three sources.

Matching the page exactly

A canvas that is slightly the wrong color looks like a rectangle pasted on the page. The shader's paper and ink colors are the same values as the CSS variables, and it receives a flag for dark mode, so switching themes flips the canvas in the same frame.

A little noise, about one percent, is added at the end. Without it, the flat areas band on some displays.

Being a good citizen

A shader that runs forever is a battery drain. Three guards keep it polite:

  • An IntersectionObserver stops the render loop when the hero scrolls out of view.
  • The loop stops when the tab is hidden.
  • If the visitor has asked their device for reduced motion, we render a single still frame.

The device pixel ratio is capped at two. On a very dense phone screen the extra pixels cost real power and nobody can see the difference.

When WebGPU is not there

WebGPU ships in current Chrome, Edge, and Safari, and in Firefox on some platforms. That still leaves real visitors without it, so the page starts with a plain CSS version of the scale pattern built from two repeating radial gradients. If the adapter request succeeds, the canvas replaces it. If anything fails, the CSS stays and a line of text says so plainly.

We also check the shader's compilation messages before using it. A shader that fails to compile should fall back, not show a black box.

Was it worth it

The whole effect is one small JavaScript module with no dependencies. It added no library to the page and loads after the text is already readable. For a site whose job is to show what we can build, that seemed like the right trade.

You can read the source in your browser's developer tools. Nothing on this site is minified. If you want something like it, or something much more useful built with the same care, get in touch.

guest@cobra: ~