Command Palette

Search for a command to run...

Hectal
Course outline
Topic 5.2Intermediate ~18 min · step 2 of 7

How Light Is Computed

In one line

The GPU finds a pixel's brightness with one multiplication: how directly the surface faces the light (a dot product), times the light's strength. You build the number by hand, compare it with what the GPU drew, and add the shiny spot on top.

0/7 · 0%

Think of it like this

Hold a book up to a lamp. Face the lamp straight on and the cover is bright. Tilt it and it gets darker. Turn the edge to the lamp and it is dark. The brightness follows the angle between the cover's direction and the lamp.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

normal
An arrow of length 1 pointing straight out of a surface. Lighting is computed from it.
dot product
A multiplication of two arrows. For two arrows of length 1 it equals the cosine of the angle between them: 1 for the same direction, 0 for a right angle.
diffuse
The matt part of light: it spreads in all directions and depends on N · L only.
ambient
A flat, directionless amount of light added everywhere.

Step by step

01Play with the light by hand

The widget shows a Phong ball. Turn ambient, diffuse and specular on one at a time. Move the light round the ball (azimuth) and up and down (elevation) and watch N · L for the point facing you.

02Turn the plates

The example turns six white plates 0°, 30°, 60°, 80°, 90° and 120° away from one light that shines straight at the front.

03Predict

The code computes N · L with normal.dot(toLight), with the normal found by turning (0, 0, 1) with the plate's rotation, and converts it to the byte the screen shows.

04Compare

predicted and measured agree for the first four rows: 255, 239, 188, 116. The last two rows print 11 because the GPU drew the background there (see the edge cases above).

Try it yourself

  1. 1

    Make the light weaker

    Change Math.PI to 1 in the light line. The numbers in measured fall: 153, 143, 111 for 0°, 30°, 60°. Intensity 1 gives a linear brightness of 1/π = 0.318, which is 153 in sRGB. The prediction column is now wrong because it assumed π; multiply the prediction by 1/π to fix it.

  2. 2

    Move the light into the plates

    Change the light position to (0, 0, 0). The plates you can see (0° to 80°) measure 0: the light is at its own target, so it has no direction. They are drawn, just unlit, so you see black and not the background 11.

Code & diagrams

Try it: interactive

Lighting decomposer

Toggle ambient, diffuse and specular, then move the light and watch N · L for the front point.

Predict the GPU: N · L against real pixels New tab

Open the scene tree to see the six plates. The prediction needs no GPU; the measured column is read back from the canvas.

idle

Console

Run the scene to see its console.

Expected console output

angle  N.L    predicted  measured
  0°   1.000      255         255
 30°   0.866      239         239
 60°   0.500      188         188
 80°   0.174      116         116
 90°   0.000        0          11
120°   0.000        0          11

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Move the light to the side and expect the same picture

Change the light position to light.position.set(5, 0, 0).

terminal
$ Run
── what you'll see ──
angle N.L predicted measured
0° 0.000 0 0
30° 0.500 188 188
60° 0.866 239 239
80° 0.985 253 253
90° 1.000 255 11
120° 0.866 239 11
The 0° plate, which faced the light before, is now black. The 90° plate faces the light but is edge-on to the camera, so the camera sees nothing: 11 is the background.

Myth vs fact

Myth

Light intensity 1 is full brightness.

Fact

With the physical units three.js uses, intensity 1 on a white matt plate facing the light gives 1/π of full linear brightness, 153 on screen. Intensity π gives full white.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The 1/π is the Lambert BRDF: a matt surface spreads incoming light over a half-sphere, and the maths divides by π so no energy is created. In r155 three.js set useLegacyLights to false by default; that old mode scaled intensities by π, so old forum answers say 'intensity 1 is white' and new ones don't.

  • ▸

    MeshLambertMaterial and MeshPhongMaterial use the same diffuse N · L. Phong adds a specular term (R · V)^shininess, where R is the reflected light and V points to the camera. Standard replaces it with a physically based specular term (Topic 5.3).

Remember this

  1. 1

    The problem this solves: lighting looks like magic until you see it is arithmetic. Compute one pixel by hand and you can predict a scene, find why a surface is dark and know what intensity does.

  2. 2

    The surface normal. A normal is an arrow of length 1 that points straight out of a surface. A plane facing you has the normal (0, 0, 1). The normal decides how the surface meets the light, and Topic 4.4 showed how to make good ones.

  3. 3

    Diffuse light: N · L. Let L be the arrow from the surface towards the light, length 1. The dot product N · L is the cosine of the angle between the two arrows: 1 when they match, 0.5 at 60°, 0 at 90° and negative beyond it. The GPU uses max(0, N · L): a surface cannot be lit from behind. This is diffuse light, the matt part of a surface.

  4. 4

    Ambient light and specular light. Ambient light is a flat amount added everywhere so shadows are not pure black; it ignores direction. Specular light is the bright spot: the mirror-like reflection of the light, which depends on where the camera is too. A small shininess (Phong) or a small roughness (Standard, Topic 5.3) makes the spot small and bright.

  5. 5

    Then the number becomes a colour. The GPU multiplies the light colour, the light intensity and the surface colour by N · L. With DirectionalLight intensity π, a white Lambert plate facing the light shows exactly N · L as its linear brightness. The example below prints the prediction next to what the GPU drew. Colours are then converted to sRGB for the screen (Topic 5.6), which is why 0.5 prints as 188, not 128.

  6. 6

    Edge cases the numbers show. A plate turned 90° is edge-on: you see nothing of it. A plate turned 120° shows its back, which is culled, so the pixel is the background colour 11, not black. A light with no direction (at its own target) gives 0 everywhere.

Explain it without notes

01

Compute the diffuse term for a surface turned 60° from the light, and say what the screen shows for a white plate under a π-intensity directional light.

02

Why is a plate turned 120° the background colour and not black?

Practice

01

A plate faces +z and the light is at (3, 0, 4). Find N · L and the linear brightness under intensity π.

02

Write the code that finds the brightness of a surface at any angle deg from a light along +z, as a number between 0 and 1.

Done when you can

  • Say what a normal is and compute N · L for an angle

  • Say why a back-facing or edge-on plate is not lit

  • Say what ambient, diffuse and specular each add

  • Explain why intensity π, not 1, gives a full-white matt plate