Course outline
PBR Texture Maps
In one line
A material's numbers (colour, roughness, metalness) can each be replaced by a picture. Each map is a different kind of data: colour in sRGB, everything else in plain numbers. A normal map changes how light bounces without changing the shape.
Think of it like this
A map of a mountain. One map shows colours (forest, snow), another shows height lines, another which slopes are slippery. Each map describes the same ground, but answers a different question. A PBR material (Topic 5.3) asks each question at every point of the surface, and a texture answers it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- PBR map
- A texture that gives one material property (colour, roughness, metalness, normal, occlusion) at every point of a surface.
- normal map
- A picture whose colours store tiny surface tilts, so flat triangles light as if they were bumpy.
- ambient occlusion (AO)
- A pre-made darkening in creases and corners, where little ambient light can reach.
- displacement
- Really moving vertices up or down using a height picture. It changes the shape, unlike a normal map.
Step by step
01Start with the colour
Load the diffuse picture, set colorSpace = SRGBColorSpace, and assign it to map. This is the first plane of the example: a pattern, but flat.
02Add the normal map
Load nor_gl, leave its colour space alone and assign it to normalMap. The second plane's pattern now has raised diamonds that catch the low light from the left.
03Add roughness and AO
Add roughnessMap and aoMap (both data, both with their defaults). With roughness: 1 on the material, the map's green channel sets the shininess. The change is gentle under direct light.
04Flip a wrong normal map
The second example shows the same normal map with normalScale (1, 1) and (1, -1). With the sign flipped the bumps look like dents: the same look a DirectX map gives.
Try it yourself
- 1
Make the normal map stronger
In the second example set the first plane's scale to
new THREE.Vector2(3, 3). The diamonds look deeper and harsher:normalScalemultiplies the tilt. Values above about 2 make surfaces look rubbery and fake, so tune by eye. - 2
Light it straight on
In the first example change the light to
sun.position.set(0, 0, 5). The second plane looks almost the same as the first: with light coming straight at the surface, small tilts barely change the shading. The third and fourth planes become brighter and patchier (measured in a screenshot), which is the roughness map letting some areas shine. Low, sideways light is how you inspect a normal map; head-on light shows roughness.
Code & diagrams
Metal plate, Poly Haven, CC0. The light comes low from the left. The second plane is where the pattern gains faint relief. The roughness and AO steps are subtle with this light: try the head-on light from the first tryIt, and the environment of Topic 6.5.
Console
Run the scene to see its console.Expected console output
colour map only | maps: map
+ normalMap | maps: map, normalMap
+ roughnessMap | maps: map, normalMap, roughnessMap
+ aoMap | maps: map, normalMap, roughnessMap, aoMap
textures in GPU memory: 5Same picture, same light. Left: normalScale (1, 1). Right: (1, -1): the light and shadow sides of every diamond swap, so the bumps read as dents. Flip the sign for a map made for DirectX.
Console
Run the scene to see its console.Expected console output
normalScale (1, 1) | normalScale: 1, 1
normalScale (1, -1) | normalScale: 1, -1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A map the material does not read
In the first example change "new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0.2, ...maps })" to "new THREE.MeshBasicMaterial({ ...maps })".
Myth vs fact
Myth
A normal map makes the shape more detailed.
Fact
It changes only the lighting. Look at the silhouette of a plane with a deep normal map: it is still a flat rectangle. To move real vertices you need a displacement map and many triangles.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The bumpiness is computed per pixel in the fragment shader, so a normal map costs one texture read plus some maths for each pixel on screen, nothing per triangle. Without tangents in the geometry, three.js builds the tilt directions from UV derivatives in the shader.
- ▸
When a model is made for a different engine, check the normal map convention before blaming the lighting: OpenGL (green up) is used by glTF, Blender export and Poly Haven's
nor_gl; DirectX maps (nor_dx) neednormalScale.y = -1.
Remember this
- 1
The problem this solves: one
roughnessnumber makes the whole bike equally shiny, but a real part is smooth in one place and scratched in another. A picture per property gives each point its own value. The danger is mixing the kinds: a map of numbers must never be treated as colour (Topic 6.1). - 2
Which map does what.
map: the base colour (sRGB).normalMap: tiny slope changes that make flat triangles look bumpy.roughnessMap: the green channel is roughness at each point.metalnessMap: the blue channel.aoMap: the red channel, hand-baked shadow in crevices.displacementMap: really moves vertices (needs many triangles).alphaMap: transparency (green channel), withtransparentoralphaTest.emissiveMap: colour that glows (sRGB). - 3
The material number multiplies the map. The final roughness is
material.roughness × the map's green. A new Standard material hasroughness: 1, so the map decides. If you setroughness: 0.2the map is scaled down to 20%. For metalness, the default is 0, so the map is ignored until you also setmetalness: 1. This is the most common reason a map appears to do nothing. - 4
Normal maps store directions as colours. Red, green and blue hold the x, y and z of a tiny tilt: flat is (128, 128, 255), the pale purple-blue of every normal map. Lighting then behaves as if the surface were bumpy. The silhouette does not change, and a plane stays a plane. Tools make two flavours: OpenGL (green = up) and DirectX (green = down). three.js expects OpenGL. A DirectX map looks inside-out: bumps look like dents. The fix is
material.normalScale.y = -1. - 5
**Data maps use
NoColorSpace.** OnlymapandemissiveMapare colour. The loader's default is right for all the others; if you setSRGBColorSpaceon a roughness or normal map, three.js converts numbers that were never colours, and the surface gets the wrong shininess and slope. Pack maps to save texture reads: glTF puts occlusion, roughness and metalness into the red, green and blue of one picture ("ORM"), and the same texture can be assigned to all three slots. - 6
AO affects ambient light only. Ambient occlusion darkens light that arrives from everywhere (ambient, hemisphere, environment), not a direct light, so it is subtle with only a sun. The docs say it wants a second set of UVs: in r186 it reads the UV set named by
aoMap.channel(0 by default), so a map that shares the first UVs works, as here. A separately unwrapped AO map usestexture.channel = 1and the geometry'suv1attribute.
Explain it without notes
A bike part's metalness map does nothing. What do you check first?
Why is a normal map not enough to make a smooth tyre look like it has a real tread silhouette?
Practice
A downloaded normal map makes your bumps look like dents. Write the one line that fixes it, and name the cause.
Write the code to load a colour map and a normal map for one material with the right colour spaces.
Done when you can
Name the property each map controls and which channel it reads
Say why the material's number multiplies the map
Fix a normal map made for the wrong convention
Set the colour space correctly for each map