Command Palette

Search for a command to run...

Hectal
Course outline
Topic 4.4Beginner ~21 min · step 4 of 7

Normals

In one line

A normal is a short arrow, one per vertex, that says which way the surface faces. Light is worked out from it. Sharing or splitting vertices decides whether an edge looks smooth or sharp, and a missing, zero or inverted normal gives the most confusing lighting bugs.

0/7 · 0%

Think of it like this

Hold a sheet of card in sunshine. When it faces the sun it is bright; tilted away it gets darker; edge-on it is dark. How bright depends only on which way the card faces. A normal is that facing direction, written as an arrow sticking straight out of the surface.

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 at a vertex that points straight out of the surface. Lighting is worked out from it.
flat shading
Each triangle is one colour because all its pixels use the same normal. The surface looks faceted.
smooth shading
Normals are blended across triangles, so brightness changes gradually and the surface looks round.
inverted normal
A normal pointing into the shape instead of out of it. The surface is lit from the wrong side.

Step by step

01Start from one sphere

SphereGeometry(0.75, 16, 10) has 187 vertices shared by 288 triangles, with a normal on every vertex. This is the smooth ball.

02Split every triangle

toNonIndexed() copies the shape so every triangle has its own three vertices: 864 vertices. Calling computeVertexNormals() on that copy gives every vertex the normal of its own triangle. This is the flat ball. Without the call, the copied normals stay the smooth ones, and the ball looks smooth even with 864 vertices: it is the normals that decide, not the vertex count.

03Look at a crease

The second example builds a two-panel roof two ways: with six shared vertices and with twelve unshared ones. After computeVertexNormals() the ridge corner has one averaged normal in the shared case and two different normals, one per panel, in the other. The blue lines show them.

04Read the numbers

The shared ridge vertex gets 0.26, 0.97, 0.00: not straight up, because two of its three triangles lie on one panel and one on the other, and the average follows the larger share. The unshared ridges get -0.62, 0.78, 0.00 and 0.62, 0.78, 0.00: each panel's own slope.

Try it yourself

  1. 1

    Flat or smooth: what really decides

    In the first example delete the line flat.computeVertexNormals();. The second ball now looks as smooth as the first, although it still has 864 vertices: toNonIndexed() copied the smooth normals too. The normals decide, not the vertex count.

  2. 2

    List the roof the other way round

    In the roof example change the triangle list to [0, 4, 1, 0, 3, 4, 1, 5, 2, 1, 4, 5] (the same corners, reversed). The ridge normal now prints -0.26, -0.97, 0.00: every y is negative, so the normals point down into the roof, because computeVertexNormals follows the winding order.

Code & diagrams

Five balls: smooth, flat, none, inverted, zero New tab

From the left: smooth, flat, no normal attribute, inverted, all zero. The status bar shows 5 draw calls and 1,440 triangles (288 each); the console lists vertex and normal counts.

idle

Console

Run the scene to see its console.

Expected console output

smooth (shared corners)  vertices: 187 | normals: 187
flat (own corners)       vertices: 864 | normals: 864
no normal attribute      vertices: 187 | normals: none
inverted normals         vertices: 187 | normals: 187
all-zero normals         vertices: 187 | normals: 187
normal of vertex 0 (the north pole): 0.00 1.00 0.00
its length: 1.00
The normals helper on a two-panel roof New tab

Left roof: shared vertices, so one averaged normal at the ridge. Right roof: separate vertices, so each panel keeps its own. The blue lines are the helper. The roofs are drawn on both sides.

idle

Console

Run the scene to see its console.

Expected console output

indexed:      6 vertices, ridge vertex normal: 0.26, 0.97, 0.00
not indexed:  12 vertices, ridge normals: -0.62, 0.78, 0.00 and 0.62, 0.78, 0.00
draw calls: 4 (two meshes and two helpers)
lines: 18

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

Helper on a geometry with no normals

In the roof example delete the line g.computeVertexNormals(); // average the faces that share a vertex.

terminal
$ Run
── what you'll see ──
Uncaught TypeError: Cannot read properties of undefined (reading 'count')
The helper fails before anything is drawn, so the page is empty.

Myth vs fact

Myth

A black mesh means the light is missing.

Fact

It is one cause. A mesh lit by a good light is also black when its normals are all zero or NaN. Check Object.keys(geometry.attributes) and print one normal.

Pro corner

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

  • ▸

    computeVertexNormals weights each face by its area, because it adds the un-normalised face normals. Long thin triangles contribute little and big ones a lot, so a mesh with very uneven triangles can look slightly lopsided. Tools such as Blender offer angle-weighted normals for that reason.

  • ▸

    A geometry with a hard edge and a smooth surface (a bottle: smooth sides, sharp rim) needs vertices split only at the rim. Artists do this in the modelling tool, and the glTF file stores the normals. computeVertexNormals has no idea where you want an edge.

Remember this

  1. 1

    The problem this solves: a triangle's three corners say where it is, not which way it faces, and the GPU draws one pixel at a time. To light a pixel it needs an arrow at that pixel. So each vertex carries a normal, and the GPU blends the three normals of a triangle across its pixels. Light is then worked out per pixel from that blended arrow (Topic 5.2 does the maths).

  2. 2

    What a normal is. Three numbers (x, y, z) giving a direction, with length 1. On a sphere the normal at a point is the direction from the centre to that point. At the north pole of a ball it is (0, 1, 0). In three.js the normal attribute has item size 3, like position, and the first example prints this pole's normal and its length: 0.00 1.00 0.00 and 1.00.

  3. 3

    Smooth or flat is decided by sharing. If a vertex is shared by several triangles it has only one normal, the average of those triangles' faces, so lighting changes gently across the corner and the surface looks smooth. If every triangle has its own three vertices, each corner has the normal of its own triangle, and the surface looks faceted. That is why a cube needs 24 vertices: its edges must be sharp.

  4. 4

    **computeVertexNormals().** This works the normals out from the positions and the index: it adds up the face normals of every triangle that uses a vertex (bigger triangles count more) and makes the result length 1. Call it after you build or edit positions. On an indexed geometry it gives smooth normals, on a non-indexed one it gives flat normals. Built-in shapes already include normals, so you only need it for your own geometry or after you move vertices.

  5. 5

    Three failures. With no normal attribute, three.js shades the mesh flat by itself (the third ball: faceted, but lit). With all-zero normals nothing can be worked out and the mesh is black (the fifth ball). With inverted normals (every arrow pointing inwards) the surface is lit as if the light were on the far side: dark where it should be bright, with a bright rim (the fourth ball). The usual cause is triangles listed in the wrong winding order (Topic 4.3), because computeVertexNormals follows the winding.

Explain it without notes

01

Why does a cube have 24 vertices and not 8?

02

A model you loaded is lit as if the light came from the opposite side. What would you check?

Practice

01

Make a ball look faceted without copying the geometry, using a material option.

02

Print the normal of vertex 0 of a PlaneGeometry(1, 1) and say why.

Done when you can

  • Say what a normal is and why lighting needs it

  • Make a mesh flat or smooth and explain what decided it

  • Recognise a black mesh from zero normals and a wrongly lit one from inverted normals