# One Path Per Pixel, and It Looks Terrible

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

Here is the whole idea of path tracing. Fire a ray from the eye through a pixel.
Where it lands, pick a *random* new direction and carry on. Multiply the surviving
light by each surface's albedo as you go, and when the path finally escapes into the sky,
that sky's brightness — scaled by everything it passed through — is your estimate of what
the pixel should be.

One estimate. It is *unbiased*: average enough of them and you get the exact
answer, no approximation anywhere. It is also, on its own, nearly worthless — one path is a
single sample of an integral over every direction light could possibly have arrived from,
and it is wrong by tens of percent.

The bounce itself is one line of geometry. Take a uniformly random point on the unit
sphere, add it to the surface normal, and normalize: that gives you a
**cosine-weighted** direction on the hemisphere — more likely to leave
straight up than sideways, which is exactly how a matte surface actually scatters. Because
the sampling density already matches the physics, no correction factor is needed; the
throughput just picks up the albedo and moves on.

## Figures

- **right on average, wrong every single time**

## Goal

**Goal:** finish the bounce — multiply the throughput by the surface's
`albedo`, then send the ray off along `normal + random unit vector`,
normalized.

## Requirements

- Multiply `through` by `albedo`, once per bounce
- Set the new direction to `nx + sx`, `ny + sy`, `nz + sz`
- Normalize it — `sphereT` and `planeT` both assume a unit direction

## Hint 1 — why add the normal at all

`(sx, sy, sz)` is a random point on the whole sphere, so half of
those directions point *into* the surface. Adding the unit normal shifts the
sphere so it sits tangent to the surface: every direction then points outward, and the
density of directions comes out proportional to the cosine of the angle from the
normal — which is Lambert's law, for free.

## Hint 2 — the six lines

```js
through = through * albedo;
dx = nx + sx;
dy = ny + sy;
dz = nz + sz;
len = Math.sqrt(dx * dx + dy * dy + dz * dz);
dx = dx / len;
dy = dy / len;
dz = dz / len;
```

## Hint 3 — what "right" looks like here

Grainy. Every pixel should be visibly wrong by itself, with soft shadows and
the contact between the spheres and the floor only *hinted* at through the
noise. If your image comes out smooth, the bounce is not random and the next task has
nothing to average.

## Same idea elsewhere

This loop — generate a ray, find the closest hit, sample a new direction, multiply
the throughput, repeat — is the core of every production renderer, from Cycles and Arnold to
the DXR sample code. The kernels get bigger (materials, textures, lights sampled directly)
but the shape is this, and the reason it is on a GPU is the reason it is here: every pixel's
path is independent of every other pixel's.

## Starter code

```js
// A whole path tracer. One sample per pixel, one frame — and it shows.
const gpu = new GPU({ mode });

// The whole random-number generator. It is a pure function: same state in,
// same number out, on every backend and every run.
gpu.addFunction(function nextRandom(state) {
  let p = state * 78.233 + 0.7213;
  p = p - Math.floor(p);
  p = p * (p + 61.7);
  return p - Math.floor(p);
});

// 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;
  const b = ex * dx + ey * dy + ez * dz;
  const c = ex * ex + ey * ey + ez * ez - r * r;
  const disc = b * b - c;
  if (disc <= 0) {
    return -1;
  }
  const t = -b - Math.sqrt(disc);
  if (t <= 0.001) {
    return -1;
  }
  return t;
});

// The floor: one division, no quadratic.
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;
});

// The only light in the scene is the sky itself.
gpu.addFunction(function skyLight(dy) {
  if (dy < 0) {
    return 0.3;
  }
  return 0.3 + 0.7 * dy;
});

const trace = gpu.createKernel(function (frame, spp) {
  // This thread's own stream: start from the frame, stir in the column,
  // then the row. Every stir goes through nextRandom, so two threads one
  // pixel apart come out completely unrelated.
  let seed = 0.1237 + frame * 0.0173;
  seed = nextRandom(seed + this.thread.x * 0.0713);
  seed = nextRandom(seed + this.thread.y * 0.0917);

  let total = 0;
  for (let s = 0; s < spp; s++) {
    // One camera ray, jittered inside this pixel — so the edges antialias
    // themselves for free as the frames pile up.
    seed = nextRandom(seed);
    const u = ((this.thread.x + seed) / 64) * 2 - 1;
    seed = nextRandom(seed);
    const v = ((this.thread.y + seed) / 64) * 2 - 1;
    let dx = u;
    let dy = v;
    let dz = -1.7;
    let len = Math.sqrt(dx * dx + dy * dy + dz * dz);
    dx = dx / len;
    dy = dy / len;
    dz = dz / len;
    let ox = 0;
    let oy = 0.3;
    let oz = 1.5;
    let through = 1;

    // Up to three segments: camera → surface → surface → sky.
    for (let d = 0; d < 3; d++) {
      const tA = sphereT(ox, oy, oz, dx, dy, dz, -0.45, 0, -0.4, 0.5);
      const tB = sphereT(ox, oy, oz, dx, dy, dz, 0.5, -0.2, -0.1, 0.3);
      const tP = planeT(oy, dy, -0.5);
      let t = 999;
      let which = -1;
      if (tA > 0 && tA < t) {
        t = tA;
        which = 0;
      }
      if (tB > 0 && tB < t) {
        t = tB;
        which = 1;
      }
      if (tP > 0 && tP < t) {
        t = tP;
        which = 2;
      }
      if (which < 0) {
        // Nothing in the way — the path ends in the sky, which is the light.
        total = total + through * skyLight(dy);
        break;
      }

      const hx = ox + t * dx;
      const hy = oy + t * dy;
      const hz = oz + t * dz;
      let nx = 0;
      let ny = 1;
      let nz = 0;
      let albedo = 0.55;
      if (which === 0) {
        nx = (hx + 0.45) / 0.5;
        ny = hy / 0.5;
        nz = (hz + 0.4) / 0.5;
        albedo = 0.85;
      }
      if (which === 1) {
        nx = (hx - 0.5) / 0.3;
        ny = (hy + 0.2) / 0.3;
        nz = (hz + 0.1) / 0.3;
        albedo = 0.3;
      }

      // A uniformly random point on the unit sphere, out of this thread's
      // stream: z is uniform on [-1, 1] and phi uniform around the axis.
      seed = nextRandom(seed);
      const z = 2 * seed - 1;
      seed = nextRandom(seed);
      const phi = 6.2831853 * seed;
      const rr = Math.sqrt(Math.max(1 - z * z, 0));
      const sx = rr * Math.cos(phi);
      const sy = rr * Math.sin(phi);
      const sz = z;

      // TODO: absorb, then bounce.
      //   1. the surface keeps some of the light: multiply `through` by `albedo`
      //   2. the new direction is the NORMAL plus that random unit vector,
      //      normalized — nx + sx, ny + sy, nz + sz, divided by its own length
      through = through * 1;
      dx = sx;
      dy = sy;
      dz = sz;
      len = Math.sqrt(dx * dx + dy * dy + dz * dz);
      dx = dx / len;
      dy = dy / len;
      dz = dz / len;

      // Step off the surface so the next ray does not hit it at t = 0.
      ox = hx + 0.001 * nx;
      oy = hy + 0.001 * ny;
      oz = hz + 0.001 * nz;
    }
  }
  return total / spp;
}, { output: [64, 64], loopMaxIterations: 8 });

// Radiance in, pixels out. The square root is a gamma curve: it is what
// makes a 0.2 and a 0.4 look as different as they are.
const paint = gpu.createKernel(function (image) {
  const g = Math.sqrt(Math.max(image[this.thread.y][this.thread.x], 0));
  this.color(g, g, g, 1);
}, { output: [64, 64], graphical: true });

const frame = await trace(0, 1);
await paint(frame);
render(paint.canvas);

let mean = 0;
for (let y = 0; y < 64; y++) {
  for (let x = 0; x < 64; x++) mean += frame[y][x];
}
console.log('mean radiance over the frame:', mean / 4096);
```

---

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

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