Building Your First Three.js Application
This guide is based on DISCOVER three.js with enhanced explanations and practical insights for building production-ready applications.
Table of Contents
- Project Structure
- HTML Setup
- CSS Styling
- The Core: main.js
- Understanding the Scene
- Camera Configuration
- The Renderer
- Creating 3D Objects: Meshes
- Anti-aliasing for Quality
- Modular Architecture
- Best Practices
Project Structure
Basic Structure (Single File Approach)
my-threejs-app/
├── index.html
├── styles/
│ └── main.css
├── src/
│ └── main.js
└── vendor/
└── three.module.js (downloaded three.js file)Initial Setup: Perfect for learning and prototyping, but not ideal for larger projects.
HTML Setup
Basic HTML Template
<!DOCTYPE html>
<html>
<head>
<title>Discoverthreejs.com - Your First Scene</title>
<!-- Viewport meta tag for responsive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8" />
<link rel="icon" href="https://discoverthreejs.com/favicon.ico" type="image/x-icon">
<!-- Link to CSS styles -->
<link href="./styles/main.css" rel="stylesheet" type="text/css">
<!--
IMPORTANT: Use type="module" to enable ES6 imports
This allows us to use modern JavaScript features
-->
<script type="module" src="./src/main.js"></script>
</head>
<body>
<h1>Discoverthreejs.com - Your First Scene</h1>
<div id="scene-container">
<!-- The three.js <canvas> element will be inserted here dynamically -->
</div>
</body>
</html>Key Points:
- ✅ Use
type="module"in script tag for ES6 imports - ✅ Create a container div for the canvas
- ✅ Include viewport meta for mobile responsiveness
- ✅ Load JavaScript as a module (enables tree-shaking)
CSS Styling
Full-Screen Canvas Setup
body {
/* Remove default margins and prevent scrolling */
margin: 0;
overflow: hidden;
/* Text styling */
text-align: center;
font-size: 12px;
font-family: Sans-Serif;
/* Text color */
color: #444;
}
h1 {
/* Position heading over the canvas */
position: absolute;
width: 100%;
/* Ensure heading is drawn on top of canvas */
z-index: 1;
}
#scene-container {
/* Make container fill the entire viewport */
position: absolute;
width: 100%;
height: 100%;
/*
IMPORTANT: Set background color to match scene background
This prevents a white flash during initial load
*/
background-color: skyblue;
}Pro Tips:
- 🎨 Match
background-colorto your scene's background for seamless loading - 📱
overflow: hiddenprevents unwanted scrollbars - 🔝 Use
z-indexto layer HTML elements over the canvas - ⚡
position: absoluteallows full-screen rendering without layout issues
The Core: main.js
Complete Implementation with Explanations
import {
BoxBufferGeometry,
Color,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
Scene,
WebGLRenderer,
} from 'three';
// ============================================
// STEP 1: Get Container Reference
// ============================================
// Get reference to the HTML element that will hold our 3D scene
const container = document.querySelector('#scene-container');
// ============================================
// STEP 2: Create the Scene
// ============================================
// The Scene is the container for all 3D objects
const scene = new Scene();
// Set background color (can also use textures or skyboxes)
scene.background = new Color('skyblue');
// ============================================
// STEP 3: Create the Camera
// ============================================
// Camera parameters explained:
const fov = 35; // Field of View in degrees (35° is a good default)
const aspect = container.clientWidth / container.clientHeight; // Width/Height ratio
const near = 0.1; // Near clipping plane (objects closer are invisible)
const far = 100; // Far clipping plane (objects farther are invisible)
const camera = new PerspectiveCamera(fov, aspect, near, far);
// Position camera (all objects start at origin: 0, 0, 0)
// Move camera back on Z-axis so we can see objects at origin
camera.position.set(0, 0, 10);
// ============================================
// STEP 4: Create 3D Geometry
// ============================================
// BoxBufferGeometry is more efficient than BoxGeometry (deprecated)
// Parameters: width, height, depth
const geometry = new BoxBufferGeometry(2, 2, 2);
// ============================================
// STEP 5: Create Material
// ============================================
// MeshBasicMaterial doesn't require lights (good for starting)
// Default color is white (0xffffff)
const material = new MeshBasicMaterial();
// ============================================
// STEP 6: Create Mesh (Geometry + Material)
// ============================================
// Mesh combines geometry and material into a visible object
const cube = new Mesh(geometry, material);
// Add the mesh to the scene
scene.add(cube);
// ============================================
// STEP 7: Create the Renderer
// ============================================
// WebGLRenderer uses WebGL to draw the scene
const renderer = new WebGLRenderer();
// Set renderer size to match container
renderer.setSize(container.clientWidth, container.clientHeight);
// Set pixel ratio for crisp rendering on high-DPI displays (Retina, etc.)
renderer.setPixelRatio(window.devicePixelRatio);
// ============================================
// STEP 8: Add Canvas to DOM
// ============================================
// The renderer automatically creates a <canvas> element
// We need to add it to our container
container.append(renderer.domElement);
// ============================================
// STEP 9: Render the Scene
// ============================================
// This creates a single static image (not animated yet)
renderer.render(scene, camera);The Rendering Pipeline Visualization
┌─────────────┐
│ Scene │ ← Contains all 3D objects
└──────┬──────┘
│
├─→ Cube (Mesh)
├─→ Lights
└─→ Other objects
┌─────────────┐
│ Camera │ ← Viewpoint into the 3D world
└──────┬──────┘
│ (defines what we see)
┌─────────────┐
│ Renderer │ ← Draws the scene from camera's perspective
└──────┬──────┘
│
▼
<canvas> element on webpageImportant Notes:
- ⚠️ Without calling
renderer.render(), nothing appears on screen - 🎯 This creates a single frame (static image)
- 🔄 For animation, you need to call
render()repeatedly (we'll cover this later)
Understanding the Scene
What is a Scene?
The Scene is the container for everything visible in your 3D world.
Think of it as a "mini universe" where all 3D objects exist. It uses the Cartesian coordinate system we learned earlier:
Y (up)
│
│
│
└────── X (right)
╱
╱
Z (toward viewer)The Scene Graph
When you add objects to the scene, they're organized in a scene graph - a tree structure with the scene at the top:
Scene (root)
├── Mesh (cube)
├── Light
├── Camera (optional in scene)
└── Group
├── Mesh (child object)
└── Mesh (another child)Benefits of Scene Graph:
- 🔗 Parent-child relationships
- 🎯 Hierarchical transformations
- 🗂️ Organized object management
- ⚡ Efficient rendering and culling
Scene Properties and Methods
const scene = new Scene();
// Background options
scene.background = new Color(0x000000); // Solid color
scene.background = new Color('skyblue'); // CSS color name
scene.background = textureLoader.load('sky.jpg'); // Image texture
// Fog (atmospheric depth)
scene.fog = new Fog(0xcccccc, 10, 50); // color, near, far
// Add/remove objects
scene.add(mesh1, mesh2, light);
scene.remove(mesh1);
// Find objects
const object = scene.getObjectByName('myObject');
scene.traverse((child) => {
// Visit every object in scene graph
console.log(child);
});Reference: Scene Graph Concept
Camera Configuration
Camera Types in Three.js
Three.js provides two main camera types:
1. PerspectiveCamera (Most Common) 👁️
Mimics how human eyes see the world - objects appear smaller as they get farther away.
const camera = new PerspectiveCamera(fov, aspect, near, far);Use cases:
- ✅ Games and interactive 3D experiences
- ✅ Architectural visualizations
- ✅ Any realistic 3D scene
- ✅ VR/AR applications
2. OrthographicCamera 📐
No perspective distortion - parallel lines stay parallel regardless of distance.
const camera = new OrthographicCamera(left, right, top, bottom, near, far);Use cases:
- ✅ 2D games with 3D graphics
- ✅ Technical/engineering diagrams
- ✅ UI overlays
- ✅ Isometric games
Visual Comparison:
Perspective Camera: Orthographic Camera:
╱ ╲ │ │
╱ ╲ │ │
╱ ╲ │ │
╱ ╲ │ │
Far objects All same size
appear smaller at any distanceUnderstanding PerspectiveCamera Parameters
const fov = 35; // Field of View (degrees)
const aspect = container.clientWidth / container.clientHeight; // Aspect ratio
const near = 0.1; // Near clipping plane
const far = 100; // Far clipping plane
const camera = new PerspectiveCamera(fov, aspect, near, far);Parameter Deep Dive
1. Field of View (FOV) 🎯
- Range: Typically 40-80 degrees
- 35-50°: Natural, human-like view (recommended for most scenes)
- 50-70°: Wider view, slight distortion
- 70-90°: Very wide, noticeable distortion (action games)
- < 35°: Telephoto effect, compressed depth
// Examples
const camera1 = new PerspectiveCamera(35, aspect, near, far); // Natural
const camera2 = new PerspectiveCamera(75, aspect, near, far); // Wide angle
const camera3 = new PerspectiveCamera(20, aspect, near, far); // Telephoto2. Aspect Ratio 📺
- Formula:
width / height - Purpose: Prevents stretching/squashing
- Must update on window resize
// Common aspect ratios
const aspectRatio = window.innerWidth / window.innerHeight; // Full window
const aspectRatio = 16 / 9; // Widescreen
const aspectRatio = 4 / 3; // Standard
const aspectRatio = 1; // Square3. Near Clipping Plane ✂️
- Range: > 0, typically 0.1 - 1
- Too small: Z-fighting issues (visual artifacts)
- Too large: Objects close to camera disappear
4. Far Clipping Plane ✂️
- Range: > near, typically 100 - 10000
- Too large: Performance issues, precision loss
- Too small: Distant objects disappear
The Viewing Frustum
These four parameters create a bounded region called the viewing frustum:
far plane
┌─────────┐
╱ ╱│
╱ ╱ │
╱ ╱ │
╱ ╱ │
└─────────┘ │
near plane │
│
Camera ●─────┘Key Concepts:
- 🔍 Only objects inside the frustum are rendered
- ✂️ Objects partially outside are clipped
- ⚡ This is called frustum culling - a major performance optimization
Camera Position and Orientation
// Set camera position (x, y, z)
camera.position.set(0, 0, 10); // 10 units back on Z-axis
// Alternative ways to set position
camera.position.x = 5;
camera.position.y = 2;
camera.position.z = 10;
// Look at a specific point
camera.lookAt(0, 0, 0); // Look at origin
camera.lookAt(cube.position); // Look at object
// Camera always looks down negative Z-axis by defaultUpdating Camera Settings
CRITICAL: After changing FOV, aspect, near, or far, you must update the frustum:
camera.fov = 50;
camera.aspect = window.innerWidth / window.innerHeight;
camera.near = 0.5;
camera.far = 200;
// ⚠️ REQUIRED: Update the frustum
camera.updateProjectionMatrix();The Renderer
Understanding the Renderer's Role
Metaphor: If the scene is a universe and the camera is a telescope, the renderer is an artist who looks through the telescope and paints what they see - incredibly fast!
The WebGLRenderer uses WebGL (Web Graphics Library) to draw 3D graphics using the GPU.
Basic Renderer Setup
// Create renderer
const renderer = new WebGLRenderer();
// Set size to match container
renderer.setSize(container.clientWidth, container.clientHeight);
// Set pixel ratio for high-DPI displays
renderer.setPixelRatio(window.devicePixelRatio);
// Add canvas to DOM
container.append(renderer.domElement);
// Render a frame
renderer.render(scene, camera);Renderer Configuration Options
const renderer = new WebGLRenderer({
// Anti-aliasing for smooth edges (slight performance cost)
antialias: true,
// Enable alpha channel (transparent background)
alpha: true,
// Preserve drawing buffer (for screenshots)
preserveDrawingBuffer: true,
// Power preference
powerPreference: 'high-performance', // or 'low-power', 'default'
// Use existing canvas element
canvas: document.querySelector('#my-canvas'),
});Setting Pixel Ratio (High-DPI Support)
Why it matters: Without proper pixel ratio, your scene will look blurry on Retina displays and high-DPI monitors.
// ✅ ALWAYS set pixel ratio
renderer.setPixelRatio(window.devicePixelRatio);
// Limit pixel ratio for performance (optional)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));Pixel Ratio Explained:
- 📱 Regular displays:
devicePixelRatio = 1 - 🖥️ Retina/High-DPI:
devicePixelRatio = 2or higher - ⚡ Higher ratio = sharper but slower
Renderer Properties and Methods
// Shadows
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Tone mapping (for realistic lighting)
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
// Output encoding (for correct colors)
renderer.outputEncoding = THREE.sRGBEncoding;
// Physically correct lights (more realistic)
renderer.physicallyCorrectLights = true;
// Background color/alpha
renderer.setClearColor(0x000000, 1); // Black, opaque
renderer.setClearColor(0xffffff, 0); // White, transparent
// Get canvas element
const canvas = renderer.domElement;Render Loop (Animation)
// Single frame (static)
renderer.render(scene, camera);
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Update animations here
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
// Render frame
renderer.render(scene, camera);
}
animate();Performance Optimization
// Clear only what's needed
renderer.autoClear = false;
renderer.clear();
renderer.render(scene, camera);
// Render on-demand (no continuous loop)
let needsRender = true;
function render() {
if (needsRender) {
renderer.render(scene, camera);
needsRender = false;
}
}
// Trigger render when needed
controls.addEventListener('change', () => {
needsRender = true;
render();
});Creating 3D Objects: Meshes
What is a Mesh?
A Mesh is the most common visible object in 3D graphics. It represents solid objects like cats, dogs, buildings, trees, mountains, characters, and more.
Formula: Mesh = Geometry + Material
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Geometry │ + │ Material │ = │ Mesh │
│ (Shape) │ │ (Appearance)│ │ (Visible) │
└─────────────┘ └─────────────┘ └─────────────┘Creating a Basic Mesh
// 1. Create geometry (shape)
const geometry = new BoxBufferGeometry(2, 2, 2); // width, height, depth
// 2. Create material (appearance)
const material = new MeshBasicMaterial({ color: 0x00ff00 }); // Green
// 3. Combine into mesh
const cube = new Mesh(geometry, material);
// 4. Add to scene
scene.add(cube);Common Geometries
// Box
const box = new BoxBufferGeometry(width, height, depth);
// Sphere
const sphere = new SphereBufferGeometry(
radius, // 1
widthSegments, // 32 (more = smoother)
heightSegments // 32
);
// Cylinder
const cylinder = new CylinderBufferGeometry(
radiusTop, // 1
radiusBottom, // 1
height, // 2
radialSegments // 32
);
// Plane (flat surface)
const plane = new PlaneBufferGeometry(width, height);
// Torus (donut)
const torus = new TorusBufferGeometry(
radius, // 1
tube, // 0.4
radialSegments, // 16
tubularSegments // 100
);
// Cone
const cone = new ConeBufferGeometry(radius, height, radialSegments);Pro Tip: Use BufferGeometry versions - they're more efficient!
Material Types Overview
1. MeshBasicMaterial (No Lighting Required) 💡
const material = new MeshBasicMaterial({
color: 0xff0000, // Red
wireframe: false, // Show as solid
transparent: false,
opacity: 1.0
});Characteristics:
- ✅ Fastest rendering
- ✅ Ignores all lights
- ✅ Flat, unshaded appearance
- ✅ Perfect for debugging
- ❌ Not realistic
2. MeshLambertMaterial (Basic Lighting) 🔆
const material = new MeshLambertMaterial({
color: 0x00ff00,
emissive: 0x000000, // Self-illumination
});Characteristics:
- ✅ Fast rendering
- ✅ Requires lights
- ✅ Matte appearance (no shine)
- ❌ No specular highlights
3. MeshPhongMaterial (Shiny Surfaces) ✨
const material = new MeshPhongMaterial({
color: 0x0000ff,
specular: 0x111111, // Highlight color
shininess: 30, // Shininess intensity
});Characteristics:
- ✅ Realistic highlights
- ✅ Good for plastic, polished surfaces
- ⚠️ Slower than Lambert
4. MeshStandardMaterial (PBR - Best Quality) 🎨
const material = new MeshStandardMaterial({
color: 0xffffff,
metalness: 0.5, // 0 = non-metal, 1 = metal
roughness: 0.5, // 0 = smooth, 1 = rough
map: colorTexture, // Color texture
normalMap: normalTexture,
roughnessMap: roughnessTexture,
});Characteristics:
- ✅ Physically Based Rendering (PBR)
- ✅ Most realistic
- ✅ Industry standard
- ⚠️ Requires good lighting setup
- ⚠️ Slower than Basic/Lambert
Mesh Properties and Methods
const mesh = new Mesh(geometry, material);
// Position
mesh.position.set(x, y, z);
mesh.position.x = 5;
// Rotation (in radians)
mesh.rotation.set(x, y, z);
mesh.rotation.y = Math.PI / 4; // 45 degrees
// Scale
mesh.scale.set(2, 2, 2); // Double size
mesh.scale.x = 0.5; // Half width
// Visibility
mesh.visible = true;
// Casting/receiving shadows
mesh.castShadow = true;
mesh.receiveShadow = true;
// Name (for finding later)
mesh.name = 'myCube';
// Parent-child relationships
mesh.add(childMesh);
// World position (accounting for parent transforms)
const worldPosition = new Vector3();
mesh.getWorldPosition(worldPosition);Multiple Meshes with Shared Geometry/Material
Performance Tip: Reuse geometries and materials to save memory!
// ✅ Good: One geometry, one material, multiple meshes
const geometry = new BoxBufferGeometry(1, 1, 1);
const material = new MeshStandardMaterial({ color: 0x00ff00 });
const cube1 = new Mesh(geometry, material);
const cube2 = new Mesh(geometry, material);
const cube3 = new Mesh(geometry, material);
cube1.position.x = -2;
cube2.position.x = 0;
cube3.position.x = 2;
scene.add(cube1, cube2, cube3);
// ❌ Bad: Creating new geometry and material for each mesh
const cube1 = new Mesh(
new BoxBufferGeometry(1, 1, 1),
new MeshStandardMaterial({ color: 0x00ff00 })
);
// ... wasteful!Anti-aliasing for Quality
What is Aliasing?
Aliasing creates "jagged" or "stair-step" edges on diagonal lines and curves:
Without Anti-aliasing: With Anti-aliasing:
████ ▓▓██
████ ▓▓▓▓██
████ ▓▓▓▓▓▓██
████ ▓▓▓▓██
████ ▓▓██Enabling Anti-aliasing
// ✅ Enable when creating renderer
const renderer = new WebGLRenderer({
antialias: true
});
// ❌ Cannot be changed after creation
// To change, you must create a new rendererImportant Notes:
- ⚠️ Cannot be changed after renderer creation
- ⚡ Slight performance cost (usually worth it)
- 🎨 Makes edges appear smoother
- 📱 More noticeable on lower-DPI displays
Anti-aliasing Method
Three.js uses MSAA (Multisample Anti-Aliasing) built into WebGL:
- Samples multiple points per pixel
- Averages colors for smooth edges
- Hardware-accelerated
- Automatic - no manual configuration needed
Reference: Multisample Anti-aliasing (Wikipedia)
When to Use Anti-aliasing
✅ Use anti-aliasing when:
- Quality is important
- You have performance headroom
- Rendering on desktop
- Creating presentations or marketing materials
❌ Consider disabling when:
- Performance is critical (mobile, low-end devices)
- Already rendering at very high resolution
- Using post-processing effects (they may override it)
Alternative: Post-Processing AA
// For more control, use post-processing anti-aliasing
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new SMAAPass(width, height));
// In render loop
composer.render();Modular Architecture
Why Modularize?
As your application grows, keeping everything in one file becomes unmaintainable. Let's restructure our code into clean, reusable modules.
Improved Project Structure
my-threejs-app/
├── index.html
├── styles/
│ └── main.css
└── src/
├── main.js (Entry point)
└── World/
├── World.js (Main app class)
├── components/ (Things in the scene)
│ ├── camera.js
│ ├── cube.js
│ ├── scene.js
│ └── lights.js
└── systems/ (Things that operate on components)
├── renderer.js
├── Resizer.js
└── Loop.jsOrganization Philosophy:
- Components: Things that exist in the scene (camera, meshes, lights, scene)
- Systems: Things that operate on components (renderer, resizer, animation loop)
The Entry Point: main.js
Goal: Hide implementation details. The main file should know nothing about three.js internals.
import { World } from './World/World.js';
function main() {
// 1. Get container reference
const container = document.querySelector('#scene-container');
// 2. Create World instance
const world = new World(container);
// 3. Render the scene
world.render();
}
// Run when DOM is ready
main();Key Benefits:
- ✅ Clean separation of concerns
- ✅ Easy to understand at a glance
- ✅ No three.js internals exposed
- ✅ Easy to extend with new features
The World Class: World.js
Central orchestrator that creates and manages all components and systems.
import { createCamera } from './components/camera.js';
import { createCube } from './components/cube.js';
import { createScene } from './components/scene.js';
import { createLights } from './components/lights.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
// Module-scoped variables (private to this module)
let camera;
let scene;
let renderer;
class World {
constructor(container) {
// Create core components
camera = createCamera();
scene = createScene();
renderer = createRenderer();
// Add renderer's canvas to container
container.append(renderer.domElement);
// Create scene objects
const cube = createCube();
const light = createLights();
// Add objects to scene
scene.add(cube, light);
// Setup resizing system
const resizer = new Resizer(container, camera, renderer);
// Hook into resize events
resizer.onResize = () => {
this.render();
};
}
// Public method to render the scene
render() {
renderer.render(scene, camera);
}
}
export { World };Design Patterns:
- 🔒 Module-scoped variables keep three.js objects private
- 🏗️ Constructor assembles the scene
- 📢 Public API exposes only necessary methods
- 🔧 Easy to extend with new features
Component: camera.js
import { PerspectiveCamera } from 'three';
function createCamera() {
const camera = new PerspectiveCamera(
35, // fov = Field Of View
1, // aspect ratio (will be set by Resizer)
0.1, // near clipping plane
100 // far clipping plane
);
// Position camera back from origin
camera.position.set(0, 0, 10);
return camera;
}
export { createCamera };Why aspect ratio = 1?
- We don't have container dimensions yet
- The
Resizersystem will set the correct aspect ratio - Avoids unnecessary coupling to container
Component: cube.js
import {
BoxBufferGeometry,
Mesh,
MeshStandardMaterial
} from 'three';
function createCube() {
// Create geometry
const geometry = new BoxBufferGeometry(2, 2, 2);
// Create material (now using Standard for better appearance)
const material = new MeshStandardMaterial({
color: 0x44aa88 // Teal color
});
// Create mesh
const cube = new Mesh(geometry, material);
return cube;
}
export { createCube };Improvements:
- 📦 Self-contained, reusable function
- 🎨 Switched to MeshStandardMaterial for better realism
- 🔄 Easy to create multiple cubes
Component: scene.js
import { Color, Scene } from 'three';
function createScene() {
const scene = new Scene();
// Set background color
scene.background = new Color('skyblue');
return scene;
}
export { createScene };Component: lights.js
import { DirectionalLight, HemisphereLight } from 'three';
function createLights() {
// Soft ambient light (sky and ground colors)
const ambientLight = new HemisphereLight(
'white', // sky color
'darkslategrey', // ground color
5 // intensity
);
// Main directional light (like the sun)
const mainLight = new DirectionalLight('white', 5);
mainLight.position.set(10, 10, 10);
return { ambientLight, mainLight };
}
export { createLights };Note: Returns an object with multiple lights for easier management.
System: renderer.js
import { WebGLRenderer } from 'three';
function createRenderer() {
const renderer = new WebGLRenderer({
antialias: true
});
// Enable physically correct lighting
// Makes lights behave more realistically
renderer.physicallyCorrectLights = true;
return renderer;
}
export { createRenderer };Why physicallyCorrectLights?
- Makes light intensity calculations more realistic
- Better for physically-based materials
- Slight performance cost, but worth it for quality
System: Resizer.js
Handles window resizing and keeps everything properly scaled.
// Helper function to update sizes
const setSize = (container, camera, renderer) => {
// Update camera aspect ratio
camera.aspect = container.clientWidth / container.clientHeight;
// CRITICAL: Update camera's frustum
camera.updateProjectionMatrix();
// Update renderer size
renderer.setSize(container.clientWidth, container.clientHeight);
// Update pixel ratio for crisp rendering
renderer.setPixelRatio(window.devicePixelRatio);
};
class Resizer {
constructor(container, camera, renderer) {
// Set initial size on page load
setSize(container, camera, renderer);
// Listen for window resize events
window.addEventListener('resize', () => {
// Update sizes
setSize(container, camera, renderer);
// Call custom resize handler
this.onResize();
});
}
// Override this method to perform custom actions on resize
onResize() {}
}
export { Resizer };Key Concepts:
- Initial Sizing: Sets correct size when page loads
- Responsive: Updates when window is resized
- Camera Update: Must call
updateProjectionMatrix()after changing aspect - Hooks:
onResize()method can be overridden for custom behavior
Why updateProjectionMatrix()?
The camera's viewing frustum is calculated from:
camera.fovcamera.aspectcamera.nearcamera.far
When we change camera.aspect, we must tell the camera to recalculate its frustum:
camera.aspect = newAspect;
camera.updateProjectionMatrix(); // ⚠️ Required!Without this call, the camera continues using the old frustum, resulting in stretched or squashed rendering.
Best Practices
1. Module Organization 📁
// ✅ Good: Clear separation
components/ // Things that exist
├── camera.js
├── cube.js
└── scene.js
systems/ // Things that act
├── renderer.js
└── Loop.js
// ❌ Bad: Everything in one file
main.js // 1000 lines of code2. Resource Management 🗑️
// Always dispose of resources when done
geometry.dispose();
material.dispose();
texture.dispose();
renderer.dispose();
// For materials with maps
material.map?.dispose();
material.normalMap?.dispose();
material.roughnessMap?.dispose();3. Performance ⚡
// ✅ Reuse geometries and materials
const geometry = new BoxBufferGeometry(1, 1, 1);
const material = new MeshStandardMaterial({ color: 0x00ff00 });
for (let i = 0; i < 100; i++) {
const mesh = new Mesh(geometry, material);
mesh.position.x = i;
scene.add(mesh);
}
// ❌ Creating new resources for each object
for (let i = 0; i < 100; i++) {
const mesh = new Mesh(
new BoxBufferGeometry(1, 1, 1),
new MeshStandardMaterial({ color: 0x00ff00 })
);
scene.add(mesh);
}4. Naming Conventions 🏷️
// Classes: PascalCase
class World {}
class Resizer {}
// Functions: camelCase
function createCamera() {}
function updateScene() {}
// Constants: UPPER_SNAKE_CASE
const MAX_PARTICLES = 1000;
const DEFAULT_COLOR = 0x00ff00;
// Files matching exports
// World.js exports class World
// camera.js exports createCamera function5. Error Handling 🛡️
// Check for WebGL support
function checkWebGLSupport() {
try {
const canvas = document.createElement('canvas');
return !!(
window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
);
} catch (e) {
return false;
}
}
if (!checkWebGLSupport()) {
alert('Your browser does not support WebGL');
}6. Responsive Design 📱
// Handle different screen sizes
const resizer = new Resizer(container, camera, renderer);
// Optional: Debounce resize for performance
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
setSize(container, camera, renderer);
}, 250);
});7. Development Helpers 🔧
// Add helpers during development
import { AxesHelper, GridHelper } from 'three';
// Show coordinate axes (RGB = XYZ)
const axesHelper = new AxesHelper(5);
scene.add(axesHelper);
// Show ground grid
const gridHelper = new GridHelper(10, 10);
scene.add(gridHelper);
// Remove in production
if (process.env.NODE_ENV === 'development') {
scene.add(axesHelper, gridHelper);
}Complete Working Example
Here's a complete, production-ready example using the modular architecture:
src/main.js
import { World } from './World/World.js';
async function main() {
const container = document.querySelector('#scene-container');
const world = new World(container);
// Start rendering
world.start();
}
main().catch((err) => {
console.error('Failed to initialize:', err);
});src/World/World.js
import { createCamera } from './components/camera.js';
import { createCube } from './components/cube.js';
import { createScene } from './components/scene.js';
import { createLights } from './components/lights.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
import { Loop } from './systems/Loop.js';
let camera;
let renderer;
let scene;
let loop;
class World {
constructor(container) {
camera = createCamera();
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
const cube = createCube();
const lights = createLights();
// Enable animation
loop.updatables.push(cube);
scene.add(cube, lights.ambientLight, lights.mainLight);
const resizer = new Resizer(container, camera, renderer);
}
render() {
renderer.render(scene, camera);
}
start() {
loop.start();
}
stop() {
loop.stop();
}
}
export { World };This modular architecture is:
- ✅ Scalable
- ✅ Maintainable
- ✅ Testable
- ✅ Professional
Next Steps
- Add Animation - Make objects move and rotate
- Add Interactivity - Mouse controls and user input
- Load 3D Models - Import external models
- Add Textures - Make objects look realistic
- Implement Lighting - Create atmosphere and mood
Continue learning: Check out the Three.js documentation and explore the official examples!