Course outline
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.
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
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
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, becausecomputeVertexNormalsfollows the winding order.
Code & diagrams
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.
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.00Left 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.
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: 18Break 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.
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.
- ▸
computeVertexNormalsweights 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.
computeVertexNormalshas no idea where you want an edge.
Remember this
- 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
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 thenormalattribute has item size 3, likeposition, and the first example prints this pole's normal and its length:0.00 1.00 0.00and1.00. - 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
**
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
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
computeVertexNormalsfollows the winding.
Explain it without notes
Why does a cube have 24 vertices and not 8?
A model you loaded is lit as if the light came from the opposite side. What would you check?
Practice
Make a ball look faceted without copying the geometry, using a material option.
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