Chapter 3: Physically Based Rendering and Lighting
Notes based on DISCOVER three.js, mostly excerpts with some personal insights.
Table of Contents
- Getting Started
- Understanding Lighting in Three.js
- Types of Direct Lighting
- Adding Lights in Code
- Materials: MeshStandardMaterial
- Switching Materials in Code
- Advanced Topics
Getting Started
Enabling Physically Correct Lighting
To enable physically correct lighting, simply turn on the renderer's .physicallyCorrectLights setting:
function createRenderer() {
const renderer = new WebGLRenderer();
// Turn on the physically correct lighting model
renderer.physicallyCorrectLights = true;
return renderer;
}Key Points:
- This setting is disabled by default for backward compatibility
- There are no downsides to enabling it
- Recommendation: Always enable this setting for realistic lighting
Units in Three.js: The Meter Convention
Three.js uses meters as the default unit of measurement.
Important: Using meters is a convention, not a rule. If you don't follow it, everything except physically accurate lighting will still work.
When to use different scales:
- Large-scale space simulations: 1 unit = 1000 km might be appropriate
- Micro-scale scenes: Different ratios may be needed
Critical Rule: If you want physically accurate lighting, you must build your scene to real-world scale.
Understanding Lighting in Three.js
The Two Types of Lighting
Direct Lighting
- Light rays that come directly from a light source and hit an object
- Easy to calculate
- Creates sharp, defined shadows
Indirect Lighting (Global Illumination)
- Light rays that bounce off walls and other objects before hitting the target
- Each bounce changes the light's color and reduces intensity
- Extremely computationally expensive to simulate accurately
How Three.js Handles These
| Real-World Lighting | Three.js Implementation |
|---|---|
| Direct Lighting | Direct Light Classes (DirectionalLight, PointLight, etc.) |
| Indirect Lighting | Ambient Light Classes (cheap approximation) |
Why Indirect Lighting is Challenging
The Problem: True indirect lighting requires calculating infinite light rays bouncing off all surfaces forever.
Solutions:
- Ray Tracing: Calculates thousands of rays with limited bounces, but still too slow for real-time
- Ambient Lighting: A "fake" but fast approximation used in Three.js
Types of Direct Lighting
Three.js provides four direct light types, each simulating a common real-world light source:
1. DirectionalLight (Sunlight)
import { DirectionalLight } from 'three';
const sunlight = new DirectionalLight(0xffffff, 1);
sunlight.position.set(100, 100, 100);Characteristics:
- Simulates distant light sources like the sun
- Light rays are parallel (all travel in the same direction)
- No distance falloff - objects at any distance receive the same intensity
- Objects are lit even if they're "behind" the light
Use Cases:
- Outdoor scenes
- Large environments
- When you need consistent lighting across a scene
2. PointLight (Light Bulb)
import { PointLight } from 'three';
const bulb = new PointLight(0xffffff, 1, 100);
bulb.position.set(0, 5, 0);Characteristics:
- Emits light in all directions from a single point
- Light intensity decreases with distance (inverse square law with
physicallyCorrectLights) - Creates realistic falloff effects
Parameters:
color: Light colorintensity: Light strengthdistance: Maximum range (0 = infinite)decay: How quickly light dims (default: 2 for physical accuracy)
Use Cases:
- Indoor scenes
- Lamps and light fixtures
- Candles and fire effects
3. SpotLight (Spotlight/Flashlight)
import { SpotLight } from 'three';
const spotlight = new SpotLight(0xffffff, 1);
spotlight.position.set(0, 10, 0);
spotlight.angle = Math.PI / 6;
spotlight.penumbra = 0.1;Characteristics:
- Emits light in a cone shape
- Has a direction it points toward
- Features soft edges (penumbra)
Parameters:
angle: Width of the cone (in radians)penumbra: Softness of the cone edges (0-1)target: Object or position the light points at
Use Cases:
- Stage lighting
- Flashlights
- Focused illumination effects
4. RectAreaLight (Area Lighting)
import { RectAreaLight } from 'three';
const areaLight = new RectAreaLight(0xffffff, 1, 10, 10);
areaLight.position.set(0, 5, 0);
areaLight.lookAt(0, 0, 0);Characteristics:
- Emits light from a rectangular plane
- Creates soft, realistic shadows
- More computationally expensive
Use Cases:
- Windows (bright daylight coming through)
- Strip/panel lighting
- Soft box photography lighting
- TV/monitor screens emitting light
Important Note: RectAreaLight only works with MeshStandardMaterial and MeshPhysicalMaterial.
Shadows in Three.js
Default Behavior: No Shadows
By default, objects in Three.js do not block light:
- Light passes through all objects
- Objects behind walls still receive illumination
- This is intentional for performance reasons
Enabling Shadows
Shadows must be enabled manually at three levels:
// 1. Enable shadows on the renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 2. Enable shadow casting on the light
light.castShadow = true;
// 3. Enable shadow casting/receiving on objects
mesh.castShadow = true;
mesh.receiveShadow = true;Shadow Performance Considerations
| Factor | Impact |
|---|---|
| Number of shadow-casting lights | High - limit to 1-2 lights |
| Shadow map resolution | High - use minimum needed |
| Scene complexity | Medium - fewer objects = faster |
| Mobile devices | Critical - may need to disable |
Best Practices:
- Only enable shadows on the most important light (usually the main directional light)
- Use shadow camera helpers during development to optimize shadow camera bounds
- Consider baking shadows for static objects
Adding Lights in Code
Creating a Light Component
Create components/lights.js:
import { DirectionalLight } from 'three';
function createLights() {
// Create a directional light
const light = new DirectionalLight('white', 8);
// Position the light
// Moving right (+x), up (+y), and toward camera (+z)
light.position.set(10, 10, 10);
return light;
}
export { createLights };Light Properties Explained
All Three.js lights inherit from the Light base class:
| Property | Description | Default |
|---|---|---|
color | Light color (Color, hex, or string) | 0xffffff (white) |
intensity | Light strength multiplier | 1 |
visible | Whether light is active | true |
Integrating Lights into the Scene
Update World.js:
import { createCamera } from './components/camera.js';
import { createCube } from './components/cube.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
let scene;
let camera;
let renderer;
class World {
constructor(container) {
camera = createCamera();
scene = createScene();
renderer = createRenderer();
container.append(renderer.domElement);
const cube = createCube();
const light = createLights();
// Add multiple objects to scene at once
scene.add(cube, light);
const resizer = new Resizer(container, camera, renderer);
}
render() {
renderer.render(scene, camera);
}
}
export { World };Materials: MeshStandardMaterial
Why MeshBasicMaterial Doesn't Show Lighting
MeshBasicMaterial:
- Ignores all lights in the scene
- Always appears flat with its assigned color
- Useful for debugging, UI elements, or stylized graphics
MeshStandardMaterial: The Go-To Choice
- Physically accurate - uses real-world physics equations
- Versatile - can recreate almost any common surface with textures
- The standard - should be your default choice for realistic rendering
Material Inheritance Hierarchy
Material (base class - cannot be used directly)
├── MeshBasicMaterial (no lighting)
├── MeshStandardMaterial (PBR - recommended)
├── MeshPhysicalMaterial (advanced PBR)
├── MeshLambertMaterial (simple diffuse)
├── MeshPhongMaterial (specular highlights)
└── ... other materialsCommon Properties (Inherited from Material Base Class)
| Property | Type | Description |
|---|---|---|
transparent | Boolean | Whether material supports transparency |
opacity | Number (0-1) | Transparency level |
visible | Boolean | Show/hide the material |
side | Constant | Which side(s) to render |
depthTest | Boolean | Whether to use depth buffer |
MeshStandardMaterial Specific Properties
| Property | Type | Description |
|---|---|---|
color | Color | Base color of the material |
roughness | Number (0-1) | Surface roughness (0=mirror, 1=diffuse) |
metalness | Number (0-1) | How metallic the surface appears |
map | Texture | Color/albedo texture |
normalMap | Texture | Surface detail texture |
roughnessMap | Texture | Per-pixel roughness |
metalnessMap | Texture | Per-pixel metalness |
aoMap | Texture | Ambient occlusion |
emissive | Color | Self-illumination color |
emissiveIntensity | Number | Strength of emissive glow |
Switching Materials in Code
Update cube.js to use the standard material:
import { BoxBufferGeometry, Mesh, MeshStandardMaterial } from 'three';
function createCube() {
// Create geometry (same as before)
const geometry = new BoxBufferGeometry(2, 2, 2);
// Switch from MeshBasicMaterial to MeshStandardMaterial
const material = new MeshStandardMaterial({ color: 'purple' });
// Create mesh
const cube = new Mesh(geometry, material);
// Apply rotation for visual interest
cube.rotation.set(-0.5, -0.1, 0.8);
return cube;
}
export { createCube };Material Configuration Examples
// Shiny metal surface
const metalMaterial = new MeshStandardMaterial({
color: 0x888888,
metalness: 1.0,
roughness: 0.2
});
// Rough plastic
const plasticMaterial = new MeshStandardMaterial({
color: 0xff0000,
metalness: 0.0,
roughness: 0.8
});
// Glowing material
const glowingMaterial = new MeshStandardMaterial({
color: 0x00ff00,
emissive: 0x00ff00,
emissiveIntensity: 0.5
});Advanced Topics
Ambient Lighting Options
Three.js provides several ambient light types:
AmbientLight - Uniform lighting from all directions
jsconst ambient = new AmbientLight(0x404040, 0.5);HemisphereLight - Sky/ground gradient
jsconst hemi = new HemisphereLight(0xffffbb, 0x080820, 1);Environment Maps - Image-based lighting (most realistic)
jsconst envMap = new CubeTextureLoader().load([...]); scene.environment = envMap;
Performance Optimization Tips
- Limit shadow-casting lights to 1-2 maximum
- Use
MeshLambertMaterialfor objects far from camera - Bake lighting for static scenes
- Use environment maps instead of multiple ambient lights
- Disable
physicallyCorrectLightsonly if performance is critical
Common Lighting Setups
Three-Point Lighting (Standard):
// Key light (main)
const keyLight = new DirectionalLight(0xffffff, 1);
keyLight.position.set(5, 5, 5);
// Fill light (softer, opposite side)
const fillLight = new DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-5, 0, 5);
// Back light (rim/separation)
const backLight = new DirectionalLight(0xffffff, 0.3);
backLight.position.set(0, 5, -5);Outdoor Scene:
// Sun
const sun = new DirectionalLight(0xffffcc, 1);
sun.position.set(100, 100, 50);
// Sky ambient
const sky = new HemisphereLight(0x87ceeb, 0x3d2817, 0.5);Summary
| Concept | Key Takeaway |
|---|---|
physicallyCorrectLights | Always enable for realistic lighting |
| Units | Use meters for physical accuracy |
| Direct vs Indirect | Direct = light classes; Indirect = ambient/environment |
| Shadows | Disabled by default; enable selectively for performance |
MeshStandardMaterial | Default choice for PBR rendering |
| Light Types | DirectionalLight (sun), PointLight (bulb), SpotLight (cone), RectAreaLight (panel) |