# Normals Without Geometry

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

Lighting needs surface normals, and a mesh would hand them to you per-vertex. We
have no mesh — but we have something better. The normal of an implicit surface is the
**gradient** of its distance field: the direction in which distance grows
fastest is exactly "straight off the surface".

Estimate it with **central differences**: nudge the hit point by a tiny
`e` along each axis, sample the field on both sides, subtract. Six extra field
evaluations, then normalize. The classic way to sanity-check normals is to paint them:
`n * 0.5 + 0.5` maps each component into color range — a head-on surface
(normal `(0, 0, -1)`, pointing at the camera) renders as
`rgb(0.5, 0.5, 0)`, that mustard-olive tone every graphics programmer knows.

## Figures

- **nudge ± e, subtract, normalize — six taps and the surface points at you**

## Goal

**Goal:** at each hit point, compute the finite-difference normal of
`sceneDist` and paint each component as `n * 0.5 + 0.5` —
hint 2 has the exact `this.color` call.

## Requirements

- Remember the hit distance: record `tHit` at the *first* hit
- Sample ± `e = 0.01` along x, y and z around the hit point
- Normalize the three differences with `Math.sqrt`
- Paint `n * 0.5 + 0.5`; misses keep the background color

## Hint 1 — one axis at a time

The x component before normalizing is

```js
sceneDist(wx + e, wy, pz, …) - sceneDist(wx - e, wy, pz, …)
```

where `pz = -2.5 + tHit`. Same pattern for y and z.

## Hint 2 — normalize and paint

```js
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
this.color(nx / len * 0.5 + 0.5,
  ny / len * 0.5 + 0.5,
  nz / len * 0.5 + 0.5, 1);
```

## Same idea elsewhere

Gradient-by-central-differences is the same stencil you'd write in a CUDA fluid
solver or a ROCm heightfield pipeline, and Metal deferred renderers reconstruct normals
from depth buffers with exactly this two-sided sampling.

## Starter code

```js
// The normal of an SDF surface is its gradient. Six samples buy it.
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;
});

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 showNormals = gpu.createKernel(function (sep, r, k) {
  const wx = (this.thread.x - 32) / 16;
  const wy = (this.thread.y - 32) / 16;
  let t = 0.0;
  let hit = 0.0;
  let tHit = 0.0;
  for (let i = 0; i < 48; i++) {
    const d = sceneDist(wx, wy, -2.5 + t, sep, r, k);
    if (hit < 0.5) {
      if (d < 0.01) {
        hit = 1.0;
        tHit = t;
      }
    }
    t += d;
  }
  if (hit > 0.5) {
    const pz = -2.5 + tHit;
    // TODO: central differences with e = 0.01 around (wx, wy, pz),
    // normalize (nx, ny, nz), then paint n * 0.5 + 0.5.
    this.color(1, 1, 1, 1);
  } else {
    this.color(0.02, 0.03, 0.06, 1);
  }
}, { output: [64, 64], graphical: true });

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

---

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

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