Half-Lambert
Keeping dark areas from going fully black comes down to this one line:
Let's break it down.
The problem with standard Lambert
Standard Lambert uses max(dot(n, l), 0.0), which clamps the back-facing side to zero — pure black. In games and stylized rendering, that looks dead: back-lit surfaces lose all their detail.
What Half-Lambert does differently
It remaps dot(n, l) from [-1, 1] down to [0, 1]:
Result:
- Facing the light (dot = 1.0) → 1.0 × 0.5 + 0.5 = 1.0 (100%, brightest)
- Perpendicular (dot = 0.0) → 0.0 × 0.5 + 0.5 = 0.5 (50%, mid)
- Facing away (dot = -1.0) → -1.0 × 0.5 + 0.5 = 0.0 (0%, darkest)
Compared to standard Lambert, the shadow side retains some brightness and visible surface detail. The technique was popularized by Valve's art team on Half-Life, hence the name.
Try changing it
| Change | Effect |
|---|---|
* 0.5 + 0.5 to * 0.4 + 0.6 | Shadow side gets brighter, lower contrast |
* 0.5 + 0.5 to * 0.8 + 0.2 | Closer to standard Lambert, only a small base brightness in shadow |
Wrap with pow(diff, 2.0) | Increases contrast — only surfaces facing the light stay bright |
Exercise
The sphere in the exercise is fully black because diff = 0.0. Fix the TODO line to compute the Half-Lambert diffuse value.
Answer Breakdown
Starting state: diff is hardcoded to 0.0, making the sphere black.
The fix: drop the max clamp and use dot(n, l) * 0.5 + 0.5 instead — this maps the full [-1, 1] range into [0, 1], so even back-facing pixels receive some brightness.
Try changing the coefficients from * 0.5 + 0.5 to * 0.6 + 0.4 and observe how the shadow side responds.