Skip to content
Three.js Lights & Shadows — Complete Guide to Lighting Strategies and Shadow Maps

Three.js Lights & Shadows — Complete Guide to Lighting Strategies and Shadow Maps

DodaTech Updated Jun 6, 2026 12 min read

Lighting transforms a flat Three.js scene into something that feels real. Without light, your objects have no depth, no mood, no presence — they’re just shapes. Three.js provides six light types and a shadow system that brings scenes to life.

What You’ll Learn

By the end of this tutorial, you’ll understand all six Three.js light types (Ambient, Directional, Point, Spot, Hemisphere, RectArea), configure shadow maps with proper bias settings, choose lighting strategies for different scenarios, and build an interactive Light Explorer studio.

Why Lights & Shadows Matter

Lighting is 80% of visual quality. Bad lighting makes even the best model look flat; good lighting makes a simple cube look cinematic. In Durga Antivirus Pro, the 3D threat dashboard uses a 3-point lighting setup with color-coded directional lights — red for high-severity zones, blue for safe areas — creating an intuitive visual hierarchy that lets analysts instantly spot threats.

    flowchart LR
    A[Light Types] --> B[Ambient: Base fill, no shadows]
    A --> C[Directional: Sun, parallel rays]
    A --> D[Point: Bulb, omni-directional]
    A --> E[Spot: Cone, stage light]
    A --> F[Hemisphere: Sky + ground gradient]
    A --> G[RectArea: Soft studio box]
    B --> H[Shadow Maps]
    C --> H
    D --> H
    E --> H
    H --> I[Lighting Strategies]
    I --> J[3-Point Studio]
    I --> K[Outdoor Daylight]
    I --> L[Indoor Warm]
    I --> M[Mood / Dramatic]
    J --> N[Light Explorer Project]
    style C fill:#44aa88,color:#fff,stroke:none
  
💡
Prerequisites: https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-basics/ and https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-geometries-materials/. You need to understand meshes and MeshStandardMaterial first — they’re the surfaces that light bounces off.

How Light Works in Three.js

Three.js uses physically-based rendering (PBR) for its standard materials. In the real world, light bounces off surfaces and enters your eyes. In Three.js, we simulate this by placing light sources and letting MeshStandardMaterial / MeshPhysicalMaterial calculate how each surface should look based on the light’s position, color, and intensity.

Key insight: MeshBasicMaterial ignores all lights. Only MeshStandardMaterial and MeshPhysicalMaterial respond to lighting. If an object looks black, check: do you have lights, and are you using a PBR material?

Light Types Explained

AmbientLight — The Base Fill

Fills the scene evenly from all directions. No shadows, no direction — like light on an overcast day.

var light = new THREE.AmbientLight(0xffffff, 0.5);  // White light at 50% intensity
scene.add(light);

When to use: Always. Even a dim ambient light (0.2-0.3 intensity) prevents shadows from being pure black. Every scene needs at least a low ambient light.

DirectionalLight — The Sun

Parallel rays from a distant source. All shadows point the same direction — just like sunlight.

var light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 7);  // Position matters: it defines the light direction
light.lookAt(0, 0, 0);

light.castShadow = true;
light.shadow.mapSize.width = 1024;   // Shadow texture resolution
light.shadow.mapSize.height = 1024;
light.shadow.camera.near = 0.5;      // Shadow camera frustum (tight = better)
light.shadow.camera.far = 50;
light.shadow.camera.left = -10;
light.shadow.camera.right = 10;
light.shadow.camera.top = 10;
light.shadow.camera.bottom = -10;
light.shadow.bias = -0.001;          // Reduces shadow acne

scene.add(light);

Why shadow camera matters: The directional light has its own camera that defines where shadows are rendered. If this frustum is too large, shadows lose detail. If too small, shadows get clipped. Visualize it with new THREE.CameraHelper(light.shadow.camera).

PointLight — The Light Bulb

Emits light in all directions from a single point. Think of a bare bulb hanging from the ceiling.

var light = new THREE.PointLight(0xff4400, 1, 20);  // Orange light, 20 unit range
light.position.set(2, 3, 4);
light.castShadow = true;
light.shadow.mapSize.width = 512;    // PointLight shadows = 6 cube faces → expensive!
light.shadow.mapSize.height = 512;
scene.add(light);

Performance warning: A PointLight with shadows renders the scene 6 times (one for each cube face). Use sparingly — 1-2 max.

SpotLight — The Stage Light

A cone of light. Think of a theatre spotlight, a flashlight, or a street lamp.

var light = new THREE.SpotLight(0xffffff, 1);
light.position.set(2, 5, 3);
light.target.position.set(0, 0, 0);
light.angle = Math.PI / 6;    // Cone angle (30 degrees)
light.penumbra = 0.3;         // Edge softness (0 = hard, 1 = very soft)
light.decay = 1;              // Light falloff rate
light.distance = 30;          // Maximum range

light.castShadow = true;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
light.shadow.bias = -0.001;

scene.add(light);
scene.add(light.target);  // ⚠️ Must add target to scene!

Common gotcha: You must add BOTH the SpotLight AND its light.target to the scene. Forgetting the target is a frequent bug.

HemisphereLight — Sky + Ground Gradient

Creates a gradient from a sky color to a ground color. Perfect for outdoor scenes.

var light = new THREE.HemisphereLight(
    0x87ceeb,    // Sky color (blue)
    0x362d25,    // Ground color (brown)
    0.6           // Intensity
);
scene.add(light);

Why use it? Outdoor scenes need blue light from above and warm/brown bounce light from below. Two DirectionalLights can approximate this, but HemisphereLight does it in one.

RectAreaLight — Studio Softbox

Emits from a rectangular surface. Like a photographer’s softbox.

import { RectAreaLightHelper } from "three/addons/helpers/RectAreaLightHelper.js";

var light = new THREE.RectAreaLight(0xffffff, 1, 4, 3);  // 4x3 unit rectangle
light.position.set(0, 5, 0);
light.lookAt(0, 0, 0);
scene.add(light);

Limitation: RectAreaLights do NOT cast shadows. Use PointLights or SpotLights if you need shadows.

Shadow Configuration

Enabling Shadows (3-Step Checklist)

Three things must all be true for shadows to appear:

// 1. Renderer must enable shadows
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;  // Best quality/performance balance

// 2. Light must cast shadows
light.castShadow = true;

// 3. Objects must opt in
mesh.castShadow = true;     // This object casts shadows on others
ground.receiveShadow = true; // This object receives shadows

// ShadowMap types (quality vs performance):
// BasicShadowMap — fastest, lowest quality (hard edges)
// PCFShadowMap — medium quality (percentage-closer filtering)
// PCFSoftShadowMap — best quality (softer edges)
// VSMShadowMap — variable softness (can have light bleeding artifacts)

Shadow Quality vs Performance

// Higher = better quality, slower
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;

// Lower = worse quality, faster
light.shadow.mapSize.width = 512;
light.shadow.mapSize.height = 512;

Shadow Bias — Fixing Artifacts

Shadow acne looks like dark banding on lit surfaces. Peter-panning looks like shadows detaching from objects.

light.shadow.bias = -0.001;        // Reduces shadow acne (shift shadow toward light)
light.shadow.normalBias = 0.02;    // Reduces peter-panning (shift along surface normal)

Start with bias = -0.001 and normalBias = 0.02, then adjust. Too much bias causes shadows to shift away from objects.

Lighting Strategies

Studio 3-Point Lighting (Product Shots)

The standard for making objects look professional:

// Key light (main) — strong, directional, creates primary shadows
var key = new THREE.DirectionalLight(0xffffff, 1.5);
key.position.set(5, 5, 5);
scene.add(key);

// Fill light — softer, opposite side, fills in harsh shadows
var fill = new THREE.DirectionalLight(0x4488ff, 0.3);
fill.position.set(-3, 2, 4);
scene.add(fill);

// Back light (rim) — behind subject, creates edge definition
var rim = new THREE.DirectionalLight(0xff8844, 0.5);
rim.position.set(-2, 3, -5);
scene.add(rim);

Outdoor Daylight

var ambient = new THREE.AmbientLight(0x404060, 0.3);
var sun = new THREE.DirectionalLight(0xffeedd, 1.5);
sun.position.set(20, 30, 10);
sun.castShadow = true;

Indoor Warm Lighting

var ambient = new THREE.AmbientLight(0xffeedd, 0.2);
var lamp = new THREE.PointLight(0xffaa44, 1.5, 15);
lamp.position.set(0, 3, 2);
lamp.castShadow = true;

Moody / Dramatic

// Single hard light from below (horror movie style)
var light = new THREE.SpotLight(0x4488ff, 2);
light.position.set(0, -2, 3);
light.angle = 0.3;
light.penumbra = 0.5;

Performance Tips

  • Use as few shadow-casting lights as possible — 1-2 max. Each shadow light renders an additional pass.
  • Keep shadow map sizes low — 512-1024 is usually enough. 2048+ for close-up hero shots only.
  • Use AmbientLight + HemisphereLight for fill — they don’t cast shadows (free).
  • Tighten shadow camera frustum — the smaller the frustum, the more shadow texels per scene unit.
  • PointLight shadows = 6 renders — a single PointLight shadow costs 6× a DirectionalLight shadow.
  • For static scenes, bake lighting into textures — pre-compute and skip runtime shadows entirely.

Common Mistakes

1. Mesh renders black (no lights)

MeshStandardMaterial and MeshPhysicalMaterial need lights. Add at least an AmbientLight or use MeshBasicMaterial for debugging.

2. Shadows not appearing — missing one of three requirements

Check all three: renderer.shadowMap.enabled = true, light.castShadow = true, and the casting mesh has castShadow = true while the receiving mesh has receiveShadow = true.

3. Shadow camera frustum too large

A huge frustum spreads the shadow map over a large area, making shadows pixelated. Tighten left/right/top/bottom to just cover the action.

4. Shadow acne (dark banding on surfaces)

Increase shadow.bias (more negative) or use shadow.normalBias. PCFSoftShadowMap reduces acne compared to BasicShadowMap.

5. Forgetting to add light.target for SpotLight

SpotLight needs both scene.add(light) AND scene.add(light.target). The target object must exist in the scene.

6. Too many shadow-casting lights

Each shadow light adds at least one render pass. Adding 4+ shadow lights will drop frame rate significantly. Use 1-2 max for shadows.

7. Not using light helpers for debugging

Helper objects are invaluable for visualizing light positions and shadow cameras. Comment them out only after you’ve verified the setup.

Practice Questions

Q1: What three conditions must be met for shadows to appear? A: (1) renderer.shadowMap.enabled = true, (2) light.castShadow = true, (3) the casting mesh must have castShadow = true and the receiving mesh must have receiveShadow = true.

Q2: Why does an object render black even with a light in the scene? A: The object likely uses MeshBasicMaterial which ignores lights, or there’s no light at all. Check that the material is MeshStandardMaterial or MeshPhysicalMaterial and that at least one light exists.

Q3: What is shadow acne and how do you fix it? A: Shadow acne is dark banding on lit surfaces caused by depth buffer precision limits. Fix it by adjusting shadow.bias (more negative) and shadow.normalBias.

Q4: Why are PointLight shadows more expensive than DirectionalLight shadows? A: PointLight renders shadows in all 6 directions (cube map), requiring 6 render passes. DirectionalLight only needs 1.

Q5: What’s the difference between shadow.bias and shadow.normalBias? A: bias shifts the shadow depth test along the light direction. normalBias shifts along the surface normal direction. NormalBias is more effective for reducing peter-panning (shadows detaching from objects).

Challenge: Set up a scene with a single object and three light types simultaneously: a blue DirectionalLight from the right, a red PointLight from above, and a green SpotLight from below. Toggle each one on/off to see how they combine. This technique is used in Durga Antivirus Pro’s threat dashboard to color-code different threat vectors.

FAQ

How many lights can I use?
Technically unlimited, but each shadow-casting light is expensive. Use 1-2 shadow lights max. Non-shadow lights (Ambient, Hemisphere, additional Directional without shadows) are cheap.
What is the best shadow map type?
THREE.PCFSoftShadowMap offers the best quality/performance balance. BasicShadowMap is fastest but has hard, jagged edges. VSMShadowMap can have light bleeding artifacts.
Why are my shadows pixelated?
Increase shadow.mapSize (1024, 2048, 4096) and tighten the shadow camera frustum to fit the scene exactly.
Can I have colored shadows?
Not natively — shadows are grayscale by default. For colored shadows, you need a custom ShaderMaterial or post-processing effect.
What is the difference between bias and normalBias?
Bias shifts the shadow along the light direction. NormalBias shifts along the surface normal. NormalBias is more effective at reducing peter-panning without causing shadow detachment.

Try It Yourself

Copy this complete HTML file, save as light-explorer.html, and open in a browser. Toggle between six light types in real-time.

<!DOCTYPE html>
<html>
<head>
    <title>Light Explorer — Three.js Lighting Studio</title>
    <style>
        body { margin: 0; overflow: hidden; }
        #ui { position: absolute; top: 10px; right: 10px;
            background: rgba(0,0,0,0.7); color: white; padding: 15px;
            border-radius: 8px; font-family: sans-serif; font-size: 13px;
            min-width: 180px; backdrop-filter: blur(4px); z-index: 10; }
        #ui label { display: block; margin: 8px 0 2px; }
        #ui input[type="range"] { width: 100%; }
        #ui select { width: 100%; padding: 4px; }
    </style>
</head>
<body>
    <div id="ui">
        <h3 style="margin:0 0 10px;">Light Controls</h3>
        <label>Light Type</label>
        <select id="lightType" onchange="changeLight()">
            <option value="ambient">Ambient</option>
            <option value="directional" selected>Directional</option>
            <option value="point">Point</option>
            <option value="spot">Spot</option>
            <option value="hemisphere">Hemisphere</option>
        </select>
        <label>Intensity: <span id="intensityVal">1.0</span></label>
        <input type="range" id="intensity" min="0" max="3" step="0.1" value="1" oninput="updateLight()">
        <label>Color</label>
        <input type="color" id="lightColor" value="#ffffff" oninput="updateLight()">
        <label>Shadows</label>
        <input type="checkbox" id="shadows" checked onchange="updateLight()">
    </div>

    <script type="importmap">
    {
        "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js"
        }
    }
    </script>
    <script type="module">
        import * as THREE from "three";
        var scene = new THREE.Scene();
        scene.background = new THREE.Color(0x222222);
        var camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 100);
        camera.position.set(5, 4, 8);
        var renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(innerWidth, innerHeight);
        renderer.setPixelRatio(devicePixelRatio);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        document.body.appendChild(renderer.domElement);

        var ground = new THREE.Mesh(
            new THREE.PlaneGeometry(10, 10),
            new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 })
        );
        ground.rotation.x = -Math.PI / 2;
        ground.position.y = -1;
        ground.receiveShadow = true;
        scene.add(ground);

        var sphere = new THREE.Mesh(
            new THREE.SphereGeometry(1, 32, 32),
            new THREE.MeshStandardMaterial({ color: 0x44aa88, roughness: 0.3, metalness: 0.2 })
        );
        sphere.position.set(-1.5, 0, 0);
        sphere.castShadow = true;
        scene.add(sphere);

        var cube = new THREE.Mesh(
            new THREE.BoxGeometry(1.2, 1.2, 1.2),
            new THREE.MeshStandardMaterial({ color: 0xcc6644, roughness: 0.5 })
        );
        cube.position.set(1.5, 0, 0);
        cube.castShadow = true;
        scene.add(cube);

        scene.add(new THREE.GridHelper(10, 10, 0x666666, 0x444444));

        var currentLight = null;
        var currentTarget = null;

        function createLight(type) {
            if (currentLight) { scene.remove(currentLight); if (currentTarget) scene.remove(currentTarget); }
            var color = document.getElementById("lightColor").value;
            var intensity = parseFloat(document.getElementById("intensity").value);
            var hasShadows = document.getElementById("shadows").checked;
            switch (type) {
                case "ambient":
                    currentLight = new THREE.AmbientLight(color, intensity);
                    currentTarget = null;
                    break;
                case "directional":
                    currentLight = new THREE.DirectionalLight(color, intensity);
                    currentLight.position.set(3, 5, 4);
                    if (hasShadows) { currentLight.castShadow = true; currentLight.shadow.mapSize.set(1024, 1024); }
                    currentTarget = null;
                    break;
                case "point":
                    currentLight = new THREE.PointLight(color, intensity, 20);
                    currentLight.position.set(2, 3, 2);
                    if (hasShadows) { currentLight.castShadow = true; currentLight.shadow.mapSize.set(512, 512); }
                    currentTarget = null;
                    break;
                case "spot":
                    currentLight = new THREE.SpotLight(color, intensity);
                    currentLight.position.set(3, 4, 3);
                    currentLight.angle = 0.4;
                    currentLight.penumbra = 0.3;
                    currentTarget = new THREE.Object3D();
                    currentTarget.position.set(0, 0, 0);
                    currentLight.target = currentTarget;
                    if (hasShadows) { currentLight.castShadow = true; currentLight.shadow.mapSize.set(1024, 1024); }
                    break;
                case "hemisphere":
                    currentLight = new THREE.HemisphereLight(color, 0x223344, intensity);
                    currentTarget = null;
                    break;
            }
            scene.add(currentLight);
            if (currentTarget) scene.add(currentTarget);
        }
        createLight("directional");

        window.changeLight = function() { createLight(document.getElementById("lightType").value); };
        window.updateLight = function() {
            createLight(document.getElementById("lightType").value);
            document.getElementById("intensityVal").textContent = document.getElementById("intensity").value;
        };

        function animate() {
            requestAnimationFrame(animate);
            sphere.rotation.y += 0.01;
            cube.rotation.x += 0.01; cube.rotation.y += 0.015;
            renderer.render(scene, camera);
        }
        animate();

        window.addEventListener("resize", function() {
            camera.aspect = innerWidth / innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(innerWidth, innerHeight);
        });
    </script>
</body>
</html>

What’s Next

TopicDescriptionLink
AnimationClocks, easing, TWEEN.js, and GSAPhttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-animation/
Models & LoadingImporting GLTF/GLB/OBJ modelshttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-models-loading/
Post-ProcessingBloom, film grain, glitch effectshttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-post-processing/
Geometries & MaterialsShapes, PBR materials, and textureshttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-geometries-materials/
Three.js BasicsScene, Camera, Renderer fundamentalshttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-basics/
WebGLThe underlying graphics APIWebGL
GSAPProfessional animation platformGSAP

You’ve mastered Three.js lighting — from the six light types to shadow configuration, bias tuning, and lighting strategies. The same 3-point lighting techniques used here power the threat severity visualization in Durga Antivirus Pro’s 3D dashboard.

What’s Next

Congratulations on completing this Threejs Lights Shadows tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro