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:

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:

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:

  1. Realtime approximations — cascaded shadow maps, PCF softening, screen-space techniques; cheap but limited.
  2. 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)
  3. 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:

  1. Use a high-quality HDRI for scene.environment and prefilter with PMREM. [Github](https://github.com/davidllona/Threejs-hdr-lighting-lab)
  2. 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)
  3. 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/)
  4. Tune renderer tone mapping and exposure to match the HDR brightness. [Github](https://github.com/davidllona/Threejs-hdr-lighting-lab)
  5. 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:


Common pitfalls and how to avoid them