# Soft Shadows, Full Scene

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

The payoff. A point is in shadow when something sits between it and the light —
and you already own the tool that answers that: *march again*, from the hit point
toward the light. Here's Inigo Quilez's beautiful upgrade: instead of a binary blocked/clear,
track how *closely* the shadow ray grazes the scene. The running minimum of
`3 · d / t` (distance over travel) is ≈1 when the ray stays clear, 0 when it's
blocked, and slides smoothly between when it grazes — a free penumbra.

The kernel takes a `shadowOn` switch so you can A/B it: with the light swung
low to the left, the left lobe casts a soft-edged shadow across the neck of the blob. One
more march, and a scene with no triangles anywhere gets real cinematography.

The driver also hands you a **separation** slider, which is task 2's point
made in one gesture: the program is a pure function of its controls, so dragging it re-runs
the whole thing. Slide the two centres together and `smin` welds them into a
single blob; pull them apart and the neck thins, necks down, and snaps. Solve the task
first — it defaults to the `0.55` the scene has always used — then drag it.

## Goal

**Goal:** add the shadow march — 32 steps from the hit point toward the
light, penumbra factor `sh = Math.min(sh, 3.0 * d / st)` — and scale the diffuse
term by it when `shadowOn` is 1.

## Requirements

- Start the shadow ray at `st = 0.06` so it clears its own surface
- Sample at `(wx + lx·st, wy + ly·st, pz + lz·st)`, 32 steps
- Keep the running minimum `sh = Math.min(sh, 3.0 * d / st)`, clamped ≥ 0
- Advance with `st += Math.max(d, 0.02)`, then shade `c = 0.15 + 0.85 * diff * sh`

## Hint 1 — why 3 · d / t?

At travel distance `st`, a field value `d` means the ray
passes within `d` of an occluder. The ratio `d / st` is the sine
of the "clearance angle" from the surface point — small angle, deep penumbra. The 3
just sets how sharp the shadow edge is.

## Hint 2 — the loop, spelled out

```js
let sh = 1.0;
let st = 0.06;
for (let j = 0; j < 32; j++) {
  const d = sceneDist(wx + lx * st, wy + ly * st,
    pz + lz * st, sep, r, k);
  sh = Math.min(sh, 3.0 * d / st);
  st += Math.max(d, 0.02);
}
sh = Math.max(sh, 0.0);
```

And remember to only apply it when
`shadowOn > 0.5`.

## Same idea elsewhere

Secondary rays are the moment ray-marching meets real-time ray tracing: shadow
rays are exactly what RTX/DXR hardware accelerates, and this penumbra estimate ships in
countless WGSL and GLSL engines as the cheap alternative when you can't afford one.

## Starter code

```js
// One more march — from the surface toward the light — buys shadows.
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 finalScene = gpu.createKernel(function (sep, r, k, lx, ly, lz, shadowOn) {
  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;
    const e = 0.01;
    const nx = sceneDist(wx + e, wy, pz, sep, r, k) - sceneDist(wx - e, wy, pz, sep, r, k);
    const ny = sceneDist(wx, wy + e, pz, sep, r, k) - sceneDist(wx, wy - e, pz, sep, r, k);
    const nz = sceneDist(wx, wy, pz + e, sep, r, k) - sceneDist(wx, wy, pz - e, sep, r, k);
    const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
    const diff = Math.max((nx * lx + ny * ly + nz * lz) / len, 0.0);

    let sh = 1.0;
    if (shadowOn > 0.5) {
      // TODO: march toward the light from st = 0.06, 32 steps:
      //   d = sceneDist(wx + lx * st, wy + ly * st, pz + lz * st, sep, r, k)
      //   sh = Math.min(sh, 3.0 * d / st)
      //   st += Math.max(d, 0.02)
      // then clamp: sh = Math.max(sh, 0.0)
    }

    const c = 0.15 + 0.85 * diff * sh;
    this.color(c, c * 0.62, c * 0.86, 1);
  } else {
    this.color(0.02, 0.03, 0.06, 1);
  }
}, { output: [64, 64], graphical: true });

// A dial, not a constant: slider() re-runs the whole program when you drag it,
// so this is the gap between the two centres. Drag it to 0 and smin has nothing
// left to blend; drag it past ~0.58 and the neck finally snaps and you have two
// spheres again. Everything between is the whole module in one gesture.
const separation = slider('separation', { min: 0, max: 1.1, value: 0.55, step: 0.01 });

// light swung low to the left — the left lobe should shade the neck
await finalScene(separation, 0.5, 0.3, -0.86, 0, -0.51, 1);
render(finalScene.canvas);
```

---

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

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