# Where the Ray Hits

*Task 2 of 5 · [Progressive Path Tracing: Noise Melting Into an Image](https://gpu.rocks/learn/path-tracing-c99efc67.md) · GPU.js Learn*

Before anything can bounce, a ray has to find a surface. The sibling module
*Ray-Marched Metaballs* walks its rays forward in safe hops, because a signed
distance field is all it has to go on. A sphere gives you something better: an exact answer,
one square root long.

Write the ray as `o + t·d` with `d` a **unit** vector,
put `e = o − centre`, and asking "where does the ray meet the surface" is asking
when `|e + t·d|² = r²`. Because `d` is unit, the `t²`
coefficient is exactly 1 and the whole thing collapses to

```js
b = e · d
c = e · e - r * r
t = -b ± Math.sqrt(b * b - c)
```

Three cases, and all three matter. A negative discriminant means the ray misses. Two
roots mean it goes in one side and out the other — the one you can *see* is the
nearer, `−b − √(b² − c)`. And a root at or behind `t = 0` is in the
ray's past: the sphere is behind the camera, and it must not be reported as a hit. Reject
anything up to `t = 0.001` rather than just the negatives — the next task fires
rays that start *on* a surface, and in float32 such a ray finds that surface again
at a `t` a hair above zero.

## Goal

**Goal:** finish `sphereT` so it returns the distance to the
nearest visible hit, or `-1` when there isn't one.

## Requirements

- Compute `b = e·d` and `c = e·e − r * r` for `e = o − centre`
- Return `-1` when the discriminant `b * b - c` is not positive
- Otherwise return the NEAR root `-b - Math.sqrt(disc)` — and `-1` instead when that root is at or behind the ray's origin (`t <= 0.001`)

## Hint 1 — the shape of the function

Three early exits and one answer:

```js
const disc = b * b - c;
if (disc <= 0) {
  return -1;          // the ray misses this sphere entirely
}
const t = -b - Math.sqrt(disc);
if (t <= 0.001) {
  return -1;          // the hit is behind us (or right on top of us)
}
return t;
```

## Hint 2 — why 0.001 and not 0

The next task fires rays that start *on* a surface. In float32 the
starting point is never quite on it, and a root at `t = 0.000001` is that
same surface hitting itself. The small epsilon is what stops a bounce from being
immediately swallowed by the thing it bounced off.

## Hint 3 — what the picture should look like

Nearer is brighter. You should get a pale sphere sitting on a floor that fades
with distance, and the flat background colour everywhere the ray found nothing.

## Same idea elsewhere

An RTX or RDNA ray-tracing core is silicon that does exactly this — traversal plus
an intersection test — a few billion times a second, and the API shape around it (DXR's
closest-hit shader, OptiX's `rtTrace`, Vulkan's ray query) is still one thread,
one ray, one nearest hit. Analytic primitives never went away either: spheres, boxes and
capsules still get intersected in closed form because closed form is faster than a mesh.

## Starter code

```js
// One ray per pixel, and the exact distance to the sphere it hits.
const gpu = new GPU({ mode });

// Nearest hit of a unit-direction ray against a sphere, or -1 for a miss.
gpu.addFunction(function sphereT(ox, oy, oz, dx, dy, dz, cx, cy, cz, r) {
  const ex = ox - cx;
  const ey = oy - cy;
  const ez = oz - cz;
  // TODO: b = e·d, c = e·e - r*r, disc = b*b - c.
  // Return -1 if disc <= 0, else the near root -b - Math.sqrt(disc),
  // and -1 again if that root is <= 0.001 (behind the ray).
  return -1;
});

gpu.addFunction(function planeT(oy, dy, planeY) {
  if (dy > -0.0001) {
    return -1;
  }
  const t = (planeY - oy) / dy;
  if (t <= 0.001) {
    return -1;
  }
  return t;
});

const depth = gpu.createKernel(function (cx, cy, cz, r) {
  // this pixel's camera ray
  const u = ((this.thread.x + 0.5) / 64) * 2 - 1;
  const v = ((this.thread.y + 0.5) / 64) * 2 - 1;
  let dx = u;
  let dy = v;
  let dz = -1.7;
  const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
  dx = dx / len;
  dy = dy / len;
  dz = dz / len;

  const tS = sphereT(0, 0.3, 1.5, dx, dy, dz, cx, cy, cz, r);
  const tP = planeT(0.3, dy, -0.5);
  if (tS > 0 && (tP < 0 || tS < tP)) {
    return tS;
  }
  return tP;
}, { output: [64, 64] });

const paint = gpu.createKernel(function (t) {
  const d = t[this.thread.y][this.thread.x];
  if (d < 0) {
    this.color(0.10, 0.12, 0.17, 1);
  } else {
    const g = 1 - Math.min(Math.max((d - 1.2) / 1.6, 0), 1);
    this.color(g, g, g, 1);
  }
}, { output: [64, 64], graphical: true });

const ts = await depth(-0.45, 0, -0.4, 0.5);
await paint(ts);
render(paint.canvas);

let hits = 0;
let nearest = 999;
for (let y = 0; y < 64; y++) {
  for (let x = 0; x < 64; x++) {
    if (ts[y][x] > 0) {
      hits++;
      nearest = Math.min(nearest, ts[y][x]);
    }
  }
}
console.log(hits, 'of 4,096 rays hit something; nearest hit at t =', nearest);
```

---

Interactive version: https://gpu.rocks/learn/path-tracing-c99efc67/2

[Previous task](https://gpu.rocks/learn/path-tracing-c99efc67/1.md) · [Next task](https://gpu.rocks/learn/path-tracing-c99efc67/3.md)
