```{index} single: shading single: lighting ``` # Shading: normals, light, and texture The [rasteriser](software-rasteriser) decided which pixels a triangle covers; **shading** decides their colour. It rests entirely on the [dot product](foundations): a surface is bright in proportion to how much it faces the light, and that is one dot product. The rest is refinement. ```{index} single: normal single: normal; per face single: normal; per vertex ``` ## Normals A **normal** is a unit vector pointing out of a surface. For a triangle it is the cross product of two edges: ```perl my $n = v_norm(v_cross(v_sub($v1, $v0), v_sub($v2, $v0))); # [0, 0, 1] for a triangle lying in the xy plane ``` Direction depends on winding, as with back-face culling: reversing the vertex order gives `[0, 0, -1]`. A mesh with inconsistent winding has some normals pointing inward, and those triangles render black. One normal per triangle lights a flat surface. For curved objects, store a normal per **vertex** instead, averaging the normals of the faces meeting there. The geometry stays faceted, the lighting does not, which is why a sphere of 200 triangles can look round. ```{index} single: Lambert term single: diffuse lighting single: ambient term ``` ## Lambert Brightness is the cosine of the angle between the normal and the direction to the light, which for unit vectors is their dot product. ```perl # Both must be unit vectors, and $light_dir points FROM the surface # TOWARD the light, not the direction the light travels. my $i = v_dot($normal, $light_dir); $i = 0 if $i < 0; ``` The clamp matters: a negative value means the light is behind the surface, and without it light shines through solid objects. ```text light direction intensity straight on [0,0,1] 1.000 30 deg off [0.5,0,0.87] 0.866 60 deg off [0.87,0,0.5] 0.500 edge on [1,0,0] 0.000 from behind [0,0,-1] 0.000 ``` This models a **matte** surface: chalk, paper, unfinished wood. It looks identical from every viewing angle, because the formula never mentions the viewer. Alone it leaves anything facing away from the light pure black, which no real scene is, because light bounces. The cheap fix is an **ambient** term: ```perl my $i = 0.1 + 0.9 * $lambert; # never fully black ``` Not physically justified: a stand-in for the bounced light the model does not simulate, which keeps shadowed areas readable. ```{index} single: specular highlight single: Blinn-Phong single: halfway vector single: shininess ``` ## Specular Shiny surfaces have a highlight that moves with the viewer, which Lambert cannot produce. **Blinn-Phong** adds one using the **halfway vector**, the direction midway between light and viewer: ```perl # $view_dir points FROM the surface TOWARD the viewer, matching # $light_dir's convention. Clamp the dot product before the power: # a negative base with a fractional exponent is a domain error. my $H = v_norm(v_add($light_dir, $view_dir)); my $nh = v_dot($normal, $H); $nh = 0 if $nh < 0; my $spec = $nh ** $shininess; ``` When the normal aligns with the halfway vector the surface bounces light straight at the viewer and the highlight peaks. The exponent controls how tight it is: ```text shininess specular 1 0.9659 8 0.7578 32 0.3298 128 0.0118 ``` Same geometry throughout, only the exponent changes. Low values give a broad sheen, high values a small bright spot: roughly 8 for polished wood, 32 for plastic, 128 and up for glass or wet metal. The three terms combined: ```perl my $colour = $ambient + $diffuse * $lambert + $specular * $spec; ``` ```{index} single: flat shading single: Gouraud shading single: Phong shading single: per-pixel shading ``` ## How often you evaluate it The formulas are the same wherever they run. How often they run changes the result enormously. **Flat shading**: one intensity per triangle, applied to every pixel. Cheap, and the faceting shows: ```text +++++++ +++++++++++++++++++ ++++++++++++++++++++++++ +++++++++++++++++++++ ++++++++++++++++++ ++++++++++++++ +++++++++++ ++++++++ +++++ + ``` **Gouraud shading**: an intensity per vertex, interpolated with the same barycentric weights the rasteriser already computes for depth: ```perl my $i = $b0 * $inten[0] + $b1 * $inten[1] + $b2 * $inten[2]; ``` ```text @%%%%## %%%####****++++===- %%###****++++====---:::: %####***++++====---:: ###***++++====---- #****+++====-- ***++++===- **++++== ++++= + ``` Three lighting calculations rather than one per pixel, and the flat region becomes a gradient. The approximation is interpolating the **result**, which shows on specular highlights: a highlight landing mid triangle with all three vertices dim vanishes, because there is nothing at the vertices to interpolate. **Per-pixel shading**, confusingly called Phong shading, interpolates the **normal** instead and runs the lighting at every pixel. More expensive, and correct. This is what a fragment shader does. The rule that carries forward: interpolate the inputs, not the answer. ```{index} single: texture mapping single: UV coordinate single: unwrapping single: normal map ``` ## Texture mapping Same interpolation again. Each vertex carries a **UV coordinate**, a position in an image, both axes 0 to 1 regardless of pixel size. Interpolate UVs across the triangle and look up the image at the result: ```perl # $b0..$b2 must be PERSPECTIVE-CORRECT barycentric weights. Screen-linear # weights slide the texture across the surface; see the rasteriser chapter. my $u = $b0 * $uv[0][0] + $b1 * $uv[1][0] + $b2 * $uv[2][0]; my $v = $b0 * $uv[0][1] + $b1 * $uv[1][1] + $b2 * $uv[2][1]; my $texel = sample($image, $u, $v); ``` A checker pattern sampled that way, over the same triangle: ```text ####... ......####...###... ...###...####......###.. ####...###......####. ...####......###.. ###......####. ......###.. ...####. ###.. . ``` Assigning UVs is **unwrapping**, a modelling task rather than a rendering one: deciding how the surface is cut and flattened, like the net of a paper model. Two practical details. UVs are the value that slides visibly without **perspective-correct** interpolation. And a UV outside 0 to 1 needs a rule, repeat or clamp, which is a per-texture setting in every graphics API. The same lookup carries more than colour: per-pixel normals (a **normal map**, bumps without geometry), shininess, or how much light a point blocks. Only the meaning of the number changes. ```{index} single: physically based rendering single: PBR ``` ## Physically based rendering The model above uses numbers chosen by eye. **PBR** replaces them with quantities that conserve energy, so a surface never reflects more light than it receives, and describes materials by measurable properties: roughness, and whether the surface is metal. The practical consequence: PBR materials hold up under any lighting, where Blinn-Phong parameters are tuned per scene and break in the next one. The pipeline shape is unchanged, with a dot product still at the centre. Blinn-Phong is still worth implementing first; every PBR treatment assumes it. ## Next You can light a surface and texture it, but have nothing to shade yet. The next arc covers [meshes](meshes), the file formats carrying them, the operations that construct new shapes, and the scene graph arranging them in a world.