Lighting and PBR explained: achieving photorealism on the web
Why PBR matters
Physically Based Rendering (PBR) replaces ad-hoc artistic knobs with measurable surface properties so materials behave consistently under any lighting. Using PBR reduces the endless trial-and-error of matching day and night lighting because materials follow energy-conserving rules. [Discover three.js](https://discoverthreejs.com/book/first-steps/physically-based-rendering/)
PBR fundamentals: roughness and metalness
The most common PBR workflow on the web is the metalness-roughness model. Two parameters control appearance:
Roughness (0–1): controls microsurface smoothness; low values produce sharp, mirror-like reflections; high values scatter reflections and look matte. [ramijames.com](https://www.ramijames.com/learn-threejs/building-blocks/physically-based-rendering)
Metalness (0–1): toggles dielectric vs metal behavior; metals reflect tinted specular light and have little to no diffuse component. [ramijames.com](https://www.ramijames.com/learn-threejs/building-blocks/physically-based-rendering)
Energy conservation: PBR enforces that surfaces cannot reflect more light than they receive, which makes materials look correct across different lighting setups. [ramijames.com](https://www.ramijames.com/learn-threejs/building-blocks/physically-based-rendering)
Three.js materials to use
For most web projects use MeshStandardMaterial or MeshPhysicalMaterial. The latter adds advanced features like clearcoat and anisotropy for car paint and layered surfaces. These materials implement the metalness-roughness workflow out of the box. [ramijames.com](https://www.ramijames.com/learn-threejs/building-blocks/physically-based-rendering)
// minimal material example
const mat = new THREE.MeshStandardMaterial({
color: 0xaaaaaa,
metalness: 0.2,
roughness: 0.4
});
mesh.material = mat;
Lighting types and when to use them
Combine a small set of complementary lights rather than many overlapping sources:
DirectionalLight — sun/strong distant source; good for crisp shadows and outdoor scenes. [Discover three.js](https://discoverthreejs.com/book/first-steps/physically-based-rendering/)
PointLight — local omni light for bulbs and lamps.
SpotLight — focused beams with soft edges for stage or accent lighting.
HemisphereLight — cheap ambient sky/ground tint for outdoor feel.
But the single most impactful source for PBR is an HDR environment used as an image-based lighting (IBL) source. Environment maps provide realistic reflections and indirect lighting cues that make materials read as real. [Github](https://github.com/davidllona/Threejs-hdr-lighting-lab)
Environment maps, PMREM, and reflections
HDR panoramas contain high dynamic range lighting information that PBR materials use for reflections and indirect illumination. In Three.js you should prefilter the HDR with PMREMGenerator so reflections blur correctly across roughness levels. Without PMREM, reflections look too sharp or noisy. [Github](https://github.com/davidllona/Threejs-hdr-lighting-lab)
// load HDR and apply PMREM
const loader = new THREE.RGBELoader();
const pmrem = new THREE.PMREMGenerator(renderer);
pmrem.compileEquirectangularShader();
loader.load('studio.hdr', (tex) => {
const envMap = pmrem.fromEquirectangular(tex).texture;
scene.environment = envMap;
scene.background = envMap; // optional: use a separate background if you prefer
tex.dispose();
pmrem.dispose();
});
Tone mapping and exposure
HDR lighting often requires tone mapping and exposure control to map scene luminance into the displayable range. Three.js exposes renderer tone mapping and exposure settings; tweak renderer.toneMapping and renderer.toneMappingExposure to match your HDR brightness. [Github](https://github.com/davidllona/Threejs-hdr-lighting-lab)
// example tone mapping
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
Shadows, global illumination, and baking
Real-world realism depends on indirect light bounces and soft, contact shadows. On the web you have three practical options:
Baked lighting / lightmaps — precompute indirect lighting into textures for static geometry; highest visual quality for static scenes but limits dynamic object movement. [three.js forum](https://discourse.threejs.org/t/how-to-get-photorrealism-light-shadows-in-three-js/28874)
Path tracing / progressive GI — real-time path tracers exist for Three.js and can produce true global illumination, reflections, and caustics, but they are heavier and often used for high-end demos or offline renders. [erichlof.github.io](https://erichlof.github.io/THREE.js-PathTracing-Renderer/)
Rule of thumb: bake what can be static; reserve realtime GI for dynamic elements or interactive demos where budget allows. [three.js forum](https://discourse.threejs.org/t/how-to-get-photorrealism-light-shadows-in-three-js/28874) [erichlof.github.io](https://erichlof.github.io/THREE.js-PathTracing-Renderer/)
Practical recipe: a small, photoreal Three.js scene
Follow these steps to get a convincing baseline quickly:
Use a high-quality HDRI for scene.environment and prefilter with PMREM. [Github](https://github.com/davidllona/Threejs-hdr-lighting-lab)
Choose MeshStandardMaterial or MeshPhysicalMaterial and author maps: baseColor, normal, roughness, metalness, and optionally ao. [ramijames.com](https://www.ramijames.com/learn-threejs/building-blocks/physically-based-rendering)
Enable a single directional light for strong shadows and a hemisphere or fill light for ambient balance. [Discover three.js](https://discoverthreejs.com/book/first-steps/physically-based-rendering/)
Tune renderer tone mapping and exposure to match the HDR brightness. [Github](https://github.com/davidllona/Threejs-hdr-lighting-lab)
Bake static indirect lighting where possible; use realtime GI or path tracing for special demos. [three.js forum](https://discourse.threejs.org/t/how-to-get-photorrealism-light-shadows-in-three-js/28874) [erichlof.github.io](https://erichlof.github.io/THREE.js-PathTracing-Renderer/)
// minimal scene setup
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.physicallyCorrectLights = true;
renderer.outputEncoding = THREE.sRGBEncoding;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth/innerHeight, 0.1, 1000);
// add directional key light
const sun = new THREE.DirectionalLight(0xffffff, 3.0);
sun.position.set(10, 10, 10);
sun.castShadow = true;
scene.add(sun);
// load HDR and PMREM (see previous snippet)
Performance and mobile considerations
Photorealism competes with performance. Key optimizations:
Texture budgets: compress textures (KTX2/ETC2/ASTC) and use mipmaps.
Reduce draw calls: combine meshes, use instancing for repeated objects.
LOD and roughness tricks: swap high-detail materials for cheaper ones at distance; blur environment maps for low-end devices.
Fallbacks: provide a simplified non-PBR material or a static image for very low-end devices or when WebGL is unavailable.
Common pitfalls and how to avoid them
Too-bright HDRs: fix with tone mapping and exposure rather than lowering material albedo.
Confusing metalness: metals should not have a colored diffuse map; their color comes from specular reflections. [ramijames.com](https://www.ramijames.com/learn-threejs/building-blocks/physically-based-rendering)
Sharp reflections on rough surfaces: ensure PMREM is used so roughness correctly blurs reflections. [Github](https://github.com/davidllona/Threejs-hdr-lighting-lab)
Expecting ray-traced quality from rasterization: use baking or path tracing when you need true GI and caustics. [erichlof.github.io](https://erichlof.github.io/THREE.js-PathTracing-Renderer/) [three.js forum](https://discourse.threejs.org/t/how-to-get-photorrealism-light-shadows-in-three-js/28874)