# March Until You Hit Something

*Task 3 of 6 · [Ray-Marched Metaballs](https://gpu.rocks/learn/ray-marched-metaballs-8b1282bd.md) · GPU.js Learn*

Now the third dimension. Every pixel fires a ray straight into the screen
(orthographic: origin `(wx, wy, -2.5)`, direction `(0, 0, 1)`), and
the SDF turns finding the surface into a beautiful trick called **sphere tracing**:
the distance at your current point is a *guaranteed-safe step size* — nothing can be
closer than that. So step exactly that far, re-evaluate, repeat.

Near a surface the distance shrinks toward zero, so the march converges right onto the
skin. When `d` drops below a small epsilon, that ray has hit. Rays that miss just
keep flying — after a fixed number of steps you paint them background. GPUs need that fixed
bound: every thread runs the same loop, so give it `48` iterations and let hits
simply stop making progress.

## Figures

- **the field value is a promise — hop exactly that far and you'll never overshoot**

## Goal

**Goal:** write the ray-marching loop — 48 steps of
`t += d` — and paint hit pixels pink, misses dark blue.

## Requirements

- Loop a fixed 48 times; sample the field at `(wx, wy, -2.5 + t)`
- Flag a hit when `d < 0.01`
- Step forward by the distance itself: `t += d`
- Hits get `this.color(0.98, 0.63, 0.89, 1)`, misses the background color

## Hint 1 — the loop shape

Two state variables before the loop: `let t = 0.0;` and
`let hit = 0.0;`. Inside: evaluate `sceneDist`, set
`hit = 1.0` when close enough, then advance `t`. After the loop,
color by `hit`.

## Hint 2 — the loop, spelled out

```js
for (let i = 0; i < 48; i++) {
  const d = sceneDist(wx, wy, -2.5 + t, sep, r, k);
  if (d < 0.01) hit = 1.0;
  t += d;
}
```

## Same idea elsewhere

The fixed bound is a gpu.js/WebGL constraint — shader loop bounds must be
static, so we guard instead of `break`. Real marchers on CUDA, WGSL or
Shadertoy *do* break on a hit, and it pays off whenever a whole warp hits or
misses together. The durable lesson is about *divergence*: warps execute in
lockstep, so a warp runs as long as its slowest thread.

## Starter code

```js
// Sphere tracing: the SDF value IS a safe step size. Step, sample, repeat.
const gpu = new GPU({ mode });

gpu.addFunction(function smin(a, b, k) {
  const h = Math.max(k - Math.abs(a - b), 0.0) / k;
  return Math.min(a, b) - h * h * k * 0.25;
});

// The full 3D metaball field: two spheres at (±sep, 0, 0), blended by k.
gpu.addFunction(function sceneDist(x, y, z, sep, r, k) {
  const d1 = Math.sqrt((x + sep) * (x + sep) + y * y + z * z) - r;
  const d2 = Math.sqrt((x - sep) * (x - sep) + y * y + z * z) - r;
  return smin(d1, d2, k);
});

const marchScene = gpu.createKernel(function (sep, r, k) {
  const wx = (this.thread.x - 32) / 16;
  const wy = (this.thread.y - 32) / 16;
  // TODO: march! Start at t = 0 and repeat 48 times:
  //   d = sceneDist(wx, wy, -2.5 + t, sep, r, k)
  //   if d < 0.01 → this ray has hit the surface
  //   step forward: t += d
  // This only checks the starting plane — nothing is that close, so
  // every pixel comes out background until you write the loop:
  let hit = 0.0;
  if (sceneDist(wx, wy, -2.5, sep, r, k) < 0.01) hit = 1.0;

  if (hit > 0.5) this.color(0.98, 0.63, 0.89, 1);
  else this.color(0.02, 0.03, 0.06, 1);
}, { output: [64, 64], graphical: true });

await marchScene(0.55, 0.5, 0.3);
render(marchScene.canvas);
```

---

Interactive version: https://gpu.rocks/learn/ray-marched-metaballs-8b1282bd/3

[Previous task](https://gpu.rocks/learn/ray-marched-metaballs-8b1282bd/2.md) · [Next task](https://gpu.rocks/learn/ray-marched-metaballs-8b1282bd/4.md)
