Skip to content
Three.js Deep Dive: 3D Scenes, Lighting & Post-Processing

Three.js Deep Dive: 3D Scenes, Lighting & Post-Processing

DodaTech Updated Jun 20, 2026 7 min read

Three.js is a cross-browser JavaScript library that abstracts WebGL into a high-level 3D engine — scene graph, cameras, lights, materials, geometries, and post-processing effects are available with intuitive APIs.

What You’ll Learn

You’ll build a complete 3D scene with multiple light types, configure shadows, apply post-processing effects (bloom, depth of field), and load GLTF models — using Three.js at an intermediate level.

Why It Matters

Three.js powers product configurators, architectural walkthroughs, data visualization, and browser-based games. Doda Browser’s 3D tab preview feature uses Three.js for smooth scene transitions. Understanding scene graphs and lighting pipelines translates directly to game engines like Unity and Unreal.

Real-World Use

A car configurator on a dealership site lets you rotate a 3D model and change paint colors in real time — that’s Three.js with PBR materials and environment maps. An architectural firm’s interactive walkthrough uses Three.js with orbit controls, shadows, and post-processing.

Three.js Scene Architecture


flowchart TD
    A[Scene] --> B[Camera]
    A --> C[Mesh]
    A --> D[Light]
    A --> E[Renderer]
    C --> F[Geometry]
    C --> G[Material]
    B -->|Perspective| H[fov, aspect, near, far]
    B -->|Orthographic| I[left, right, top, bottom]
    D --> J[AmbientLight]
    D --> K[DirectionalLight]
    D --> L[PointLight]
    D --> M[SpotLight]
    E --> N[WebGLRenderer]
    E --> O[Post-Processing]

Step 1: Scene Setup with Multiple Light Types

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);

const camera = new THREE.PerspectiveCamera(
  60,                                 // field of view
  window.innerWidth / window.innerHeight, // aspect ratio
  0.1,                                // near clip
  1000                                // far clip
);
camera.position.set(5, 5, 10);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

// Lights
const ambient = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambient);

const directional = new THREE.DirectionalLight(0xffffff, 1.0);
directional.position.set(5, 10, 7);
directional.castShadow = true;
directional.shadow.mapSize.width = 2048;
directional.shadow.mapSize.height = 2048;
scene.add(directional);

const pointLight = new THREE.PointLight(0xff4400, 0.8, 20);
pointLight.position.set(-3, 2, 3);
scene.add(pointLight);

const spotLight = new THREE.SpotLight(0x00ff88, 0.5);
spotLight.position.set(0, 8, 0);
spotLight.angle = 0.3;
spotLight.penumbra = 0.2;
spotLight.decay = 1;
spotLight.distance = 30;
scene.add(spotLight);

const gridHelper = new THREE.GridHelper(20, 20, 0x444488, 0x222244);
scene.add(gridHelper);

Expected output: A 3D viewport with a grid floor, ambient blue-violet light, a bright directional light casting from above-right, an orange point light on the left, and a green spot light from above. You can orbit, pan, and zoom with mouse controls.

Step 2: Materials and Textures

// Standard material with roughness/metalness (PBR)
const floorMat = new THREE.MeshStandardMaterial({
  color: 0x334466,
  roughness: 0.7,
  metalness: 0.1,
});
const floor = new THREE.Mesh(
  new THREE.PlaneGeometry(10, 10),
  floorMat
);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.5;
floor.receiveShadow = true;
scene.add(floor);

// Sphere with metallic material
const sphereMat = new THREE.MeshStandardMaterial({
  color: 0xff6633,
  roughness: 0.2,
  metalness: 0.8,
  envMapIntensity: 1.0,
});
const sphere = new THREE.Mesh(
  new THREE.SphereGeometry(1, 32, 32),
  sphereMat
);
sphere.position.set(-2, 1, 0);
sphere.castShadow = true;
scene.add(sphere);

// Torus Knot (complex geometry)
const knotMat = new THREE.MeshStandardMaterial({
  color: 0x33ccff,
  roughness: 0.3,
  metalness: 0.6,
});
const knot = new THREE.Mesh(
  new THREE.TorusKnotGeometry(0.8, 0.3, 100, 16),
  knotMat
);
knot.position.set(2, 1, 0);
knot.castShadow = true;
scene.add(knot);

Expected output: A metallic orange sphere and a blue torus knot floating above a dark blue floor. Shadows from the directional light fall on the floor and between objects. The sphere shows specular highlights — it reflects the environment.

Step 3: Post-Processing — Bloom Effect

Add bloom (glow) using Three.js post-processing:

import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";

const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  0.5,   // strength
  0.4,   // radius
  0.85   // threshold
);
composer.addPass(bloomPass);

// Animate
function animate() {
  requestAnimationFrame(animate);

  sphere.rotation.y += 0.01;
  knot.rotation.x += 0.01;
  knot.rotation.y += 0.02;
  controls.update();

  composer.render(); // instead of renderer.render()
}
animate();

window.addEventListener("resize", () => {
  const w = window.innerWidth;
  const h = window.innerHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
  composer.setSize(w, h);
});

Expected output: The scene appears with a glow/bloom effect — bright areas bleed into neighboring pixels. The metallic sphere and torus knot develop a subtle halo. The bloom strength (0.5) creates a soft cinematic look.

Step 4: Loading GLTF Models

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loader = new GLTFLoader();
loader.load(
  "models/scene.gltf",
  (gltf) => {
    const model = gltf.scene;
    model.scale.set(2, 2, 2);
    model.position.set(0, 0, 0);
    model.traverse((child) => {
      if (child.isMesh) {
        child.castShadow = true;
        child.receiveShadow = true;
      }
    });
    scene.add(model);
  },
  (xhr) => {
    console.log(`Loading: ${(xhr.loaded / xhr.total) * 100}%`);
  },
  (error) => {
    console.error("GLTF load error:", error);
  }
);

Expected output: The loaded 3D model appears in the scene with proper materials and shadows. The console shows the loading progress percentage.

Common Errors

1. Model appears black or invisible The model may be outside the camera frustum. Check its position and scale. Models exported without materials or with unsupported material types (e.g., PBR specular/glossiness) may render as black. Use MeshStandardMaterial and ensure textures are embedded or referenced with correct paths.

2. Shadows don’t appear You need all three components: the light must have castShadow = true, the mesh must have castShadow = true, and the receiving mesh must have receiveShadow = true. Also ensure the shadow camera frustum encompasses the scene: directional.shadow.camera.left/right/top/bottom.

3. Bloom effect covers everything If the threshold is too low, everything glows — including dark areas. Increase threshold (0.85+) so only bright areas bloom. If nothing glows, decrease threshold or increase strength. The threshold controls the minimum luminance that triggers bloom.

4. Post-processing causes performance drop Bloom, DOF, and SSR are expensive. Reduce the render target resolution or use downsampling. The EffectComposer can render at half resolution: new EffectComposer(renderer, 0.5). Disable post-processing on mobile devices.

5. GLTF models with missing textures External textures must be referenced relative to the GLTF file, or embedded as base64 (increases file size). Use DRACOLoader for compressed geometry. Verify all texture paths resolve correctly in your build setup.

Practice Questions

1. What is the difference between PerspectiveCamera and OrthographicCamera? PerspectiveCamera simulates human vision — objects farther away appear smaller. OrthographicCamera ignores depth, showing objects at the same size regardless of distance. Use perspective for realistic 3D scenes, orthographic for 2D-like isometric or engineering views.

2. How does shadow mapping work in Three.js? Shadows are rendered from the light’s perspective into a depth texture (shadow map). During the main render pass, each pixel compares its depth against the shadow map to determine if it’s in shadow. PCFSoftShadowMap provides softer, more realistic shadow edges.

3. What does the roughness and metalness material properties control? Roughness (0-1) controls how rough a surface is — 0 is mirror-smooth, 1 is completely diffuse. Metalness (0-1) controls how much the material behaves like a metal — metallic surfaces reflect their environment and tint reflections with their color.

4. Why use EffectComposer instead of rendering directly? EffectComposer enables multi-pass rendering — you can chain multiple post-processing effects (bloom + DOF + color grading) that process the rendered image as a texture. The renderer can only output one final image.

5. Challenge: Add depth of field Add a BokehPass or UnrealBloomPass with focus distance. Make the front and back of the scene blurry while the center remains sharp. Animate the focus target to follow a moving object.

FAQ

What is the difference between Three.js and raw WebGL?
Three.js handles shader compilation, matrix math, geometry construction, and render loop boilerplate. Raw WebGL requires you to write GLSL shaders, manage buffers, and compute matrices manually. Three.js is 50× less code for the same result.
Can Three.js be used for mobile web?
Yes. Three.js has a mobile-compatible render path. Use lower polygon counts, compressed textures (KTX2/Basis), reduce shadow map resolution, and disable expensive post-processing on low-end devices.
How do I optimize a Three.js scene?
Use geometry instancing for repeated objects, merge static geometry with BufferGeometryUtils.mergeGeometries, use LOD (Level of Detail), limit shadow map resolution to 1024, and use texture atlases instead of individual textures.
What is the GLTF format?
GLTF (GL Transmission Format) is the standard format for 3D assets on the web — like JPEG for 3D. It stores geometry, materials, textures, animations, and scene hierarchy in a compact, loadable format. Always prefer GLTF over OBJ or Collada for web use.

What’s Next

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro