# Turn On the Light

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

With normals in hand, lighting is one dot product. **Lambert's law**:
a surface facing a light head-on catches full brightness; tilt it away and brightness falls
with the cosine of the angle — which is exactly `n · l`, the dot product of the
unit normal and the unit direction *toward* the light. Clamp it at zero so surfaces
facing away go dark instead of negative.

Two finishing touches make it look right: an **ambient floor** of
`0.15` so shadowed sides stay readable, and an albedo tint — multiply the final
brightness into the metaball's pink `(1.0, 0.62, 0.86)`.

## Goal

**Goal:** light each hit point with
`c = 0.15 + 0.85 * Math.max(nx * lx + ny * ly + nz * lz, 0)` and paint
`this.color(c, c * 0.62, c * 0.86, 1)`.

## Requirements

- Take the dot product of the normal with the light direction `(lx, ly, lz)`
- Clamp with `Math.max(dot, 0.0)` — no negative light
- Apply the ambient floor: `c = 0.15 + 0.85 * diff`
- Tint by the albedo: `this.color(c, c * 0.62, c * 0.86, 1)`

## Hint 1 — sanity-check the center

The center pixel's normal is `(0, 0, -1)` and the light is
`(-0.6, 0, -0.8)`, so the dot product is `0.8` and
`c = 0.15 + 0.85 × 0.8 = 0.83` — a bright, not-quite-white pink.

## Hint 2 — the two lines

```js
const diff = Math.max(nx / len * lx + ny / len * ly
  + nz / len * lz, 0.0);
const c = 0.15 + 0.85 * diff;
```

## Same idea elsewhere

`max(dot(n, l), 0.0)` is character-for-character the same in GLSL,
WGSL, HLSL and Metal Shading Language — Lambert diffuse may be the single most portable
line of shading code in existence.

## Starter code

```js
// Lighting is a dot product: brightness = how squarely you face the light.
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 shadeScene = gpu.createKernel(function (sep, r, k, lx, ly, lz) {
  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);
    // TODO: Lambert — dot the unit normal with (lx, ly, lz), clamp at 0,
    // then c = 0.15 + 0.85 * diff. Flat 1.0 means "fully lit everywhere":
    const c = 1.0;
    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 });

// light direction: up-left of the camera, pointing at the scene
await shadeScene(0.55, 0.5, 0.3, -0.6, 0, -0.8);
render(shadeScene.canvas);
```

---

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

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