PBR & Lighting
Awaken lights every surface with a physically based Cook-Torrance model, up to eight lights, a flat ambient term, distance fog, and a choice of tonemapper - all evaluated in one forward fragment shader.
The lighting model
Surfaces are shaded with the Cook-Torrance microfacet BRDF, the same specular model used by Unity's URP, Unreal, and Godot. It combines three terms, all in MESH_WGSL (packages/render/src/shader.ts):
- D - Normal distribution (GGX / Trowbridge-Reitz):
distributionGGX(N, H, rough)- how many microfacets point toward the half-vector. Roughness controls the highlight's tightness. - G - Geometry (Smith with Schlick-GGX):
geomSmith(N, V, L, rough)- self-shadowing and masking of microfacets at grazing angles. - F - Fresnel (Schlick):
fresnel(cosT, F0)- reflectivity rising toward glancing angles.F0is0.04for dielectrics, tinted toward the albedo as metallic rises.
The specular contribution per light is spec = D * G * F / (4 · (N·V) · (N·L)), and the diffuse contribution is the albedo weighted by kd = (1 − F) · (1 − metallic) so metals lose their diffuse lobe. Each light accumulates Lo += (kd·albedo + spec) · radiance · (N·L).
Roughness is clamped to [0.04, 1.0] (a perfectly smooth surface is not representable and would alias), and metallic to [0, 1].
The energy convention (why direct light has no 1/π)
This is the detail that makes imported Unity/Godot scenes look right. The direct term deliberately omits the 1/π diffuse normalization: engine light energies (Godot light_energy, Unity intensity) are authored as surface brightness, where the artist has already accounted for the 1/π, so an intensity of 1 reads as a roughly fully-lit surface.
The ambient term does keep the 1/π:
wgsl
let amb = albedo * (1.0 - metallic) * lights.ambientIntensity * lights.ambientColor / PI * ao;Keeping 1/π on ambient but not on direct means the sun stays brighter than the fill light, so shadows read as shadows instead of washing out. Ambient is deliberately flat - driven by the neutral ambient colour, not tinted by the sky - because a coloured sky was greening every up-facing surface. Ambient is also the only term SSAO darkens (see Post-Processing).
Lights
A Light component (packages/render/src/light.ts) has four fields:
| Field | Type | Meaning |
|---|---|---|
kind | directional / point | Parallel rays vs. a positioned lamp |
color | RGB | Linear colour; multiplied by intensity into the radiance |
intensity | number | Brightness multiplier |
range | number | Point-light falloff distance (metres) |
gatherLights() walks every active, enabled Light in the world and packs up to 8 into the Lights uniform. Each is a Light { data: vec4, color: vec4 }:
- Directional -
data.xyzis the travel direction (the world matrix's normalized −Z column),data.w = 0. Rotate the light's GameObject to aim it. - Point -
data.xyzis the world position,data.w = 1. Attenuation isatt = clamp(1 − dist/range, 0, 1)²- a smooth quadratic falloff to zero atrange. color.rgbislight.color × intensity;color.wcarries the point-lightrange.
The Lights uniform header also carries scene-wide values: light count, ambientIntensity, shadowStrength, ambientColor, and fogDensity.
NOTE
Only the first 8 lights are shaded - there is no light culling or clustering per object. Keep the count of simultaneously-visible lights modest, and prefer one directional sun plus a few points.
The sun
The first directional light in the scene is the sun. It is the only light that casts shadows: its direction (cacheSunDir) drives the cascade fit, and its shadowStrength (from the render settings) controls how dark shadowed pixels get - 1 = fully dark, 0 = shadows off. Point lights and any additional directional lights contribute to shading but never to the shadow maps.
Ambient, fog, and the sky
- Ambient comes from the Ambient effect in the render settings stack. With no Ambient effect present,
ambientIntensityis0and surfaces are lit purely by direct light. - Fog is applied per-fragment after tonemapping:
fogF = 1 − exp(−distance · fogDensity), blending the shaded colour toward the sky colour in the same view direction. Fogging toward the sky (not a flat grey) is what lets distant geometry blend into the background instead of fading to grey. The Fog effect setsfogDensity(anexpmode uses density directly;linearmode derives a density from its end distance). - Sky is a fullscreen gradient (
skyColor()): a zenith→horizon blend above the horizon and a horizon→ground blend below, continuous atd.y = 0so there is no seam. The same function is shared by the background sky pass and the fog blend. Set it with the Sky effect.
Tonemapping
The shaded HDR-ish colour is tonemapped in the mesh fragment shader before gamma and fog, selected by camera.skyHorizon.w:
| Mode | Operator | When |
|---|---|---|
Linear (0) | Hard clamp(c, 0, 1) | Default / Godot imports - keeps stylized saturation |
ACES (1) | Narkowicz ACES filmic | Unity / Unreal imports - rolls HDR highlights off instead of clipping to flat white |
The mode is the tonemap field on the effect stack ("linear" or "aces") - a base mode, not an add/remove card. After tonemapping the colour is gamma-corrected (pow(c, 1/2.2)) and then fogged toward the gamma-space sky colour, so foreground and background share one colour space.
📸 Screenshot - save as img/render-pbr-metal-rough.png
A row of spheres varying metallic left→right and roughness top→bottom under one directional sun, showing the highlight tightening and the metal/dielectric split.
Where lighting values come from
- Per-object albedo, metallic, roughness are the
MeshRendererfields (color,metallic,roughness,texture) packed into the instance record - see Materials. - Lights are
Lightcomponents you add to GameObjects. - Ambient, fog, sky, tonemap live in the Render Settings effect stack.
See also
- Shadows - how the sun's cascaded shadow maps are built and sampled
- Post-Processing (Volume) - sky, ambient, fog, and the tonemap mode as effects
- Materials & Hook Shaders - where per-object albedo/metallic/roughness come from
- Camera - the uniform that carries sky colours and the tonemap flag
- Render Pipeline Overview - where lighting sits in the frame